Browse Source

chore(backend): added eslint and prettier, along with changing all files to conform

Signed-off-by: Jonathan <theflametrooper@gmail.com>
Jonathan 3 years ago
parent
commit
7e01f2bdaa
61 changed files with 22083 additions and 16569 deletions
  1. 1 0
      backend/.eslintignore
  2. 36 0
      backend/.eslintrc
  3. 1 0
      backend/.prettierignore
  4. 9 0
      backend/.prettierrc
  5. 41 42
      backend/classes/Timer.class.js
  6. 211 199
      backend/core.js
  7. 173 164
      backend/index.js
  8. 5 0
      backend/loadEnvVariables.js
  9. 67 91
      backend/logic/actions/activities.js
  10. 181 192
      backend/logic/actions/apis.js
  11. 42 56
      backend/logic/actions/hooks/adminRequired.js
  12. 6 6
      backend/logic/actions/hooks/index.js
  13. 32 46
      backend/logic/actions/hooks/loginRequired.js
  14. 62 71
      backend/logic/actions/hooks/ownerRequired.js
  15. 23 13
      backend/logic/actions/index.js
  16. 171 211
      backend/logic/actions/news.js
  17. 1106 1177
      backend/logic/actions/playlists.js
  18. 142 155
      backend/logic/actions/punishments.js
  19. 350 368
      backend/logic/actions/queueSongs.js
  20. 281 331
      backend/logic/actions/reports.js
  21. 915 1027
      backend/logic/actions/songs.js
  22. 2113 2391
      backend/logic/actions/stations.js
  23. 1852 2065
      backend/logic/actions/users.js
  24. 79 91
      backend/logic/actions/utils.js
  25. 64 70
      backend/logic/activities.js
  26. 238 252
      backend/logic/api.js
  27. 453 535
      backend/logic/app.js
  28. 255 257
      backend/logic/cache/index.js
  29. 4 8
      backend/logic/cache/schemas/officialPlaylist.js
  30. 3 7
      backend/logic/cache/schemas/playlist.js
  31. 7 5
      backend/logic/cache/schemas/punishment.js
  32. 6 10
      backend/logic/cache/schemas/session.js
  33. 1 5
      backend/logic/cache/schemas/song.js
  34. 3 7
      backend/logic/cache/schemas/station.js
  35. 227 324
      backend/logic/db/index.js
  36. 16 12
      backend/logic/db/schemas/activity.js
  37. 1 1
      backend/logic/db/schemas/news.js
  38. 1 1
      backend/logic/db/schemas/playlist.js
  39. 2 2
      backend/logic/db/schemas/punishment.js
  40. 1 1
      backend/logic/db/schemas/queueSong.js
  41. 8 6
      backend/logic/db/schemas/report.js
  42. 2 2
      backend/logic/db/schemas/song.js
  43. 16 14
      backend/logic/db/schemas/station.js
  44. 2 2
      backend/logic/db/schemas/user.js
  45. 96 108
      backend/logic/discord.js
  46. 313 377
      backend/logic/io.js
  47. 39 45
      backend/logic/mail/index.js
  48. 18 20
      backend/logic/mail/schemas/passwordRequest.js
  49. 18 20
      backend/logic/mail/schemas/resetPasswordRequest.js
  50. 22 24
      backend/logic/mail/schemas/verifyEmail.js
  51. 244 265
      backend/logic/notifications.js
  52. 286 295
      backend/logic/playlists.js
  53. 297 303
      backend/logic/punishments.js
  54. 256 248
      backend/logic/songs.js
  55. 85 105
      backend/logic/spotify.js
  56. 967 1192
      backend/logic/stations.js
  57. 259 318
      backend/logic/tasks.js
  58. 791 806
      backend/logic/utils.js
  59. 9170 2223
      backend/package-lock.json
  60. 12 3
      backend/package.json
  61. 1 0
      frontend/src/api/auth.js

+ 1 - 0
backend/.eslintignore

@@ -0,0 +1 @@
+node_modules

+ 36 - 0
backend/.eslintrc

@@ -0,0 +1,36 @@
+{
+	"env": {
+		"browser": false,
+        "es2021": true,
+        "node": true
+	},
+	"parserOptions": {
+		"ecmaVersion": 2021,
+		"sourceType": "module"
+	},
+	"extends": [
+		"eslint:recommended",
+		"airbnb-base",
+		"prettier",
+		"plugin:jsdoc/recommended"
+    ],
+    "plugins": [ "prettier", "jsdoc" ],
+	"rules": {
+		"no-console": 0,
+		"no-control-regex": 0,
+		"no-var": 2,
+		"no-underscore-dangle": 0,
+		"radix": 0,
+		"no-multi-assign": 0,
+		"no-shadow": 0,
+		"no-new": 0,
+        "import/no-unresolved": 0,
+		"prettier/prettier": ["error"], // end of copied frontend rules
+		"max-classes-per-file": 0,
+		"max-len": ["error", { "code": 140, "ignoreComments": true, "ignoreUrls": true, "ignoreTemplateLiterals": true }],
+		"no-param-reassign": 0,
+		"implicit-arrow-linebreak": 0,
+		"import/extensions": 0,
+		"class-methods-use-this": 0
+    }
+}

+ 1 - 0
backend/.prettierignore

@@ -0,0 +1 @@
+node_modules/

+ 9 - 0
backend/.prettierrc

@@ -0,0 +1,9 @@
+{
+    "singleQuote": false,
+    "tabWidth": 4,
+    "useTabs": true,
+    "trailingComma": "none",
+    "arrowParens": "avoid",
+    "endOfLine":"auto",
+    "printWidth": 120
+}

+ 41 - 42
backend/classes/Timer.class.js

@@ -1,48 +1,47 @@
-module.exports = class Timer {
-    constructor(callback, delay, paused) {
-        this.callback = callback;
-        this.timerId = undefined;
-        this.start = undefined;
-        this.paused = paused;
-        this.remaining = delay;
-        this.timeWhenPaused = 0;
-        this.timePaused = Date.now();
+export default class Timer {
+	constructor(callback, delay, paused) {
+		this.callback = callback;
+		this.timerId = undefined;
+		this.start = undefined;
+		this.paused = paused;
+		this.remaining = delay;
+		this.timeWhenPaused = 0;
+		this.timePaused = Date.now();
 
-        if (!paused) {
-            this.resume();
-        }
-    }
+		if (!paused) {
+			this.resume();
+		}
+	}
 
-    pause() {
-        clearTimeout(this.timerId);
-        this.remaining -= Date.now() - this.start;
-        this.timePaused = Date.now();
-        this.paused = true;
-    }
+	pause() {
+		clearTimeout(this.timerId);
+		this.remaining -= Date.now() - this.start;
+		this.timePaused = Date.now();
+		this.paused = true;
+	}
 
-    ifNotPaused() {
-        if (!this.paused) {
-            this.resume();
-        }
-    }
+	ifNotPaused() {
+		if (!this.paused) {
+			this.resume();
+		}
+	}
 
-    resume() {
-        this.start = Date.now();
-        clearTimeout(this.timerId);
-        this.timerId = setTimeout(this.callback, this.remaining);
-        this.timeWhenPaused = Date.now() - this.timePaused;
-        this.paused = false;
-    }
+	resume() {
+		this.start = Date.now();
+		clearTimeout(this.timerId);
+		this.timerId = setTimeout(this.callback, this.remaining);
+		this.timeWhenPaused = Date.now() - this.timePaused;
+		this.paused = false;
+	}
 
-    resetTimeWhenPaused() {
-        this.timeWhenPaused = 0;
-    }
+	resetTimeWhenPaused() {
+		this.timeWhenPaused = 0;
+	}
 
-    getTimePaused() {
-        if (!this.paused) {
-            return this.timeWhenPaused;
-        } else {
-            return Date.now() - this.timePaused;
-        }
-    }
-};
+	getTimePaused() {
+		if (!this.paused) {
+			return this.timeWhenPaused;
+		}
+		return Date.now() - this.timePaused;
+	}
+}

+ 211 - 199
backend/core.js

@@ -1,207 +1,219 @@
-const async = require("async");
-const config = require("config");
+import async from "async";
+import config from "config";
 
 class DeferredPromise {
-    constructor() {
-        this.promise = new Promise((resolve, reject) => {
-            this.reject = reject;
-            this.resolve = resolve;
-        });
-    }
+	constructor() {
+		this.promise = new Promise((resolve, reject) => {
+			this.reject = reject;
+			this.resolve = resolve;
+		});
+	}
 }
 
 class MovingAverageCalculator {
-    constructor() {
-        this.count = 0;
-        this._mean = 0;
-    }
-
-    update(newValue) {
-        this.count++;
-        const differential = (newValue - this._mean) / this.count;
-        this._mean += differential;
-    }
-
-    get mean() {
-        this.validate();
-        return this._mean;
-    }
-
-    validate() {
-        if (this.count === 0) throw new Error("Mean is undefined");
-    }
+	constructor() {
+		this.count = 0;
+		this._mean = 0;
+	}
+
+	update(newValue) {
+		this.count += 1;
+		const differential = (newValue - this._mean) / this.count;
+		this._mean += differential;
+	}
+
+	get mean() {
+		this.validate();
+		return this._mean;
+	}
+
+	validate() {
+		if (this.count === 0) throw new Error("Mean is undefined");
+	}
 }
 
-class CoreClass {
-    constructor(name) {
-        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.pause();
-        this.runningJobs = [];
-        this.priorities = {};
-        this.stage = 0;
-        this.jobStatistics = {};
-
-        this.registerJobs();
-    }
-
-    setStatus(status) {
-        this.status = status;
-        this.log("INFO", `Status changed to: ${status}`);
-        if (this.status === "READY") this.jobQueue.resume();
-        else if (this.status === "FAIL" || this.status === "LOCKDOWN")
-            this.jobQueue.pause();
-    }
-
-    getStatus() {
-        return this.status;
-    }
-
-    setStage(stage) {
-        this.stage = stage;
-    }
-
-    getStage() {
-        return this.stage;
-    }
-
-    _initialize() {
-        this.setStatus("INITIALIZING");
-        this.initialize()
-            .then(() => {
-                this.setStatus("READY");
-                this.moduleManager.onInitialize(this);
-            })
-            .catch((err) => {
-                console.error(err);
-                this.setStatus("FAILED");
-                this.moduleManager.onFail(this);
-            });
-    }
-
-    log() {
-        let _arguments = Array.from(arguments);
-        const type = _arguments[0];
-
-        if (config.debug && config.debug.stationIssue === true && type === "STATION_ISSUE") {
-            this.moduleManager.debugLogs.stationIssue.push(_arguments);
-            return;
-        }
-
-        _arguments.splice(0, 1);
-        const start = `|${this.name.toUpperCase()}|`;
-        const numberOfTabsNeeded = 4 - Math.ceil(start.length / 8);
-        _arguments.unshift(`${start}${Array(numberOfTabsNeeded).join("\t")}`);
-
-        if (type === "INFO") {
-            _arguments[0] = _arguments[0] + "\x1b[36m";
-            _arguments.push("\x1b[0m");
-            console.log.apply(null, _arguments);
-        } else if (type === "ERROR") {
-            _arguments[0] = _arguments[0] + "\x1b[31m";
-            _arguments.push("\x1b[0m");
-            console.error.apply(null, _arguments);
-        }
-    }
-
-    registerJobs() {
-        let props = [];
-        let obj = this;
-        do {
-            props = props.concat(Object.getOwnPropertyNames(obj));
-        } while ((obj = Object.getPrototypeOf(obj)));
-
-        const jobNames = props
-            .sort()
-            .filter(
-                (prop) =>
-                    typeof this[prop] == "function" &&
-                    prop === prop.toUpperCase()
-            );
-
-        jobNames.forEach((jobName) => {
-            this.jobStatistics[jobName] = {
-                successful: 0,
-                failed: 0,
-                total: 0,
-                averageTiming: new MovingAverageCalculator(),
-            };
-        });
-    }
-
-    runJob(name, payload, options = { isQuiet: false, bypassQueue: false }) {
-        let deferredPromise = new DeferredPromise();
-        const job = { name, payload, onFinish: deferredPromise };
-
-        if (config.debug && config.debug.stationIssue === true && config.debug.captureJobs && config.debug.captureJobs.indexOf(name) !== -1) {
-            this.moduleManager.debugJobs.all.push(job);
-        }
-
-        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;
-    }
-
-    setModuleManager(moduleManager) {
-        this.moduleManager = moduleManager;
-    }
-
-    _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 === 2) args.push({});
-            args[2].bypassQueue = true;
-            return this.runJob.apply(this, 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++;
-                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 });
-                }
-                job.onFinish.resolve(response);
-            })
-            .catch((error) => {
-                this.log("INFO", `Running job ${job.name} failed`);
-                this.jobStatistics[job.name].failed++;
-                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++;
-                this.jobStatistics[job.name].averageTiming.update(
-                    executionTime
-                );
-                this.runningJobs.splice(this.runningJobs.indexOf(job), 1);
-                cb();
-            });
-    }
+export default class CoreClass {
+	constructor(name) {
+		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.pause();
+		this.runningJobs = [];
+		this.priorities = {};
+		this.stage = 0;
+		this.jobStatistics = {};
+
+		this.registerJobs();
+	}
+
+	setStatus(status) {
+		this.status = status;
+		this.log("INFO", `Status changed to: ${status}`);
+		if (this.status === "READY") this.jobQueue.resume();
+		else if (this.status === "FAIL" || this.status === "LOCKDOWN") this.jobQueue.pause();
+	}
+
+	getStatus() {
+		return this.status;
+	}
+
+	setStage(stage) {
+		this.stage = stage;
+	}
+
+	getStage() {
+		return this.stage;
+	}
+
+	_initialize() {
+		this.setStatus("INITIALIZING");
+
+		this.initialize()
+			.then(() => {
+				this.setStatus("READY");
+				this.moduleManager.onInitialize(this);
+			})
+			.catch(err => {
+				console.error(err);
+				this.setStatus("FAILED");
+				this.moduleManager.onFail(this);
+			});
+	}
+
+	log(...args) {
+		const _arguments = Array.from(args);
+		const type = _arguments[0];
+
+		if (config.debug && config.debug.stationIssue === true && type === "STATION_ISSUE") {
+			this.moduleManager.debugLogs.stationIssue.push(_arguments);
+			return;
+		}
+
+		_arguments.splice(0, 1);
+		const start = `|${this.name.toUpperCase()}|`;
+		const numberOfTabsNeeded = 4 - Math.ceil(start.length / 8);
+		_arguments.unshift(`${start}${Array(numberOfTabsNeeded).join("\t")}`);
+
+		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);
+		}
+	}
+
+	registerJobs() {
+		let props = [];
+		let obj = this;
+		do {
+			props = props.concat(Object.getOwnPropertyNames(obj));
+			// eslint-disable-next-line no-cond-assign
+		} while ((obj = Object.getPrototypeOf(obj)));
+
+		const jobNames = props.sort().filter(prop => typeof this[prop] === "function" && prop === prop.toUpperCase());
+
+		jobNames.forEach(jobName => {
+			this.jobStatistics[jobName] = {
+				successful: 0,
+				failed: 0,
+				total: 0,
+				averageTiming: new MovingAverageCalculator()
+			};
+		});
+	}
+
+	runJob(name, payload, options = { isQuiet: false, bypassQueue: false }) {
+		const deferredPromise = new DeferredPromise();
+		const job = { name, payload, onFinish: deferredPromise };
+
+		if (
+			config.debug &&
+			config.debug.stationIssue === true &&
+			config.debug.captureJobs &&
+			config.debug.captureJobs.indexOf(name) !== -1
+		) {
+			this.moduleManager.debugJobs.all.push(job);
+		}
+
+		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;
+	}
+
+	setModuleManager(moduleManager) {
+		this.moduleManager = moduleManager;
+	}
+
+	_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
+					});
+				}
+				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();
+			});
+	}
 }
-
-module.exports = CoreClass;

+ 173 - 164
backend/index.js

@@ -1,54 +1,51 @@
-"use strict";
+import "./loadEnvVariables.js";
 
-const util = require("util");
+import util from "util";
+import config from "config";
 
-process.env.NODE_CONFIG_DIR = `${__dirname}/config`;
-
-const config = require("config");
-
-process.on("uncaughtException", (err) => {
-    if (err.code === "ECONNREFUSED" || err.code === "UNCERTAIN_STATE") return;
-    console.log(`UNCAUGHT EXCEPTION: ${err.stack}`);
+process.on("uncaughtException", err => {
+	if (err.code === "ECONNREFUSED" || err.code === "UNCERTAIN_STATE") return;
+	console.log(`UNCAUGHT EXCEPTION: ${err.stack}`);
 });
 
 const blacklistedConsoleLogs = [
-    "Running job IO",
-    "Ran job IO successfully",
-    "Running job HGET",
-    "Ran job HGET successfully",
-    "Running job HGETALL",
-    "Ran job HGETALL successfully",
-    "Running job GET_ERROR",
-    "Ran job GET_ERROR successfully",
-    "Running job GET_SCHEMA",
-    "Ran job GET_SCHEMA successfully",
-    "Running job SUB",
-    "Ran job SUB successfully",
-    "Running job GET_MODEL",
-    "Ran job GET_MODEL successfully",
-    "Running job HSET",
-    "Ran job HSET successfully",
-    "Running job CAN_USER_VIEW_STATION",
-    "Ran job CAN_USER_VIEW_STATION successfully",
+	"Running job IO",
+	"Ran job IO successfully",
+	"Running job HGET",
+	"Ran job HGET successfully",
+	"Running job HGETALL",
+	"Ran job HGETALL successfully",
+	"Running job GET_ERROR",
+	"Ran job GET_ERROR successfully",
+	"Running job GET_SCHEMA",
+	"Ran job GET_SCHEMA successfully",
+	"Running job SUB",
+	"Ran job SUB successfully",
+	"Running job GET_MODEL",
+	"Ran job GET_MODEL successfully",
+	"Running job HSET",
+	"Ran job HSET successfully",
+	"Running job CAN_USER_VIEW_STATION",
+	"Ran job CAN_USER_VIEW_STATION successfully"
 ];
 
 const oldConsole = {};
 oldConsole.log = console.log;
 
 console.log = (...args) => {
-    const string = util.format.apply(null, args);
-    let blacklisted = false;
-    blacklistedConsoleLogs.forEach((blacklistedConsoleLog) => {
-        if (string.indexOf(blacklistedConsoleLog) !== -1) blacklisted = true;
-    });
-    if (!blacklisted) oldConsole.log.apply(null, args);
+	const string = util.format.apply(null, args);
+	let blacklisted = false;
+	blacklistedConsoleLogs.forEach(blacklistedConsoleLog => {
+		if (string.indexOf(blacklistedConsoleLog) !== -1) blacklisted = true;
+	});
+	if (!blacklisted) oldConsole.log.apply(null, args);
 };
 
 const fancyConsole = config.get("fancyConsole");
 
 if (config.debug && config.debug.traceUnhandledPromises === true) {
-    console.log("Enabled trace-unhandled/register");
-    require("trace-unhandled/register");
+	console.log("Enabled trace-unhandled/register");
+	import("trace-unhandled/register");
 }
 
 // class ModuleManager {
@@ -234,97 +231,107 @@ if (config.debug && config.debug.traceUnhandledPromises === true) {
 // }
 
 class ModuleManager {
-    constructor() {
-        this.modules = {};
-        this.modulesNotInitialized = [];
-        this.i = 0;
-        this.lockdown = false;
-        this.fancyConsole = fancyConsole;
-        this.debugLogs = {
-            stationIssue: []
-        };
-        this.debugJobs = {
-            all: [],
-            completed: []
-        };
-    }
-
-    addModule(moduleName) {
-        console.log("add module", moduleName);
-        const module = require(`./logic/${moduleName}`);
-        this.modules[moduleName] = module;
-        this.modulesNotInitialized.push(module);
-    }
-
-    initialize() {
-        // if (!this.modules["logger"]) return console.error("There is no logger module");
-        // this.logger = this.modules["logger"];
-        // if (this.fancyConsole) {
-        // this.replaceConsoleWithLogger();
-        this.reservedLines = Object.keys(this.modules).length + 5;
-        // }
-
-        for (let moduleName in this.modules) {
-            let module = this.modules[moduleName];
-            module.setModuleManager(this);
-
-            if (this.lockdown) break;
-
-            module._initialize();
-
-            // let dependenciesInitializedPromises = [];
-
-            // module.dependsOn.forEach(dependencyName => {
-            // 	let dependency = this.modules[dependencyName];
-            // 	dependenciesInitializedPromises.push(dependency._onInitialize());
-            // });
-
-            // module.lastTime = Date.now();
-
-            // Promise.all(dependenciesInitializedPromises).then((res, res2) => {
-            // 	if (this.lockdown) return;
-            // 	this.logger.info("MODULE_MANAGER", `${moduleName} dependencies have been completed`);
-            // 	module._initialize();
-            // });
-        }
-    }
-
-    onInitialize(module) {
-        if (this.modulesNotInitialized.indexOf(module) !== -1) {
-            this.modulesNotInitialized.splice(
-                this.modulesNotInitialized.indexOf(module),
-                1
-            );
-
-            console.log(
-                "MODULE_MANAGER",
-                `Initialized: ${Object.keys(this.modules).length -
-                    this.modulesNotInitialized.length}/${
-                    Object.keys(this.modules).length
-                }.`
-            );
-
-            if (this.modulesNotInitialized.length === 0)
-                this.onAllModulesInitialized();
-        }
-    }
-
-    onFail(module) {
-        if (this.modulesNotInitialized.indexOf(module) !== -1) {
-            console.log("A module failed to initialize!");
-        }
-    }
-
-    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: [],
-        });
-    }
+	constructor() {
+		this.modules = {};
+		this.modulesNotInitialized = [];
+		this.i = 0;
+		this.lockdown = false;
+		this.fancyConsole = fancyConsole;
+		this.debugLogs = {
+			stationIssue: []
+		};
+		this.debugJobs = {
+			all: [],
+			completed: []
+		};
+	}
+
+	async addModule(moduleName) {
+		console.log("add module", moduleName);
+		// import(`./logic/${moduleName}`).then(Module => {
+		// 	// eslint-disable-next-line new-cap
+
+		// 	const instantiatedModule = new Module.default();
+		// 	this.modules[moduleName] = instantiatedModule;
+		// 	this.modulesNotInitialized.push(instantiatedModule);
+		// 	if (moduleName === "cache") console.log(56, this.modules);
+		// });
+
+		this.modules[moduleName] = import(`./logic/${moduleName}`);
+	}
+
+	async initialize() {
+		// if (!this.modules["logger"]) return console.error("There is no logger module");
+		// this.logger = this.modules["logger"];
+		// if (this.fancyConsole) {
+		// this.replaceConsoleWithLogger();
+		this.reservedLines = Object.keys(this.modules).length + 5;
+		// }
+
+		await Promise.all(Object.values(this.modules)).then(modules => {
+			for (let module = 0; module < modules.length; module += 1) {
+				this.modules[modules[module].default.name] = modules[module].default;
+				this.modulesNotInitialized.push(modules[module].default);
+			}
+		}); // 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]];
+
+			module.setModuleManager(this);
+
+			if (this.lockdown) break;
+
+			module._initialize();
+
+			// let dependenciesInitializedPromises = [];
+
+			// module.dependsOn.forEach(dependencyName => {
+			// 	let dependency = this.modules[dependencyName];
+			// 	dependenciesInitializedPromises.push(dependency._onInitialize());
+			// });
+
+			// module.lastTime = Date.now();
+
+			// Promise.all(dependenciesInitializedPromises).then((res, res2) => {
+			// 	if (this.lockdown) return;
+			// 	this.logger.info("MODULE_MANAGER", `${moduleName} dependencies have been completed`);
+			// 	module._initialize();
+			// });
+		}
+	}
+
+	onInitialize(module) {
+		if (this.modulesNotInitialized.indexOf(module) !== -1) {
+			this.modulesNotInitialized.splice(this.modulesNotInitialized.indexOf(module), 1);
+
+			console.log(
+				"MODULE_MANAGER",
+				`Initialized: ${Object.keys(this.modules).length - this.modulesNotInitialized.length}/${
+					Object.keys(this.modules).length
+				}.`
+			);
+
+			if (this.modulesNotInitialized.length === 0) this.onAllModulesInitialized();
+		}
+	}
+
+	onFail(module) {
+		if (this.modulesNotInitialized.indexOf(module) !== -1) {
+			console.log("A module failed to initialize!");
+		}
+	}
+
+	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: []
+		});
+	}
 }
 
 const moduleManager = new ModuleManager();
@@ -348,43 +355,45 @@ moduleManager.addModule("utils");
 
 moduleManager.initialize();
 
-process.stdin.on("data", function(data) {
-    const command = data.toString().replace(/\r?\n|\r/g, "");
-    if (command === "lockdown") {
-        console.log("Locking down.");
-        moduleManager._lockdown();
-    }
-    if (command === "status") {
-        console.log("Status:");
-
-        for (let moduleName in moduleManager.modules) {
-            let 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.jobQueue.concurrency
-                }. Stage: ${module.getStage()}`
-            );
-        }
-        // moduleManager._lockdown();
-    }
-    if (command.startsWith("running")) {
-        const parts = command
-            .split(" ");
-
-        console.log(moduleManager.modules[parts[1]].runningJobs);
-    }
-    if (command.startsWith("stats")) {
-        const parts = command
-            .split(" ");
-
-        console.log(moduleManager.modules[parts[1]].jobStatistics);
-    }
-    if (command.startsWith("debug")) {
-        moduleManager.modules["utils"].runJob("DEBUG");
-    }
+process.stdin.on("data", data => {
+	const command = data.toString().replace(/\r?\n|\r/g, "");
+	if (command === "lockdown") {
+		console.log("Locking down.");
+		moduleManager._lockdown();
+	}
+	if (command === "status") {
+		console.log("Status:");
+
+		for (
+			let moduleName = 0, moduleKeys = Object.keys(moduleManager.modules);
+			moduleName < moduleKeys.length;
+			moduleName += 1
+		) {
+			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.jobQueue.concurrency
+				}. Stage: ${module.getStage()}`
+			);
+		}
+		// moduleManager._lockdown();
+	}
+	if (command.startsWith("running")) {
+		const parts = command.split(" ");
+
+		console.log(moduleManager.modules[parts[1]].runningJobs);
+	}
+	if (command.startsWith("stats")) {
+		const parts = command.split(" ");
+
+		console.log(moduleManager.modules[parts[1]].jobStatistics);
+	}
+	if (command.startsWith("debug")) {
+		moduleManager.modules.utils.runJob("DEBUG");
+	}
 });
 
-module.exports = moduleManager;
+export default moduleManager;

+ 5 - 0
backend/loadEnvVariables.js

@@ -0,0 +1,5 @@
+import { dirname } from "path";
+import { fileURLToPath } from "url";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+process.env.NODE_CONFIG_DIR = `${__dirname}/config`;

+ 67 - 91
backend/logic/actions/activities.js

@@ -1,99 +1,75 @@
-"use strict";
+import async from "async";
 
-const async = require("async");
-
-const hooks = require("./hooks");
-
-const db = require("../db");
-const utils = require("../utils");
-const activities = require("../activities");
+import { isLoginRequired } from "./hooks";
+import db from "../db";
+import utils from "../utils";
 
 // const logger = moduleManager.modules["logger"];
 
-module.exports = {
-    /**
-     * Gets a set of activities
-     *
-     * @param session
-     * @param {String} userId - the user whose activities we are looking for
-     * @param {Integer} set - the set number to return
-     * @param cb
-     */
-    getSet: async (session, userId, set, cb) => {
-        const activityModel = await db.runJob("GET_MODEL", {
-            modelName: "activity",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    activityModel
-                        .find({ userId, hidden: false })
-                        .skip(15 * (set - 1))
-                        .limit(15)
-                        .sort("createdAt")
-                        .exec(next);
-                },
-            ],
-            async (err, activities) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "ACTIVITIES_GET_SET",
-                        `Failed to get set ${set} from activities. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
+export default {
+	/**
+	 * Gets a set of activities
+	 *
+	 * @param {object} session - user session
+	 * @param {string} userId - the user whose activities we are looking for
+	 * @param {number} set - the set number to return
+	 * @param {Function} cb - callback
+	 */
+	getSet: async (session, userId, set, cb) => {
+		const activityModel = await db.runJob("GET_MODEL", {
+			modelName: "activity"
+		});
+		async.waterfall(
+			[
+				next => {
+					activityModel
+						.find({ userId, hidden: false })
+						.skip(15 * (set - 1))
+						.limit(15)
+						.sort("createdAt")
+						.exec(next);
+				}
+			],
+			async (err, activities) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "ACTIVITIES_GET_SET", `Failed to get set ${set} from activities. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
 
-                console.log(
-                    "SUCCESS",
-                    "ACTIVITIES_GET_SET",
-                    `Set ${set} from activities obtained successfully.`
-                );
-                cb({ status: "success", data: activities });
-            }
-        );
-    },
+				console.log("SUCCESS", "ACTIVITIES_GET_SET", `Set ${set} from activities obtained successfully.`);
+				return cb({ status: "success", data: activities });
+			}
+		);
+	},
 
-    /**
-     * Hides an activity for a user
-     *
-     * @param session
-     * @param {String} activityId - the activity which should be hidden
-     * @param cb
-     */
-    hideActivity: hooks.loginRequired(async (session, activityId, cb) => {
-        const activityModel = await db.runJob("GET_MODEL", {
-            modelName: "activity",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    activityModel.updateOne(
-                        { _id: activityId },
-                        { $set: { hidden: true } },
-                        next
-                    );
-                },
-            ],
-            async (err) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "ACTIVITIES_HIDE_ACTIVITY",
-                        `Failed to hide activity ${activityId}. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
+	/**
+	 * Hides an activity for a user
+	 *
+	 * @param session
+	 * @param {string} activityId - the activity which should be hidden
+	 * @param cb
+	 */
+	hideActivity: isLoginRequired(async (session, activityId, cb) => {
+		const activityModel = await db.runJob("GET_MODEL", {
+			modelName: "activity"
+		});
+		async.waterfall(
+			[
+				next => {
+					activityModel.updateOne({ _id: activityId }, { $set: { hidden: true } }, next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "ACTIVITIES_HIDE_ACTIVITY", `Failed to hide activity ${activityId}. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
 
-                console.log(
-                    "SUCCESS",
-                    "ACTIVITIES_HIDE_ACTIVITY",
-                    `Successfully hid activity ${activityId}.`
-                );
-                cb({ status: "success" });
-            }
-        );
-    }),
+				console.log("SUCCESS", "ACTIVITIES_HIDE_ACTIVITY", `Successfully hid activity ${activityId}.`);
+				return cb({ status: "success" });
+			}
+		);
+	})
 };

+ 181 - 192
backend/logic/actions/apis.js

@@ -1,208 +1,197 @@
-"use strict";
+import config from "config";
 
-const request = require("request");
-const config = require("config");
-const async = require("async");
+import async from "async";
 
-const hooks = require("./hooks");
+import request from "request";
+import { isAdminRequired } from "./hooks";
 // const moduleManager = require("../../index");
 
-const utils = require("../utils");
+import utils from "../utils";
+
 // const logger = moduleManager.modules["logger"];
 
-module.exports = {
-    /**
-     * Fetches a list of songs from Youtubes API
-     *
-     * @param session
-     * @param query - the query we'll pass to youtubes api
-     * @param cb
-     * @return {{ status: String, data: Object }}
-     */
-    searchYoutube: (session, query, cb) => {
-        const params = [
-            "part=snippet",
-            `q=${encodeURIComponent(query)}`,
-            `key=${config.get("apis.youtube.key")}`,
-            "type=video",
-            "maxResults=15",
-        ].join("&");
+export default {
+	/**
+	 * Fetches a list of songs from Youtubes API
+	 *
+	 * @param {object} session - user session
+	 * @param {string} query - the query we'll pass to youtubes api
+	 * @param {Function} cb - callback
+	 * @returns {{status: string, data: object}} - returns an object
+	 */
+	searchYoutube: (session, query, cb) => {
+		const params = [
+			"part=snippet",
+			`q=${encodeURIComponent(query)}`,
+			`key=${config.get("apis.youtube.key")}`,
+			"type=video",
+			"maxResults=15"
+		].join("&");
 
-        async.waterfall(
-            [
-                (next) => {
-                    request(
-                        `https://www.googleapis.com/youtube/v3/search?${params}`,
-                        next
-                    );
-                },
+		return async.waterfall(
+			[
+				next => {
+					request(`https://www.googleapis.com/youtube/v3/search?${params}`, next);
+				},
 
-                (res, body, next) => {
-                    next(null, JSON.parse(body));
-                },
-            ],
-            async (err, data) => {
-                console.log(data.error);
-                if (err || data.error) {
-                    if (!err) err = data.error.message;
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "APIS_SEARCH_YOUTUBE",
-                        `Searching youtube failed with query "${query}". "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "APIS_SEARCH_YOUTUBE",
-                    `Searching YouTube successful with query "${query}".`
-                );
-                return cb({ status: "success", data });
-            }
-        );
-    },
+				(res, body, next) => {
+					next(null, JSON.parse(body));
+				}
+			],
+			async (err, data) => {
+				console.log(data.error);
+				if (err || data.error) {
+					if (!err) err = data.error.message;
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"APIS_SEARCH_YOUTUBE",
+						`Searching youtube failed with query "${query}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "APIS_SEARCH_YOUTUBE", `Searching YouTube successful with query "${query}".`);
+				return cb({ status: "success", data });
+			}
+		);
+	},
 
-    /**
-     * Gets Spotify data
-     *
-     * @param session
-     * @param title - the title of the song
-     * @param artist - an artist for that song
-     * @param cb
-     */
-    getSpotifySongs: hooks.adminRequired((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: songs });
-            }
-        );
-    }),
+	/**
+	 * 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
-     *
-     * @param session
-     * @param query - the query
-     * @param cb
-     */
-    searchDiscogs: hooks.adminRequired((session, query, page, cb) => {
-        async.waterfall(
-            [
-                (next) => {
-                    const params = [
-                        `q=${encodeURIComponent(query)}`,
-                        `per_page=20`,
-                        `page=${page}`,
-                    ].join("&");
+	/**
+	 * Gets Discogs data
+	 *
+	 * @param session
+	 * @param query - the query
+	 * @param {Function} cb
+	 */
+	searchDiscogs: isAdminRequired((session, query, page, cb) => {
+		async.waterfall(
+			[
+				next => {
+					const params = [`q=${encodeURIComponent(query)}`, `per_page=20`, `page=${page}`].join("&");
 
-                    const options = {
-                        url: `https://api.discogs.com/database/search?${params}`,
-                        headers: {
-                            "User-Agent": "Request",
-                            Authorization: `Discogs key=${config.get(
-                                "apis.discogs.client"
-                            )}, secret=${config.get("apis.discogs.secret")}`,
-                        },
-                    };
+					const options = {
+						url: `https://api.discogs.com/database/search?${params}`,
+						headers: {
+							"User-Agent": "Request",
+							Authorization: `Discogs key=${config.get("apis.discogs.client")}, secret=${config.get(
+								"apis.discogs.secret"
+							)}`
+						}
+					};
 
-                    request(options, (err, res, body) => {
-                        if (err) next(err);
-                        body = JSON.parse(body);
-                        next(null, body);
-                        if (body.error) next(body.error);
-                    });
-                },
-            ],
-            async (err, body) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "APIS_SEARCH_DISCOGS",
-                        `Searching discogs failed with query "${query}". "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "APIS_SEARCH_DISCOGS",
-                    `User "${session.userId}" searched Discogs succesfully for query "${query}".`
-                );
-                cb({
-                    status: "success",
-                    results: body.results,
-                    pages: body.pagination.pages,
-                });
-            }
-        );
-    }),
+					request(options, (err, res, body) => {
+						if (err) next(err);
+						body = JSON.parse(body);
+						next(null, body);
+						if (body.error) next(body.error);
+					});
+				}
+			],
+			async (err, body) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"APIS_SEARCH_DISCOGS",
+						`Searching discogs failed with query "${query}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"APIS_SEARCH_DISCOGS",
+					`User "${session.userId}" searched Discogs succesfully for query "${query}".`
+				);
+				return cb({
+					status: "success",
+					results: body.results,
+					pages: body.pagination.pages
+				});
+			}
+		);
+	}),
 
-    /**
-     * Joins a room
-     *
-     * @param session
-     * @param page - the room to join
-     * @param cb
-     */
-    joinRoom: (session, page, cb) => {
-        if (page === "home") {
-            utils.runJob("SOCKET_JOIN_ROOM", {
-                socketId: session.socketId,
-                room: page,
-            });
-        }
-        cb({});
-    },
+	/**
+	 * Joins a room
+	 *
+	 * @param {object} session - user session
+	 * @param {string} page - the room to join
+	 * @param {Function} cb - callback
+	 */
+	joinRoom: (session, page, cb) => {
+		if (page === "home") {
+			utils.runJob("SOCKET_JOIN_ROOM", {
+				socketId: session.socketId,
+				room: page
+			});
+		}
+		cb({});
+	},
 
-    /**
-     * Joins an admin room
-     *
-     * @param session
-     * @param page - the admin room to join
-     * @param cb
-     */
-    joinAdminRoom: hooks.adminRequired((session, page, cb) => {
-        if (
-            page === "queue" ||
-            page === "songs" ||
-            page === "stations" ||
-            page === "reports" ||
-            page === "news" ||
-            page === "users" ||
-            page === "statistics" ||
-            page === "punishments"
-        ) {
-            utils.runJob("SOCKET_JOIN_ROOM", {
-                socketId: session.socketId,
-                room: `admin.${page}`,
-            });
-        }
-        cb({});
-    }),
+	/**
+	 * Joins an admin room
+	 *
+	 * @param {object} session - user session
+	 * @param {string} page - the admin room to join
+	 * @param {Function} cb - callback
+	 */
+	joinAdminRoom: isAdminRequired((session, page, cb) => {
+		if (
+			page === "queue" ||
+			page === "songs" ||
+			page === "stations" ||
+			page === "reports" ||
+			page === "news" ||
+			page === "users" ||
+			page === "statistics" ||
+			page === "punishments"
+		) {
+			utils.runJob("SOCKET_JOIN_ROOM", {
+				socketId: session.socketId,
+				room: `admin.${page}`
+			});
+		}
+		cb({});
+	}),
 
-    /**
-     * Returns current date
-     *
-     * @param session
-     * @param cb
-     */
-    ping: (session, cb) => {
-        cb({ date: Date.now() });
-    },
+	/**
+	 * Returns current date
+	 *
+	 * @param {object} session - user session
+	 * @param {Function} cb - callback
+	 */
+	ping: (session, cb) => {
+		cb({ date: Date.now() });
+	}
 };

+ 42 - 56
backend/logic/actions/hooks/adminRequired.js

@@ -1,59 +1,45 @@
-const async = require("async");
+import async from "async";
 
-const db = require("../../db");
-const cache = require("../../cache");
-const utils = require("../../utils");
+import db from "../../db";
+import cache from "../../cache";
+import utils from "../../utils";
 
-module.exports = function(next) {
-    return async function(session) {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        let args = [];
-        for (let prop in arguments) args.push(arguments[prop]);
-        let cb = args[args.length - 1];
-        async.waterfall(
-            [
-                (next) => {
-                    cache
-                        .runJob("HGET", {
-                            table: "sessions",
-                            key: session.sessionId,
-                        })
-                        .then((session) => {
-                            next(null, session);
-                        })
-                        .catch(next);
-                },
-                (session, next) => {
-                    if (!session || !session.userId)
-                        return next("Login required.");
-                    this.session = session;
-                    userModel.findOne({ _id: session.userId }, next);
-                },
-                (user, next) => {
-                    if (!user) return next("Login required.");
-                    if (user.role !== "admin")
-                        return next("Insufficient permissions.");
-                    next();
-                },
-            ],
-            async (err) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "INFO",
-                        "ADMIN_REQUIRED",
-                        `User failed to pass admin required check. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "INFO",
-                    "ADMIN_REQUIRED",
-                    `User "${session.userId}" passed admin required check.`,
-                    false
-                );
-                next.apply(null, args);
-            }
-        );
-    };
+export default destination => async (session, ...args) => {
+	const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+
+	const cb = args[args.length - 1];
+
+	async.waterfall(
+		[
+			next => {
+				cache
+					.runJob("HGET", {
+						table: "sessions",
+						key: session.sessionId
+					})
+					.then(session => {
+						next(null, session);
+					})
+					.catch(next);
+			},
+			(session, next) => {
+				if (!session || !session.userId) return next("Login required.");
+				return userModel.findOne({ _id: session.userId }, next);
+			},
+			(user, next) => {
+				if (!user) return next("Login required.");
+				if (user.role !== "admin") return next("Insufficient permissions.");
+				return next();
+			}
+		],
+		async err => {
+			if (err) {
+				err = await utils.runJob("GET_ERROR", { error: err });
+				console.log("INFO", "ADMIN_REQUIRED", `User failed to pass admin required check. "${err}"`);
+				return cb({ status: "failure", message: err });
+			}
+			console.log("INFO", "ADMIN_REQUIRED", `User "${session.userId}" passed admin required check.`, false);
+			return destination(session, ...args);
+		}
+	);
 };

+ 6 - 6
backend/logic/actions/hooks/index.js

@@ -1,7 +1,7 @@
-'use strict';
+import loginRequired from "./loginRequired";
+import adminRequired from "./adminRequired";
+import ownerRequired from "./ownerRequired";
 
-module.exports = {
-	loginRequired: require('./loginRequired'),
-	adminRequired: require('./adminRequired'),
-	ownerRequired: require('./ownerRequired')
-};
+export const isLoginRequired = loginRequired;
+export const isAdminRequired = adminRequired;
+export const isOwnerRequired = ownerRequired;

+ 32 - 46
backend/logic/actions/hooks/loginRequired.js

@@ -1,50 +1,36 @@
-const async = require("async");
+import async from "async";
 
-const cache = require("../../cache");
-const utils = require("../../utils");
+import cache from "../../cache";
+import utils from "../../utils";
 // const logger = moduleManager.modules["logger"];
 
-module.exports = function(next) {
-    return function(session) {
-        let args = [];
-        for (let prop in arguments) args.push(arguments[prop]);
-        let cb = args[args.length - 1];
-        async.waterfall(
-            [
-                (next) => {
-                    cache
-                        .runJob("HGET", {
-                            table: "sessions",
-                            key: session.sessionId,
-                        })
-                        .then((session) => {
-                            next(null, session);
-                        })
-                        .catch(next);
-                },
-                (session, next) => {
-                    if (!session || !session.userId)
-                        return next("Login required.");
-                    this.session = session;
-                    next();
-                },
-            ],
-            async (err) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    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
-                );
-                next.apply(null, args);
-            }
-        );
-    };
+export default destination => (session, ...args) => {
+	const cb = args[args.length - 1];
+
+	async.waterfall(
+		[
+			next => {
+				cache
+					.runJob("HGET", {
+						table: "sessions",
+						key: session.sessionId
+					})
+					.then(session => next(null, session))
+					.catch(next);
+			},
+			(session, next) => {
+				if (!session || !session.userId) return next("Login required.");
+				return next();
+			}
+		],
+		async err => {
+			if (err) {
+				err = await utils.runJob("GET_ERROR", { error: err });
+				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);
+			return destination(session, ...args);
+		}
+	);
 };

+ 62 - 71
backend/logic/actions/hooks/ownerRequired.js

@@ -1,75 +1,66 @@
-const async = require("async");
+import async from "async";
 
-const moduleManager = require("../../../index");
+import db from "../../db";
+import cache from "../../cache";
+import utils from "../../utils";
+import stations from "../../stations";
 
-const db = require("../../db");
-const cache = require("../../cache");
-const utils = require("../../utils");
-const stations = require("../../stations");
+export default destination => async (session, stationId, ...args) => {
+	const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
 
-module.exports = function(next) {
-    return async function(session, stationId) {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        let args = [];
-        for (let prop in arguments) args.push(arguments[prop]);
-        let cb = args[args.length - 1];
-        async.waterfall(
-            [
-                (next) => {
-                    cache
-                        .runJob("HGET", {
-                            table: "sessions",
-                            key: session.sessionId,
-                        })
-                        .then((session) => {
-                            next(null, session)
-                        })
-                        .catch(next);
-                },
-                (session, next) => {
-                    if (!session || !session.userId)
-                        return next("Login required.");
-                    this.session = session;
-                    userModel.findOne({ _id: session.userId }, next);
-                },
-                (user, next) => {
-                    if (!user) return next("Login required.");
-                    if (user.role === "admin") return next(true);
-                    stations
-                        .runJob("GET_STATION", { stationId })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-                (station, next) => {
-                    if (!station) return next("Station not found.");
-                    if (
-                        station.type === "community" &&
-                        station.owner === session.userId
-                    )
-                        return next(true);
-                    next("Invalid permissions.");
-                },
-            ],
-            async (err) => {
-                if (err !== true) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "INFO",
-                        "OWNER_REQUIRED",
-                        `User failed to pass owner required check for station "${stationId}". "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "INFO",
-                    "OWNER_REQUIRED",
-                    `User "${session.userId}" passed owner required check for station "${stationId}"`,
-                    false
-                );
-                next.apply(null, args);
-            }
-        );
-    };
+	const cb = args[args.length - 1];
+
+	async.waterfall(
+		[
+			next => {
+				cache
+					.runJob("HGET", {
+						table: "sessions",
+						key: session.sessionId
+					})
+					.then(session => {
+						next(null, session);
+					})
+					.catch(next);
+			},
+			(session, next) => {
+				if (!session || !session.userId) return next("Login required.");
+				return userModel.findOne({ _id: session.userId }, next);
+			},
+			(user, next) => {
+				if (!user) return next("Login required.");
+				if (user.role === "admin") return next(true);
+				return stations
+					.runJob("GET_STATION", { stationId })
+					.then(station => {
+						next(null, station);
+					})
+					.catch(next);
+			},
+			(station, next) => {
+				if (!station) return next("Station not found.");
+				if (station.type === "community" && station.owner === session.userId) return next(true);
+				return next("Invalid permissions.");
+			}
+		],
+		async err => {
+			if (err !== true) {
+				err = await utils.runJob("GET_ERROR", { error: err });
+				console.log(
+					"INFO",
+					"OWNER_REQUIRED",
+					`User failed to pass owner required check for station "${stationId}". "${err}"`
+				);
+				return cb({ status: "failure", message: err });
+			}
+			console.log(
+				"INFO",
+				"OWNER_REQUIRED",
+				`User "${session.userId}" passed owner required check for station "${stationId}"`,
+				false
+			);
+
+			return destination(session, stationId, ...args);
+		}
+	);
 };

+ 23 - 13
backend/logic/actions/index.js

@@ -1,15 +1,25 @@
-"use strict";
+import apis from "./apis";
+import songs from "./songs";
+import queueSongs from "./queueSongs";
+import stations from "./stations";
+import playlists from "./playlists";
+import users from "./users";
+import activities from "./activities";
+import reports from "./reports";
+import news from "./news";
+import punishments from "./punishments";
+import utils from "./utils";
 
-module.exports = {
-    apis: require("./apis"),
-    songs: require("./songs"),
-    queueSongs: require("./queueSongs"),
-    stations: require("./stations"),
-    playlists: require("./playlists"),
-    users: require("./users"),
-    activities: require("./activities"),
-    reports: require("./reports"),
-    news: require("./news"),
-    punishments: require("./punishments"),
-    utils: require("./utils"),
+export default {
+	apis,
+	songs,
+	queueSongs,
+	stations,
+	playlists,
+	users,
+	activities,
+	reports,
+	news,
+	punishments,
+	utils
 };

+ 171 - 211
backend/logic/actions/news.js

@@ -1,230 +1,190 @@
-"use strict";
+import async from "async";
 
-const async = require("async");
+import { isAdminRequired, isLoginRequired, isOwnerRequired } from "./hooks";
 
-const hooks = require("./hooks");
-const moduleManager = require("../../index");
+import db from "../db";
+import utils from "../utils";
 
-const db = require("../db");
-const cache = require("../cache");
-const utils = require("../utils");
+import cache from "../cache";
 // const logger = require("logger");
 
 cache.runJob("SUB", {
-    channel: "news.create",
-    cb: (news) => {
-        utils.runJob("EMIT_TO_ROOM", {
-            room: "admin.news",
-            args: ["event:admin.news.created", news],
-        });
-    },
+	channel: "news.create",
+	cb: news => {
+		utils.runJob("EMIT_TO_ROOM", {
+			room: "admin.news",
+			args: ["event:admin.news.created", news]
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "news.remove",
-    cb: (news) => {
-        utils.runJob("EMIT_TO_ROOM", {
-            room: "admin.news",
-            args: ["event:admin.news.removed", news],
-        });
-    },
+	channel: "news.remove",
+	cb: news => {
+		utils.runJob("EMIT_TO_ROOM", {
+			room: "admin.news",
+			args: ["event:admin.news.removed", news]
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "news.update",
-    cb: (news) => {
-        utils.runJob("EMIT_TO_ROOM", {
-            room: "admin.news",
-            args: ["event:admin.news.updated", news],
-        });
-    },
+	channel: "news.update",
+	cb: news => {
+		utils.runJob("EMIT_TO_ROOM", {
+			room: "admin.news",
+			args: ["event:admin.news.updated", news]
+		});
+	}
 });
 
-module.exports = {
-    /**
-     * Gets all news items
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {Function} cb - gets called with the result
-     */
-    index: async (session, cb) => {
-        const newsModel = await db.runJob("GET_MODEL", { modelName: "news" });
-        async.waterfall(
-            [
-                (next) => {
-                    newsModel
-                        .find({})
-                        .sort({ createdAt: "desc" })
-                        .exec(next);
-                },
-            ],
-            async (err, news) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "NEWS_INDEX",
-                        `Indexing news failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "NEWS_INDEX",
-                    `Indexing news successful.`,
-                    false
-                );
-                return cb({ status: "success", data: news });
-            }
-        );
-    },
+export default {
+	/**
+	 * Gets all news items
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {Function} cb - gets called with the result
+	 */
+	index: async (session, cb) => {
+		const newsModel = await db.runJob("GET_MODEL", { modelName: "news" });
+		async.waterfall(
+			[
+				next => {
+					newsModel.find({}).sort({ createdAt: "desc" }).exec(next);
+				}
+			],
+			async (err, news) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "NEWS_INDEX", `Indexing news failed. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "NEWS_INDEX", `Indexing news successful.`, false);
+				return cb({ status: "success", data: news });
+			}
+		);
+	},
 
-    /**
-     * Creates a news item
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {Object} data - the object of the news data
-     * @param {Function} cb - gets called with the result
-     */
-    create: hooks.adminRequired(async (session, data, cb) => {
-        const newsModel = await db.runJob("GET_MODEL", { modelName: "news" });
-        async.waterfall(
-            [
-                (next) => {
-                    data.createdBy = session.userId;
-                    data.createdAt = Date.now();
-                    newsModel.create(data, next);
-                },
-            ],
-            async (err, news) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "NEWS_CREATE",
-                        `Creating news failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                cache.runJob("PUB", { channel: "news.create", value: news });
-                console.log(
-                    "SUCCESS",
-                    "NEWS_CREATE",
-                    `Creating news successful.`
-                );
-                return cb({
-                    status: "success",
-                    message: "Successfully created News",
-                });
-            }
-        );
-    }),
+	/**
+	 * Creates a news item
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} data - the object of the news data
+	 * @param {Function} cb - gets called with the result
+	 */
+	create: isAdminRequired(async (session, data, cb) => {
+		const newsModel = await db.runJob("GET_MODEL", { modelName: "news" });
+		async.waterfall(
+			[
+				next => {
+					data.createdBy = session.userId;
+					data.createdAt = Date.now();
+					newsModel.create(data, next);
+				}
+			],
+			async (err, news) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "NEWS_CREATE", `Creating news failed. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				cache.runJob("PUB", { channel: "news.create", value: news });
+				console.log("SUCCESS", "NEWS_CREATE", `Creating news successful.`);
+				return cb({
+					status: "success",
+					message: "Successfully created News"
+				});
+			}
+		);
+	}),
 
-    /**
-     * Gets the latest news item
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {Function} cb - gets called with the result
-     */
-    newest: async (session, cb) => {
-        const newsModel = await db.runJob("GET_MODEL", { modelName: "news" });
-        async.waterfall(
-            [
-                (next) => {
-                    newsModel
-                        .findOne({})
-                        .sort({ createdAt: "desc" })
-                        .exec(next);
-                },
-            ],
-            async (err, news) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "NEWS_NEWEST",
-                        `Getting the latest news failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "NEWS_NEWEST",
-                    `Successfully got the latest news.`,
-                    false
-                );
-                return cb({ status: "success", data: news });
-            }
-        );
-    },
+	/**
+	 * Gets the latest news item
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {Function} cb - gets called with the result
+	 */
+	newest: async (session, cb) => {
+		const newsModel = await db.runJob("GET_MODEL", { modelName: "news" });
+		async.waterfall(
+			[
+				next => {
+					newsModel.findOne({}).sort({ createdAt: "desc" }).exec(next);
+				}
+			],
+			async (err, news) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "NEWS_NEWEST", `Getting the latest news failed. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "NEWS_NEWEST", `Successfully got the latest news.`, false);
+				return cb({ status: "success", data: news });
+			}
+		);
+	},
 
-    /**
-     * Removes a news item
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {Object} news - the news object
-     * @param {Function} cb - gets called with the result
-     */
-    //TODO Pass in an id, not an object
-    //TODO Fix this
-    remove: hooks.adminRequired(async (session, news, cb) => {
-        const newsModel = await db.runJob("GET_MODEL", { modelName: "news" });
-        newsModel.deleteOne({ _id: news._id }, async (err) => {
-            if (err) {
-                err = await utils.runJob("GET_ERROR", { error: err });
-                console.log(
-                    "ERROR",
-                    "NEWS_REMOVE",
-                    `Removing news "${news._id}" failed for user "${session.userId}". "${err}"`
-                );
-                return cb({ status: "failure", message: err });
-            } else {
-                cache.runJob("PUB", { channel: "news.remove", value: news });
-                console.log(
-                    "SUCCESS",
-                    "NEWS_REMOVE",
-                    `Removing news "${news._id}" successful by user "${session.userId}".`
-                );
-                return cb({
-                    status: "success",
-                    message: "Successfully removed News",
-                });
-            }
-        });
-    }),
+	/**
+	 * Removes a news item
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} news - the news object
+	 * @param {Function} cb - gets called with the result
+	 */
+	// TODO Pass in an id, not an object
+	// TODO Fix this
+	remove: isAdminRequired(async (session, news, cb) => {
+		const newsModel = await db.runJob("GET_MODEL", { modelName: "news" });
+		newsModel.deleteOne({ _id: news._id }, async err => {
+			if (err) {
+				err = await utils.runJob("GET_ERROR", { error: err });
+				console.log(
+					"ERROR",
+					"NEWS_REMOVE",
+					`Removing news "${news._id}" failed for user "${session.userId}". "${err}"`
+				);
+				return cb({ status: "failure", message: err });
+			}
+			cache.runJob("PUB", { channel: "news.remove", value: news });
+			console.log(
+				"SUCCESS",
+				"NEWS_REMOVE",
+				`Removing news "${news._id}" successful by user "${session.userId}".`
+			);
+			return cb({
+				status: "success",
+				message: "Successfully removed News"
+			});
+		});
+	}),
 
-    /**
-     * Removes a news item
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} _id - the news id
-     * @param {Object} news - the news object
-     * @param {Function} cb - gets called with the result
-     */
-    //TODO Fix this
-    update: hooks.adminRequired(async (session, _id, news, cb) => {
-        const newsModel = await db.runJob("GET_MODEL", { modelName: "news" });
-        newsModel.updateOne({ _id }, news, { upsert: true }, async (err) => {
-            if (err) {
-                err = await utils.runJob("GET_ERROR", { error: err });
-                console.log(
-                    "ERROR",
-                    "NEWS_UPDATE",
-                    `Updating news "${_id}" failed for user "${session.userId}". "${err}"`
-                );
-                return cb({ status: "failure", message: err });
-            } else {
-                cache.runJob("PUB", { channel: "news.update", value: news });
-                console.log(
-                    "SUCCESS",
-                    "NEWS_UPDATE",
-                    `Updating news "${_id}" successful for user "${session.userId}".`
-                );
-                return cb({
-                    status: "success",
-                    message: "Successfully updated News",
-                });
-            }
-        });
-    }),
+	/**
+	 * Removes a news item
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} _id - the news id
+	 * @param {object} news - the news object
+	 * @param {Function} cb - gets called with the result
+	 */
+	// TODO Fix this
+	update: isAdminRequired(async (session, _id, news, cb) => {
+		const newsModel = await db.runJob("GET_MODEL", { modelName: "news" });
+		newsModel.updateOne({ _id }, news, { upsert: true }, async err => {
+			if (err) {
+				err = await utils.runJob("GET_ERROR", { error: err });
+				console.log(
+					"ERROR",
+					"NEWS_UPDATE",
+					`Updating news "${_id}" failed for user "${session.userId}". "${err}"`
+				);
+				return cb({ status: "failure", message: err });
+			}
+			cache.runJob("PUB", { channel: "news.update", value: news });
+			console.log("SUCCESS", "NEWS_UPDATE", `Updating news "${_id}" successful for user "${session.userId}".`);
+			return cb({
+				status: "success",
+				message: "Successfully updated News"
+			});
+		});
+	})
 };

+ 1106 - 1177
backend/logic/actions/playlists.js

@@ -1,1204 +1,1133 @@
-"use strict";
+import async from "async";
 
-const async = require("async");
+import { isAdminRequired, isLoginRequired, isOwnerRequired } from "./hooks";
 
-const hooks = require("./hooks");
-const moduleManager = require("../../index");
+import db from "../db";
+import utils from "../utils";
+import songs from "../songs";
 
-const db = require("../db");
-const cache = require("../cache");
-const utils = require("../utils");
-const playlists = require("../playlists");
-const songs = require("../songs");
-const activities = require("../activities");
+import cache from "../cache";
+
+import moduleManager from "../../index";
+
+import playlists from "../playlists";
+import activities from "../activities";
 
 cache.runJob("SUB", {
-    channel: "playlist.create",
-    cb: (playlistId) => {
-        playlists.runJob("GET_PLAYLIST", { playlistId }).then((playlist) => {
-            utils
-                .runJob("SOCKETS_FROM_USER", { userId: playlist.createdBy })
-                .then((response) => {
-                    response.sockets.forEach((socket) => {
-                        socket.emit("event:playlist.create", playlist);
-                    });
-                });
-        });
-    },
+	channel: "playlist.create",
+	cb: playlistId => {
+		playlists.runJob("GET_PLAYLIST", { playlistId }).then(playlist => {
+			utils.runJob("SOCKETS_FROM_USER", { userId: playlist.createdBy }).then(response => {
+				response.sockets.forEach(socket => {
+					socket.emit("event:playlist.create", playlist);
+				});
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "playlist.delete",
-    cb: (res) => {
-        utils
-            .runJob("SOCKETS_FROM_USER", { userId: res.userId })
-            .then((response) => {
-                response.sockets.forEach((socket) => {
-                    socket.emit("event:playlist.delete", res.playlistId);
-                });
-            });
-    },
+	channel: "playlist.delete",
+	cb: res => {
+		utils.runJob("SOCKETS_FROM_USER", { userId: res.userId }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("event:playlist.delete", res.playlistId);
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "playlist.moveSongToTop",
-    cb: (res) => {
-        utils
-            .runJob("SOCKETS_FROM_USER", { userId: res.userId })
-            .then((response) => {
-                response.sockets.forEach((socket) => {
-                    socket.emit("event:playlist.moveSongToTop", {
-                        playlistId: res.playlistId,
-                        songId: res.songId,
-                    });
-                });
-            });
-    },
+	channel: "playlist.moveSongToTop",
+	cb: res => {
+		utils.runJob("SOCKETS_FROM_USER", { userId: res.userId }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("event:playlist.moveSongToTop", {
+					playlistId: res.playlistId,
+					songId: res.songId
+				});
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "playlist.moveSongToBottom",
-    cb: (res) => {
-        utils
-            .runJob("SOCKETS_FROM_USER", { userId: res.userId })
-            .then((response) => {
-                response.sockets.forEach((socket) => {
-                    socket.emit("event:playlist.moveSongToBottom", {
-                        playlistId: res.playlistId,
-                        songId: res.songId,
-                    });
-                });
-            });
-    },
+	channel: "playlist.moveSongToBottom",
+	cb: res => {
+		utils.runJob("SOCKETS_FROM_USER", { userId: res.userId }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("event:playlist.moveSongToBottom", {
+					playlistId: res.playlistId,
+					songId: res.songId
+				});
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "playlist.addSong",
-    cb: (res) => {
-        utils
-            .runJob("SOCKETS_FROM_USER", { userId: res.userId })
-            .then((response) => {
-                response.sockets.forEach((socket) => {
-                    socket.emit("event:playlist.addSong", {
-                        playlistId: res.playlistId,
-                        song: res.song,
-                    });
-                });
-            });
-    },
+	channel: "playlist.addSong",
+	cb: res => {
+		utils.runJob("SOCKETS_FROM_USER", { userId: res.userId }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("event:playlist.addSong", {
+					playlistId: res.playlistId,
+					song: res.song
+				});
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "playlist.removeSong",
-    cb: (res) => {
-        utils
-            .runJob("SOCKETS_FROM_USER", { userId: res.userId })
-            .then((response) => {
-                response.sockets.forEach((socket) => {
-                    socket.emit("event:playlist.removeSong", {
-                        playlistId: res.playlistId,
-                        songId: res.songId,
-                    });
-                });
-            });
-    },
+	channel: "playlist.removeSong",
+	cb: res => {
+		utils.runJob("SOCKETS_FROM_USER", { userId: res.userId }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("event:playlist.removeSong", {
+					playlistId: res.playlistId,
+					songId: res.songId
+				});
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "playlist.updateDisplayName",
-    cb: (res) => {
-        utils
-            .runJob("SOCKETS_FROM_USER", { userId: res.userId })
-            .then((response) => {
-                response.sockets.forEach((socket) => {
-                    socket.emit("event:playlist.updateDisplayName", {
-                        playlistId: res.playlistId,
-                        displayName: res.displayName,
-                    });
-                });
-            });
-    },
+	channel: "playlist.updateDisplayName",
+	cb: res => {
+		utils.runJob("SOCKETS_FROM_USER", { userId: res.userId }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("event:playlist.updateDisplayName", {
+					playlistId: res.playlistId,
+					displayName: res.displayName
+				});
+			});
+		});
+	}
 });
 
-let lib = {
-    /**
-     * Gets the first song from a private playlist
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} playlistId - the id of the playlist we are getting the first song from
-     * @param {Function} cb - gets called with the result
-     */
-    getFirstSong: hooks.loginRequired((session, playlistId, cb) => {
-        async.waterfall(
-            [
-                (next) => {
-                    playlists
-                        .runJob("GET_PLAYLIST", { playlistId })
-                        .then((playlist) => {
-                            next(null, playlist);
-                        })
-                        .catch(next);
-                },
-
-                (playlist, next) => {
-                    if (!playlist || playlist.createdBy !== session.userId)
-                        return next("Playlist not found.");
-                    next(null, playlist.songs[0]);
-                },
-            ],
-            async (err, song) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "PLAYLIST_GET_FIRST_SONG",
-                        `Getting the first song of playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "PLAYLIST_GET_FIRST_SONG",
-                    `Successfully got the first song of playlist "${playlistId}" for user "${session.userId}".`
-                );
-                cb({
-                    status: "success",
-                    song: song,
-                });
-            }
-        );
-    }),
-
-    /**
-     * Gets all playlists for the user requesting it
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {Function} cb - gets called with the result
-     */
-    indexForUser: hooks.loginRequired(async (session, cb) => {
-        const playlistModel = await db.runJob("GET_MODEL", {
-            modelName: "playlist",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    playlistModel.find({ createdBy: session.userId }, next);
-                },
-            ],
-            async (err, playlists) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "PLAYLIST_INDEX_FOR_USER",
-                        `Indexing playlists for user "${session.userId}" failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "PLAYLIST_INDEX_FOR_USER",
-                    `Successfully indexed playlists for user "${session.userId}".`
-                );
-                cb({
-                    status: "success",
-                    data: playlists,
-                });
-            }
-        );
-    }),
-
-    /**
-     * Creates a new private playlist
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {Object} data - the data for the new private playlist
-     * @param {Function} cb - gets called with the result
-     */
-    create: hooks.loginRequired(async (session, data, cb) => {
-        const playlistModel = await db.runJob("GET_MODEL", {
-            modelName: "playlist",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    return data
-                        ? next()
-                        : cb({ status: "failure", message: "Invalid data" });
-                },
-
-                (next) => {
-                    const { displayName, songs } = data;
-                    playlistModel.create(
-                        {
-                            displayName,
-                            songs,
-                            createdBy: session.userId,
-                            createdAt: Date.now(),
-                        },
-                        next
-                    );
-                },
-            ],
-            async (err, playlist) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "PLAYLIST_CREATE",
-                        `Creating private playlist failed for user "${session.userId}". "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                cache.runJob("PUB", {
-                    channel: "playlist.create",
-                    value: playlist._id,
-                });
-                activities.runJob("ADD_ACTIVITY", {
-                    userId: session.userId,
-                    activityType: "created_playlist",
-                    payload: [playlist._id],
-                });
-                console.log(
-                    "SUCCESS",
-                    "PLAYLIST_CREATE",
-                    `Successfully created private playlist for user "${session.userId}".`
-                );
-                cb({
-                    status: "success",
-                    message: "Successfully created playlist",
-                    data: {
-                        _id: playlist._id,
-                    },
-                });
-            }
-        );
-    }),
-
-    /**
-     * Gets a playlist from id
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} playlistId - the id of the playlist we are getting
-     * @param {Function} cb - gets called with the result
-     */
-    getPlaylist: hooks.loginRequired((session, playlistId, cb) => {
-        async.waterfall(
-            [
-                (next) => {
-                    playlists
-                        .runJob("GET_PLAYLIST", { playlistId })
-                        .then((playlist) => {
-                            next(null, playlist);
-                        })
-                        .catch(next);
-                },
-
-                (playlist, next) => {
-                    if (!playlist || playlist.createdBy !== session.userId)
-                        return next("Playlist not found");
-                    next(null, playlist);
-                },
-            ],
-            async (err, playlist) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "PLAYLIST_GET",
-                        `Getting private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "PLAYLIST_GET",
-                    `Successfully got private playlist "${playlistId}" for user "${session.userId}".`
-                );
-                cb({
-                    status: "success",
-                    data: playlist,
-                });
-            }
-        );
-    }),
-
-    /**
-     * Obtains basic metadata of a playlist in order to format an activity
-     *
-     * @param session
-     * @param playlistId - the playlist id
-     * @param cb
-     */
-    getPlaylistForActivity: (session, playlistId, cb) => {
-        async.waterfall(
-            [
-                (next) => {
-                    playlists
-                        .runJob("GET_PLAYLIST", { playlistId })
-                        .then((playlist) => {
-                            next(null, playlist);
-                        })
-                        .catch(next);
-                },
-            ],
-            async (err, playlist) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "PLAYLISTS_GET_PLAYLIST_FOR_ACTIVITY",
-                        `Failed to obtain metadata of playlist ${playlistId} for activity formatting. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "PLAYLISTS_GET_PLAYLIST_FOR_ACTIVITY",
-                        `Obtained metadata of playlist ${playlistId} for activity formatting successfully.`
-                    );
-                    cb({
-                        status: "success",
-                        data: {
-                            title: playlist.displayName,
-                        },
-                    });
-                }
-            }
-        );
-    },
-
-    //TODO Remove this
-    /**
-     * Updates a private playlist
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} playlistId - the id of the playlist we are updating
-     * @param {Object} playlist - the new private playlist object
-     * @param {Function} cb - gets called with the result
-     */
-    update: hooks.loginRequired(async (session, playlistId, playlist, cb) => {
-        const playlistModel = await db.runJob("GET_MODEL", {
-            modelName: "playlist",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    playlistModel.updateOne(
-                        { _id: playlistId, createdBy: session.userId },
-                        playlist,
-                        { runValidators: true },
-                        next
-                    );
-                },
-
-                (res, next) => {
-                    playlists
-                        .runJob("UPDATE_PLAYLIST", { playlistId })
-                        .then((playlist) => {
-                            next(null, playlist);
-                        })
-                        .catch(next);
-                },
-            ],
-            async (err, playlist) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "PLAYLIST_UPDATE",
-                        `Updating private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "PLAYLIST_UPDATE",
-                    `Successfully updated private playlist "${playlistId}" for user "${session.userId}".`
-                );
-                cb({
-                    status: "success",
-                    data: playlist,
-                });
-            }
-        );
-    }),
-
-    /**
-     * Updates a private playlist
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} playlistId - the id of the playlist we are updating
-     * @param {Function} cb - gets called with the result
-     */
-    shuffle: hooks.loginRequired(async (session, playlistId, cb) => {
-        const playlistModel = await db.runJob("GET_MODEL", {
-            modelName: "playlist",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    if (!playlistId) return next("No playlist id.");
-                    playlistModel.findById(playlistId, next);
-                },
-
-                (playlist, next) => {
-                    utils
-                        .runJob("SHUFFLE", { array: playlist.songs })
-                        .then((result) => {
-                            next(null, result.array);
-                        })
-                        .catch(next);
-                },
-
-                (songs, next) => {
-                    playlistModel.updateOne(
-                        { _id: playlistId },
-                        { $set: { songs } },
-                        { runValidators: true },
-                        next
-                    );
-                },
-
-                (res, next) => {
-                    playlists
-                        .runJob("UPDATE_PLAYLIST", { playlistId })
-                        .then((playlist) => {
-                            next(null, playlist);
-                        })
-                        .catch(next);
-                },
-            ],
-            async (err, playlist) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "PLAYLIST_SHUFFLE",
-                        `Updating private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "PLAYLIST_SHUFFLE",
-                    `Successfully updated private playlist "${playlistId}" for user "${session.userId}".`
-                );
-                cb({
-                    status: "success",
-                    message: "Successfully shuffled playlist.",
-                    data: playlist,
-                });
-            }
-        );
-    }),
-
-    /**
-     * Adds a song to a private playlist
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {Boolean} isSet - is the song part of a set of songs to be added
-     * @param {String} songId - the id of the song we are trying to add
-     * @param {String} playlistId - the id of the playlist we are adding the song to
-     * @param {Function} cb - gets called with the result
-     */
-    addSongToPlaylist: hooks.loginRequired(
-        async (session, isSet, songId, playlistId, cb) => {
-            const playlistModel = await db.runJob("GET_MODEL", {
-                modelName: "playlist",
-            });
-
-            async.waterfall(
-                [
-                    (next) => {
-                        playlists
-                            .runJob("GET_PLAYLIST", { playlistId })
-                            .then((playlist) => {
-                                if (
-                                    !playlist ||
-                                    playlist.createdBy !== session.userId
-                                )
-                                    return next(
-                                        "Something went wrong when trying to get the playlist"
-                                    );
-
-                                async.each(
-                                    playlist.songs,
-                                    (song, next) => {
-                                        if (song.songId === songId)
-                                            return next(
-                                                "That song is already in the playlist"
-                                            );
-                                        next();
-                                    },
-                                    next
-                                );
-                            })
-                            .catch(next);
-                    },
-                    (next) => {
-                        songs
-                            .runJob("GET_SONG", { id: songId })
-                            .then((response) => {
-                                const song = response.song;
-                                next(null, {
-                                    _id: song._id,
-                                    songId: songId,
-                                    title: song.title,
-                                    duration: song.duration,
-                                });
-                            })
-                            .catch(() => {
-                                utils
-                                    .runJob("GET_SONG_FROM_YOUTUBE", { songId })
-                                    .then((response) =>
-                                        next(null, response.song)
-                                    )
-                                    .catch(next);
-                            });
-                    },
-                    (newSong, next) => {
-                        playlistModel.updateOne(
-                            { _id: playlistId },
-                            { $push: { songs: newSong } },
-                            { runValidators: true },
-                            (err) => {
-                                if (err) return next(err);
-                                playlists
-                                    .runJob("UPDATE_PLAYLIST", { playlistId })
-                                    .then((playlist) =>
-                                        next(null, playlist, newSong)
-                                    )
-                                    .catch(next);
-                            }
-                        );
-                    },
-                ],
-                async (err, playlist, newSong) => {
-                    if (err) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "PLAYLIST_ADD_SONG",
-                            `Adding song "${songId}" to private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
-                        );
-                        return cb({ status: "failure", message: err });
-                    } else {
-                        console.log(
-                            "SUCCESS",
-                            "PLAYLIST_ADD_SONG",
-                            `Successfully added song "${songId}" to private playlist "${playlistId}" for user "${session.userId}".`
-                        );
-                        if (!isSet)
-                            activities.runJob("ADD_ACTIVITY", {
-                                userId: session.userId,
-                                activityType: "added_song_to_playlist",
-                                payload: [{ songId, playlistId }],
-                            });
-
-                        cache.runJob("PUB", {
-                            channel: "playlist.addSong",
-                            value: {
-                                playlistId: playlist._id,
-                                song: newSong,
-                                userId: session.userId,
-                            },
-                        });
-                        return cb({
-                            status: "success",
-                            message:
-                                "Song has been successfully added to the playlist",
-                            data: playlist.songs,
-                        });
-                    }
-                }
-            );
-        }
-    ),
-
-    /**
-     * Adds a set of songs to a private playlist
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} url - the url of the the YouTube playlist
-     * @param {String} playlistId - the id of the playlist we are adding the set of songs to
-     * @param {Boolean} musicOnly - whether to only add music to the playlist
-     * @param {Function} cb - gets called with the result
-     */
-    addSetToPlaylist: hooks.loginRequired(
-        (session, url, playlistId, musicOnly, cb) => {
-            let videosInPlaylistTotal = 0;
-            let songsInPlaylistTotal = 0;
-            let songsSuccess = 0;
-            let songsFail = 0;
-
-            let addedSongs = [];
-
-            async.waterfall(
-                [
-                    (next) => {
-                        utils
-                            .runJob("GET_PLAYLIST_FROM_YOUTUBE", {
-                                url,
-                                musicOnly,
-                            })
-                            .then((response) => {
-                                if (response.filteredSongs) {
-                                    videosInPlaylistTotal =
-                                        response.songs.length;
-                                    songsInPlaylistTotal =
-                                        response.filteredSongs.length;
-                                } else {
-                                    songsInPlaylistTotal = videosInPlaylistTotal =
-                                        response.songs.length;
-                                }
-                                next(null, response.songs);
-                            });
-                    },
-                    (songIds, next) => {
-                        let processed = 0;
-                        function checkDone() {
-                            if (processed === songIds.length) next();
-                        }
-                        for (let s = 0; s < songIds.length; s++) {
-                            lib.addSongToPlaylist(
-                                session,
-                                true,
-                                songIds[s],
-                                playlistId,
-                                (res) => {
-                                    processed++;
-                                    if (res.status === "success") {
-                                        addedSongs.push(songIds[s]);
-                                        songsSuccess++;
-                                    } else songsFail++;
-                                    checkDone();
-                                }
-                            );
-                        }
-                    },
-
-                    (next) => {
-                        playlists
-                            .runJob("GET_PLAYLIST", { playlistId })
-                            .then((playlist) => {
-                                next(null, playlist);
-                            })
-                            .catch(next);
-                    },
-
-                    (playlist, next) => {
-                        if (!playlist || playlist.createdBy !== session.userId)
-                            return next("Playlist not found.");
-                        next(null, playlist);
-                    },
-                ],
-                async (err, playlist) => {
-                    if (err) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "PLAYLIST_IMPORT",
-                            `Importing a YouTube playlist to private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
-                        );
-                        return cb({ status: "failure", message: err });
-                    } else {
-                        activities.runJob("ADD_ACTIVITY", {
-                            userId: session.userId,
-                            activityType: "added_songs_to_playlist",
-                            payload: addedSongs,
-                        });
-                        console.log(
-                            "SUCCESS",
-                            "PLAYLIST_IMPORT",
-                            `Successfully imported a YouTube playlist to private playlist "${playlistId}" for user "${session.userId}". Videos in playlist: ${videosInPlaylistTotal}, songs in playlist: ${songsInPlaylistTotal}, songs successfully added: ${songsSuccess}, songs failed: ${songsFail}.`
-                        );
-                        cb({
-                            status: "success",
-                            message: "Playlist has been successfully imported.",
-                            data: playlist.songs,
-                            stats: {
-                                videosInPlaylistTotal,
-                                songsInPlaylistTotal,
-                                songsAddedSuccessfully: songsSuccess,
-                                songsFailedToAdd: songsFail,
-                            },
-                        });
-                    }
-                }
-            );
-        }
-    ),
-
-    /**
-     * Removes a song from a private playlist
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} songId - the id of the song we are removing from the private playlist
-     * @param {String} playlistId - the id of the playlist we are removing the song from
-     * @param {Function} cb - gets called with the result
-     */
-    removeSongFromPlaylist: hooks.loginRequired(
-        async (session, songId, playlistId, cb) => {
-            const playlistModel = await db.runJob("GET_MODEL", {
-                modelName: "playlist",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        if (!songId || typeof songId !== "string")
-                            return next("Invalid song id.");
-                        if (!playlistId || typeof playlistId !== "string")
-                            return next("Invalid playlist id.");
-                        next();
-                    },
-
-                    (next) => {
-                        playlists
-                            .runJob("GET_PLAYLIST", { playlistId })
-                            .then((playlist) => {
-                                next(null, playlist);
-                            })
-                            .catch(next);
-                    },
-
-                    (playlist, next) => {
-                        if (!playlist || playlist.createdBy !== session.userId)
-                            return next("Playlist not found");
-                        playlistModel.updateOne(
-                            { _id: playlistId },
-                            { $pull: { songs: { songId: songId } } },
-                            next
-                        );
-                    },
-
-                    (res, next) => {
-                        playlists
-                            .runJob("UPDATE_PLAYLIST", { playlistId })
-                            .then((playlist) => {
-                                next(null, playlist);
-                            })
-                            .catch(next);
-                    },
-                ],
-                async (err, playlist) => {
-                    if (err) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "PLAYLIST_REMOVE_SONG",
-                            `Removing song "${songId}" from private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
-                        );
-                        return cb({ status: "failure", message: err });
-                    } else {
-                        console.log(
-                            "SUCCESS",
-                            "PLAYLIST_REMOVE_SONG",
-                            `Successfully removed song "${songId}" from private playlist "${playlistId}" for user "${session.userId}".`
-                        );
-                        cache.runJob("PUB", {
-                            channel: "playlist.removeSong",
-                            value: {
-                                playlistId: playlist._id,
-                                songId: songId,
-                                userId: session.userId,
-                            },
-                        });
-                        return cb({
-                            status: "success",
-                            message:
-                                "Song has been successfully removed from playlist",
-                            data: playlist.songs,
-                        });
-                    }
-                }
-            );
-        }
-    ),
-
-    /**
-     * Updates the displayName of a private playlist
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} playlistId - the id of the playlist we are updating the displayName for
-     * @param {Function} cb - gets called with the result
-     */
-    updateDisplayName: hooks.loginRequired(
-        async (session, playlistId, displayName, cb) => {
-            const playlistModel = await db.runJob("GET_MODEL", {
-                modelName: "playlist",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        playlistModel.updateOne(
-                            { _id: playlistId, createdBy: session.userId },
-                            { $set: { displayName } },
-                            { runValidators: true },
-                            next
-                        );
-                    },
-
-                    (res, next) => {
-                        playlists
-                            .runJob("UPDATE_PLAYLIST", { playlistId })
-                            .then((playlist) => {
-                                next(null, playlist);
-                            })
-                            .catch(next);
-                    },
-                ],
-                async (err, playlist) => {
-                    if (err) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "PLAYLIST_UPDATE_DISPLAY_NAME",
-                            `Updating display name to "${displayName}" for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
-                        );
-                        return cb({ status: "failure", message: err });
-                    }
-                    console.log(
-                        "SUCCESS",
-                        "PLAYLIST_UPDATE_DISPLAY_NAME",
-                        `Successfully updated display name to "${displayName}" for private playlist "${playlistId}" for user "${session.userId}".`
-                    );
-                    cache.runJob("PUB", {
-                        channel: "playlist.updateDisplayName",
-                        value: {
-                            playlistId: playlistId,
-                            displayName: displayName,
-                            userId: session.userId,
-                        },
-                    });
-                    return cb({
-                        status: "success",
-                        message: "Playlist has been successfully updated",
-                    });
-                }
-            );
-        }
-    ),
-
-    /**
-     * Moves a song to the top of the list in a private playlist
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} playlistId - the id of the playlist we are moving the song to the top from
-     * @param {String} songId - the id of the song we are moving to the top of the list
-     * @param {Function} cb - gets called with the result
-     */
-    moveSongToTop: hooks.loginRequired(
-        async (session, playlistId, songId, cb) => {
-            const playlistModel = await db.runJob("GET_MODEL", {
-                modelName: "playlist",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        playlists
-                            .runJob("GET_PLAYLIST", { playlistId })
-                            .then((playlist) => {
-                                next(null, playlist);
-                            })
-                            .catch(next);
-                    },
-
-                    (playlist, next) => {
-                        if (!playlist || playlist.createdBy !== session.userId)
-                            return next("Playlist not found");
-                        async.each(
-                            playlist.songs,
-                            (song, next) => {
-                                if (song.songId === songId) return next(song);
-                                next();
-                            },
-                            (err) => {
-                                if (err && err.songId) return next(null, err);
-                                next("Song not found");
-                            }
-                        );
-                    },
-
-                    (song, next) => {
-                        playlistModel.updateOne(
-                            { _id: playlistId },
-                            { $pull: { songs: { songId } } },
-                            (err) => {
-                                if (err) return next(err);
-                                return next(null, song);
-                            }
-                        );
-                    },
-
-                    (song, next) => {
-                        playlistModel.updateOne(
-                            { _id: playlistId },
-                            {
-                                $push: {
-                                    songs: {
-                                        $each: [song],
-                                        $position: 0,
-                                    },
-                                },
-                            },
-                            next
-                        );
-                    },
-
-                    (res, next) => {
-                        playlists
-                            .runJob("UPDATE_PLAYLIST", { playlistId })
-                            .then((playlist) => {
-                                next(null, playlist);
-                            })
-                            .catch(next);
-                    },
-                ],
-                async (err, playlist) => {
-                    if (err) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "PLAYLIST_MOVE_SONG_TO_TOP",
-                            `Moving song "${songId}" to the top for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
-                        );
-                        return cb({ status: "failure", message: err });
-                    }
-                    console.log(
-                        "SUCCESS",
-                        "PLAYLIST_MOVE_SONG_TO_TOP",
-                        `Successfully moved song "${songId}" to the top for private playlist "${playlistId}" for user "${session.userId}".`
-                    );
-                    cache.runJob("PUB", {
-                        channel: "playlist.moveSongToTop",
-                        value: {
-                            playlistId,
-                            songId,
-                            userId: session.userId,
-                        },
-                    });
-                    return cb({
-                        status: "success",
-                        message: "Playlist has been successfully updated",
-                    });
-                }
-            );
-        }
-    ),
-
-    /**
-     * Moves a song to the bottom of the list in a private playlist
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} playlistId - the id of the playlist we are moving the song to the bottom from
-     * @param {String} songId - the id of the song we are moving to the bottom of the list
-     * @param {Function} cb - gets called with the result
-     */
-    moveSongToBottom: hooks.loginRequired(
-        async (session, playlistId, songId, cb) => {
-            const playlistModel = await db.runJob("GET_MODEL", {
-                modelName: "playlist",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        playlists
-                            .runJob("GET_PLAYLIST", { playlistId })
-                            .then((playlist) => {
-                                next(null, playlist);
-                            })
-                            .catch(next);
-                    },
-
-                    (playlist, next) => {
-                        if (!playlist || playlist.createdBy !== session.userId)
-                            return next("Playlist not found");
-                        async.each(
-                            playlist.songs,
-                            (song, next) => {
-                                if (song.songId === songId) return next(song);
-                                next();
-                            },
-                            (err) => {
-                                if (err && err.songId) return next(null, err);
-                                next("Song not found");
-                            }
-                        );
-                    },
-
-                    (song, next) => {
-                        playlistModel.updateOne(
-                            { _id: playlistId },
-                            { $pull: { songs: { songId } } },
-                            (err) => {
-                                if (err) return next(err);
-                                return next(null, song);
-                            }
-                        );
-                    },
-
-                    (song, next) => {
-                        playlistModel.updateOne(
-                            { _id: playlistId },
-                            {
-                                $push: {
-                                    songs: song,
-                                },
-                            },
-                            next
-                        );
-                    },
-
-                    (res, next) => {
-                        playlists
-                            .runJob("UPDATE_PLAYLIST", { playlistId })
-                            .then((playlist) => {
-                                next(null, playlist);
-                            })
-                            .catch(next);
-                    },
-                ],
-                async (err, playlist) => {
-                    if (err) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "PLAYLIST_MOVE_SONG_TO_BOTTOM",
-                            `Moving song "${songId}" to the bottom for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
-                        );
-                        return cb({ status: "failure", message: err });
-                    }
-                    console.log(
-                        "SUCCESS",
-                        "PLAYLIST_MOVE_SONG_TO_BOTTOM",
-                        `Successfully moved song "${songId}" to the bottom for private playlist "${playlistId}" for user "${session.userId}".`
-                    );
-                    cache.runJob("PUB", {
-                        channel: "playlist.moveSongToBottom",
-                        value: {
-                            playlistId,
-                            songId,
-                            userId: session.userId,
-                        },
-                    });
-                    return cb({
-                        status: "success",
-                        message: "Playlist has been successfully updated",
-                    });
-                }
-            );
-        }
-    ),
-
-    /**
-     * Removes a private playlist
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} playlistId - the id of the playlist we are moving the song to the top from
-     * @param {Function} cb - gets called with the result
-     */
-    remove: hooks.loginRequired(async (session, playlistId, cb) => {
-        const stationModel = await db.runJob("GET_MODEL", {
-            modelName: "station",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    playlists
-                        .runJob("DELETE_PLAYLIST", { playlistId })
-                        .then(next)
-                        .catch(next);
-                },
-
-                (next) => {
-                    stationModel.find({ privatePlaylist: playlistId }, (err, res) => {
-                        next(err, res);
-                    });
-                },
-
-                (stations, next) => {
-                    async.each(
-                        stations,
-                        (station, next) => {
-                            async.waterfall(
-                                [
-                                    (next) => {
-                                        stationModel.updateOne(
-                                            { _id: station._id },
-                                            { $set: { privatePlaylist: null } },
-                                            { runValidators: true },
-                                            next
-                                        );
-                                    },
-
-                                    (res, next) => {
-                                        if (!station.partyMode) {
-                                            moduleManager.modules["stations"]
-                                                .runJob("UPDATE_STATION", {
-                                                    stationId: station._id,
-                                                })
-                                                .then((station) =>
-                                                    next(null, station)
-                                                )
-                                                .catch(next);
-                                            cache.runJob("PUB", {
-                                                channel:
-                                                    "privatePlaylist.selected",
-                                                value: {
-                                                    playlistId: null,
-                                                    stationId: station._id,
-                                                },
-                                            });
-                                        } else next();
-                                    },
-                                ],
-
-                                (err) => {
-                                    next();
-                                }
-                            );
-                        },
-                        (err) => {
-                            next();
-                        }
-                    );
-                },
-            ],
-            async (err) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "PLAYLIST_REMOVE",
-                        `Removing private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "PLAYLIST_REMOVE",
-                    `Successfully removed private playlist "${playlistId}" for user "${session.userId}".`
-                );
-                cache.runJob("PUB", {
-                    channel: "playlist.delete",
-                    value: {
-                        userId: session.userId,
-                        playlistId,
-                    },
-                });
-                activities.runJob("ADD_ACTIVITY", {
-                    userId: session.userId,
-                    activityType: "deleted_playlist",
-                    payload: [playlistId],
-                });
-                return cb({
-                    status: "success",
-                    message: "Playlist successfully removed",
-                });
-            }
-        );
-    }),
+const lib = {
+	/**
+	 * Gets the first song from a private playlist
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} playlistId - the id of the playlist we are getting the first song from
+	 * @param {Function} cb - gets called with the result
+	 */
+	getFirstSong: isLoginRequired((session, playlistId, cb) => {
+		async.waterfall(
+			[
+				next => {
+					playlists
+						.runJob("GET_PLAYLIST", { playlistId })
+						.then(playlist => {
+							next(null, playlist);
+						})
+						.catch(next);
+				},
+
+				(playlist, next) => {
+					if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found.");
+					return next(null, playlist.songs[0]);
+				}
+			],
+			async (err, song) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"PLAYLIST_GET_FIRST_SONG",
+						`Getting the first song of playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"PLAYLIST_GET_FIRST_SONG",
+					`Successfully got the first song of playlist "${playlistId}" for user "${session.userId}".`
+				);
+				return cb({
+					status: "success",
+					song
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Gets all playlists for the user requesting it
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {Function} cb - gets called with the result
+	 */
+	indexForUser: isLoginRequired(async (session, cb) => {
+		const playlistModel = await db.runJob("GET_MODEL", {
+			modelName: "playlist"
+		});
+		async.waterfall(
+			[
+				next => {
+					playlistModel.find({ createdBy: session.userId }, next);
+				}
+			],
+			async (err, playlists) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"PLAYLIST_INDEX_FOR_USER",
+						`Indexing playlists for user "${session.userId}" failed. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"PLAYLIST_INDEX_FOR_USER",
+					`Successfully indexed playlists for user "${session.userId}".`
+				);
+				return cb({
+					status: "success",
+					data: playlists
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Creates a new private playlist
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} data - the data for the new private playlist
+	 * @param {Function} cb - gets called with the result
+	 */
+	create: isLoginRequired(async (session, data, cb) => {
+		const playlistModel = await db.runJob("GET_MODEL", {
+			modelName: "playlist"
+		});
+		async.waterfall(
+			[
+				next => (data ? next() : cb({ status: "failure", message: "Invalid data" })),
+
+				next => {
+					const { displayName, songs } = data;
+					playlistModel.create(
+						{
+							displayName,
+							songs,
+							createdBy: session.userId,
+							createdAt: Date.now()
+						},
+						next
+					);
+				}
+			],
+			async (err, playlist) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"PLAYLIST_CREATE",
+						`Creating private playlist failed for user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				cache.runJob("PUB", {
+					channel: "playlist.create",
+					value: playlist._id
+				});
+				activities.runJob("ADD_ACTIVITY", {
+					userId: session.userId,
+					activityType: "created_playlist",
+					payload: [playlist._id]
+				});
+				console.log(
+					"SUCCESS",
+					"PLAYLIST_CREATE",
+					`Successfully created private playlist for user "${session.userId}".`
+				);
+				return cb({
+					status: "success",
+					message: "Successfully created playlist",
+					data: {
+						_id: playlist._id
+					}
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Gets a playlist from id
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} playlistId - the id of the playlist we are getting
+	 * @param {Function} cb - gets called with the result
+	 */
+	getPlaylist: isLoginRequired((session, playlistId, cb) => {
+		async.waterfall(
+			[
+				next => {
+					playlists
+						.runJob("GET_PLAYLIST", { playlistId })
+						.then(playlist => {
+							next(null, playlist);
+						})
+						.catch(next);
+				},
+
+				(playlist, next) => {
+					if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found");
+					return next(null, playlist);
+				}
+			],
+			async (err, playlist) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"PLAYLIST_GET",
+						`Getting private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"PLAYLIST_GET",
+					`Successfully got private playlist "${playlistId}" for user "${session.userId}".`
+				);
+				return cb({
+					status: "success",
+					data: playlist
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Obtains basic metadata of a playlist in order to format an activity
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} playlistId - the playlist id
+	 * @param {Function} cb - callback
+	 */
+	getPlaylistForActivity: (session, playlistId, cb) => {
+		async.waterfall(
+			[
+				next => {
+					playlists
+						.runJob("GET_PLAYLIST", { playlistId })
+						.then(playlist => {
+							next(null, playlist);
+						})
+						.catch(next);
+				}
+			],
+			async (err, playlist) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"PLAYLISTS_GET_PLAYLIST_FOR_ACTIVITY",
+						`Failed to obtain metadata of playlist ${playlistId} for activity formatting. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"PLAYLISTS_GET_PLAYLIST_FOR_ACTIVITY",
+					`Obtained metadata of playlist ${playlistId} for activity formatting successfully.`
+				);
+				return cb({
+					status: "success",
+					data: {
+						title: playlist.displayName
+					}
+				});
+			}
+		);
+	},
+
+	// TODO Remove this
+	/**
+	 * Updates a private playlist
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} playlistId - the id of the playlist we are updating
+	 * @param {object} playlist - the new private playlist object
+	 * @param {Function} cb - gets called with the result
+	 */
+	update: isLoginRequired(async (session, playlistId, playlist, cb) => {
+		const playlistModel = await db.runJob("GET_MODEL", {
+			modelName: "playlist"
+		});
+		async.waterfall(
+			[
+				next => {
+					playlistModel.updateOne(
+						{ _id: playlistId, createdBy: session.userId },
+						playlist,
+						{ runValidators: true },
+						next
+					);
+				},
+
+				(res, next) => {
+					playlists
+						.runJob("UPDATE_PLAYLIST", { playlistId })
+						.then(playlist => {
+							next(null, playlist);
+						})
+						.catch(next);
+				}
+			],
+			async (err, playlist) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"PLAYLIST_UPDATE",
+						`Updating private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"PLAYLIST_UPDATE",
+					`Successfully updated private playlist "${playlistId}" for user "${session.userId}".`
+				);
+				return cb({
+					status: "success",
+					data: playlist
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Updates a private playlist
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} playlistId - the id of the playlist we are updating
+	 * @param {Function} cb - gets called with the result
+	 */
+	shuffle: isLoginRequired(async (session, playlistId, cb) => {
+		const playlistModel = await db.runJob("GET_MODEL", {
+			modelName: "playlist"
+		});
+		async.waterfall(
+			[
+				next => {
+					if (!playlistId) return next("No playlist id.");
+					return playlistModel.findById(playlistId, next);
+				},
+
+				(playlist, next) => {
+					utils
+						.runJob("SHUFFLE", { array: playlist.songs })
+						.then(result => {
+							next(null, result.array);
+						})
+						.catch(next);
+				},
+
+				(songs, next) => {
+					playlistModel.updateOne({ _id: playlistId }, { $set: { songs } }, { runValidators: true }, next);
+				},
+
+				(res, next) => {
+					playlists
+						.runJob("UPDATE_PLAYLIST", { playlistId })
+						.then(playlist => {
+							next(null, playlist);
+						})
+						.catch(next);
+				}
+			],
+			async (err, playlist) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"PLAYLIST_SHUFFLE",
+						`Updating private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"PLAYLIST_SHUFFLE",
+					`Successfully updated private playlist "${playlistId}" for user "${session.userId}".`
+				);
+				return cb({
+					status: "success",
+					message: "Successfully shuffled playlist.",
+					data: playlist
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Adds a song to a private playlist
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {boolean} isSet - is the song part of a set of songs to be added
+	 * @param {string} songId - the id of the song we are trying to add
+	 * @param {string} playlistId - the id of the playlist we are adding the song to
+	 * @param {Function} cb - gets called with the result
+	 */
+	addSongToPlaylist: isLoginRequired(async (session, isSet, songId, playlistId, cb) => {
+		const playlistModel = await db.runJob("GET_MODEL", {
+			modelName: "playlist"
+		});
+
+		async.waterfall(
+			[
+				next => {
+					playlists
+						.runJob("GET_PLAYLIST", { playlistId })
+						.then(playlist => {
+							if (!playlist || playlist.createdBy !== session.userId)
+								return next("Something went wrong when trying to get the playlist");
+
+							return async.each(
+								playlist.songs,
+								(song, next) => {
+									if (song.songId === songId) return next("That song is already in the playlist");
+									return next();
+								},
+								next
+							);
+						})
+						.catch(next);
+				},
+				next => {
+					songs
+						.runJob("GET_SONG", { id: songId })
+						.then(response => {
+							const { song } = response;
+							next(null, {
+								_id: song._id,
+								songId,
+								title: song.title,
+								duration: song.duration
+							});
+						})
+						.catch(() => {
+							utils
+								.runJob("GET_SONG_FROM_YOUTUBE", { songId })
+								.then(response => next(null, response.song))
+								.catch(next);
+						});
+				},
+				(newSong, next) => {
+					playlistModel.updateOne(
+						{ _id: playlistId },
+						{ $push: { songs: newSong } },
+						{ runValidators: true },
+						err => {
+							if (err) return next(err);
+							return playlists
+								.runJob("UPDATE_PLAYLIST", { playlistId })
+								.then(playlist => next(null, playlist, newSong))
+								.catch(next);
+						}
+					);
+				}
+			],
+			async (err, playlist, newSong) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"PLAYLIST_ADD_SONG",
+						`Adding song "${songId}" to private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"PLAYLIST_ADD_SONG",
+					`Successfully added song "${songId}" to private playlist "${playlistId}" for user "${session.userId}".`
+				);
+				if (!isSet)
+					activities.runJob("ADD_ACTIVITY", {
+						userId: session.userId,
+						activityType: "added_song_to_playlist",
+						payload: [{ songId, playlistId }]
+					});
+
+				cache.runJob("PUB", {
+					channel: "playlist.addSong",
+					value: {
+						playlistId: playlist._id,
+						song: newSong,
+						userId: session.userId
+					}
+				});
+				return cb({
+					status: "success",
+					message: "Song has been successfully added to the playlist",
+					data: playlist.songs
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Adds a set of songs to a private playlist
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} url - the url of the the YouTube playlist
+	 * @param {string} playlistId - the id of the playlist we are adding the set of songs to
+	 * @param {boolean} musicOnly - whether to only add music to the playlist
+	 * @param {Function} cb - gets called with the result
+	 */
+	addSetToPlaylist: isLoginRequired((session, url, playlistId, musicOnly, cb) => {
+		let videosInPlaylistTotal = 0;
+		let songsInPlaylistTotal = 0;
+		let songsSuccess = 0;
+		let songsFail = 0;
+
+		const addedSongs = [];
+
+		async.waterfall(
+			[
+				next => {
+					utils
+						.runJob("GET_PLAYLIST_FROM_YOUTUBE", {
+							url,
+							musicOnly
+						})
+						.then(response => {
+							if (response.filteredSongs) {
+								videosInPlaylistTotal = response.songs.length;
+								songsInPlaylistTotal = response.filteredSongs.length;
+							} else {
+								songsInPlaylistTotal = videosInPlaylistTotal = response.songs.length;
+							}
+							next(null, response.songs);
+						});
+				},
+				(songIds, next) => {
+					let processed = 0;
+
+					/**
+					 *
+					 */
+					function checkDone() {
+						if (processed === songIds.length) next();
+					}
+					for (let s = 0; s < songIds.length; s += 1) {
+						// eslint-disable-next-line no-loop-func
+						lib.addSongToPlaylist(session, true, songIds[s], playlistId, res => {
+							processed += 1;
+							if (res.status === "success") {
+								addedSongs.push(songIds[s]);
+								songsSuccess += 1;
+							} else songsFail += 1;
+							checkDone();
+						});
+					}
+				},
+
+				next => {
+					playlists
+						.runJob("GET_PLAYLIST", { playlistId })
+						.then(playlist => {
+							next(null, playlist);
+						})
+						.catch(next);
+				},
+
+				(playlist, next) => {
+					if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found.");
+					return next(null, playlist);
+				}
+			],
+			async (err, playlist) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"PLAYLIST_IMPORT",
+						`Importing a YouTube playlist to private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				activities.runJob("ADD_ACTIVITY", {
+					userId: session.userId,
+					activityType: "added_songs_to_playlist",
+					payload: addedSongs
+				});
+				console.log(
+					"SUCCESS",
+					"PLAYLIST_IMPORT",
+					`Successfully imported a YouTube playlist to private playlist "${playlistId}" for user "${session.userId}". Videos in playlist: ${videosInPlaylistTotal}, songs in playlist: ${songsInPlaylistTotal}, songs successfully added: ${songsSuccess}, songs failed: ${songsFail}.`
+				);
+				return cb({
+					status: "success",
+					message: "Playlist has been successfully imported.",
+					data: playlist.songs,
+					stats: {
+						videosInPlaylistTotal,
+						songsInPlaylistTotal,
+						songsAddedSuccessfully: songsSuccess,
+						songsFailedToAdd: songsFail
+					}
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Removes a song from a private playlist
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} songId - the id of the song we are removing from the private playlist
+	 * @param {string} playlistId - the id of the playlist we are removing the song from
+	 * @param {Function} cb - gets called with the result
+	 */
+	removeSongFromPlaylist: isLoginRequired(async (session, songId, playlistId, cb) => {
+		const playlistModel = await db.runJob("GET_MODEL", {
+			modelName: "playlist"
+		});
+		async.waterfall(
+			[
+				next => {
+					if (!songId || typeof songId !== "string") return next("Invalid song id.");
+					if (!playlistId || typeof playlistId !== "string") return next("Invalid playlist id.");
+					return next();
+				},
+
+				next => {
+					playlists
+						.runJob("GET_PLAYLIST", { playlistId })
+						.then(playlist => {
+							next(null, playlist);
+						})
+						.catch(next);
+				},
+
+				(playlist, next) => {
+					if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found");
+					return playlistModel.updateOne({ _id: playlistId }, { $pull: { songs: { songId } } }, next);
+				},
+
+				(res, next) => {
+					playlists
+						.runJob("UPDATE_PLAYLIST", { playlistId })
+						.then(playlist => {
+							next(null, playlist);
+						})
+						.catch(next);
+				}
+			],
+			async (err, playlist) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"PLAYLIST_REMOVE_SONG",
+						`Removing song "${songId}" from private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"PLAYLIST_REMOVE_SONG",
+					`Successfully removed song "${songId}" from private playlist "${playlistId}" for user "${session.userId}".`
+				);
+				cache.runJob("PUB", {
+					channel: "playlist.removeSong",
+					value: {
+						playlistId: playlist._id,
+						songId,
+						userId: session.userId
+					}
+				});
+				return cb({
+					status: "success",
+					message: "Song has been successfully removed from playlist",
+					data: playlist.songs
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Updates the displayName of a private playlist
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} playlistId - the id of the playlist we are updating the displayName for
+	 * @param {Function} cb - gets called with the result
+	 */
+	updateDisplayName: isLoginRequired(async (session, playlistId, displayName, cb) => {
+		const playlistModel = await db.runJob("GET_MODEL", {
+			modelName: "playlist"
+		});
+		async.waterfall(
+			[
+				next => {
+					playlistModel.updateOne(
+						{ _id: playlistId, createdBy: session.userId },
+						{ $set: { displayName } },
+						{ runValidators: true },
+						next
+					);
+				},
+
+				(res, next) => {
+					playlists
+						.runJob("UPDATE_PLAYLIST", { playlistId })
+						.then(playlist => {
+							next(null, playlist);
+						})
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"PLAYLIST_UPDATE_DISPLAY_NAME",
+						`Updating display name to "${displayName}" for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"PLAYLIST_UPDATE_DISPLAY_NAME",
+					`Successfully updated display name to "${displayName}" for private playlist "${playlistId}" for user "${session.userId}".`
+				);
+				cache.runJob("PUB", {
+					channel: "playlist.updateDisplayName",
+					value: {
+						playlistId,
+						displayName,
+						userId: session.userId
+					}
+				});
+				return cb({
+					status: "success",
+					message: "Playlist has been successfully updated"
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Moves a song to the top of the list in a private playlist
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} playlistId - the id of the playlist we are moving the song to the top from
+	 * @param {string} songId - the id of the song we are moving to the top of the list
+	 * @param {Function} cb - gets called with the result
+	 */
+	moveSongToTop: isLoginRequired(async (session, playlistId, songId, cb) => {
+		const playlistModel = await db.runJob("GET_MODEL", {
+			modelName: "playlist"
+		});
+		async.waterfall(
+			[
+				next => {
+					playlists
+						.runJob("GET_PLAYLIST", { playlistId })
+						.then(playlist => {
+							next(null, playlist);
+						})
+						.catch(next);
+				},
+
+				(playlist, next) => {
+					if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found");
+					return async.each(
+						playlist.songs,
+						(song, next) => {
+							if (song.songId === songId) return next(song);
+							return next();
+						},
+						err => {
+							if (err && err.songId) return next(null, err);
+							return next("Song not found");
+						}
+					);
+				},
+
+				(song, next) => {
+					playlistModel.updateOne({ _id: playlistId }, { $pull: { songs: { songId } } }, err => {
+						if (err) return next(err);
+						return next(null, song);
+					});
+				},
+
+				(song, next) => {
+					playlistModel.updateOne(
+						{ _id: playlistId },
+						{
+							$push: {
+								songs: {
+									$each: [song],
+									$position: 0
+								}
+							}
+						},
+						next
+					);
+				},
+
+				(res, next) => {
+					playlists
+						.runJob("UPDATE_PLAYLIST", { playlistId })
+						.then(playlist => {
+							next(null, playlist);
+						})
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"PLAYLIST_MOVE_SONG_TO_TOP",
+						`Moving song "${songId}" to the top for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+
+				console.log(
+					"SUCCESS",
+					"PLAYLIST_MOVE_SONG_TO_TOP",
+					`Successfully moved song "${songId}" to the top for private playlist "${playlistId}" for user "${session.userId}".`
+				);
+
+				cache.runJob("PUB", {
+					channel: "playlist.moveSongToTop",
+					value: {
+						playlistId,
+						songId,
+						userId: session.userId
+					}
+				});
+
+				return cb({
+					status: "success",
+					message: "Playlist has been successfully updated"
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Moves a song to the bottom of the list in a private playlist
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} playlistId - the id of the playlist we are moving the song to the bottom from
+	 * @param {string} songId - the id of the song we are moving to the bottom of the list
+	 * @param {Function} cb - gets called with the result
+	 */
+	moveSongToBottom: isLoginRequired(async (session, playlistId, songId, cb) => {
+		const playlistModel = await db.runJob("GET_MODEL", {
+			modelName: "playlist"
+		});
+		async.waterfall(
+			[
+				next => {
+					playlists
+						.runJob("GET_PLAYLIST", { playlistId })
+						.then(playlist => {
+							next(null, playlist);
+						})
+						.catch(next);
+				},
+
+				(playlist, next) => {
+					if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found");
+					return async.each(
+						playlist.songs,
+						(song, next) => {
+							if (song.songId === songId) return next(song);
+							return next();
+						},
+						err => {
+							if (err && err.songId) return next(null, err);
+							return next("Song not found");
+						}
+					);
+				},
+
+				(song, next) => {
+					playlistModel.updateOne({ _id: playlistId }, { $pull: { songs: { songId } } }, err => {
+						if (err) return next(err);
+						return next(null, song);
+					});
+				},
+
+				(song, next) => {
+					playlistModel.updateOne(
+						{ _id: playlistId },
+						{
+							$push: {
+								songs: song
+							}
+						},
+						next
+					);
+				},
+
+				(res, next) => {
+					playlists
+						.runJob("UPDATE_PLAYLIST", { playlistId })
+						.then(playlist => {
+							next(null, playlist);
+						})
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"PLAYLIST_MOVE_SONG_TO_BOTTOM",
+						`Moving song "${songId}" to the bottom for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+
+				console.log(
+					"SUCCESS",
+					"PLAYLIST_MOVE_SONG_TO_BOTTOM",
+					`Successfully moved song "${songId}" to the bottom for private playlist "${playlistId}" for user "${session.userId}".`
+				);
+
+				cache.runJob("PUB", {
+					channel: "playlist.moveSongToBottom",
+					value: {
+						playlistId,
+						songId,
+						userId: session.userId
+					}
+				});
+
+				return cb({
+					status: "success",
+					message: "Playlist has been successfully updated"
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Removes a private playlist
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} playlistId - the id of the playlist we are moving the song to the top from
+	 * @param {Function} cb - gets called with the result
+	 */
+	remove: isLoginRequired(async (session, playlistId, cb) => {
+		const stationModel = await db.runJob("GET_MODEL", {
+			modelName: "station"
+		});
+
+		async.waterfall(
+			[
+				next => {
+					playlists.runJob("DELETE_PLAYLIST", { playlistId }).then(next).catch(next);
+				},
+
+				next => {
+					stationModel.find({ privatePlaylist: playlistId }, (err, res) => {
+						next(err, res);
+					});
+				},
+
+				(stations, next) => {
+					async.each(
+						stations,
+						(station, next) => {
+							async.waterfall(
+								[
+									next => {
+										stationModel.updateOne(
+											{ _id: station._id },
+											{ $set: { privatePlaylist: null } },
+											{ runValidators: true },
+											next
+										);
+									},
+
+									(res, next) => {
+										if (!station.partyMode) {
+											moduleManager.modules.stations
+												.runJob("UPDATE_STATION", {
+													stationId: station._id
+												})
+												.then(station => next(null, station))
+												.catch(next);
+											cache.runJob("PUB", {
+												channel: "privatePlaylist.selected",
+												value: {
+													playlistId: null,
+													stationId: station._id
+												}
+											});
+										} else next();
+									}
+								],
+
+								() => {
+									next();
+								}
+							);
+						},
+						() => {
+							next();
+						}
+					);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"PLAYLIST_REMOVE",
+						`Removing private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+
+				console.log(
+					"SUCCESS",
+					"PLAYLIST_REMOVE",
+					`Successfully removed private playlist "${playlistId}" for user "${session.userId}".`
+				);
+
+				cache.runJob("PUB", {
+					channel: "playlist.delete",
+					value: {
+						userId: session.userId,
+						playlistId
+					}
+				});
+
+				activities.runJob("ADD_ACTIVITY", {
+					userId: session.userId,
+					activityType: "deleted_playlist",
+					payload: [playlistId]
+				});
+
+				return cb({
+					status: "success",
+					message: "Playlist successfully removed"
+				});
+			}
+		);
+	})
 };
 
-module.exports = lib;
+export default lib;

+ 142 - 155
backend/logic/actions/punishments.js

@@ -1,168 +1,155 @@
-"use strict";
+import async from "async";
 
-const async = require("async");
-
-const hooks = require("./hooks");
+import { isAdminRequired, isLoginRequired, isOwnerRequired } from "./hooks";
+import db from "../db";
 // const moduleManager = require("../../index");
 
 // const logger = require("logger");
-const utils = require("../utils");
-const cache = require("../cache");
-const db = require("../db");
-const punishments = require("../punishments");
+import utils from "../utils";
+
+import cache from "../cache";
+
+import punishments from "../punishments";
 
 cache.runJob("SUB", {
-    channel: "ip.ban",
-    cb: (data) => {
-        utils.runJob("EMIT_TO_ROOM", {
-            room: "admin.punishments",
-            args: ["event:admin.punishment.added", data.punishment],
-        });
-        utils.runJob("SOCKETS_FROM_IP", { ip: data.ip }).then((sockets) => {
-            sockets.forEach((socket) => {
-                socket.disconnect(true);
-            });
-        });
-    },
+	channel: "ip.ban",
+	cb: data => {
+		utils.runJob("EMIT_TO_ROOM", {
+			room: "admin.punishments",
+			args: ["event:admin.punishment.added", data.punishment]
+		});
+		utils.runJob("SOCKETS_FROM_IP", { ip: data.ip }).then(sockets => {
+			sockets.forEach(socket => {
+				socket.disconnect(true);
+			});
+		});
+	}
 });
 
-module.exports = {
-    /**
-     * Gets all punishments
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {Function} cb - gets called with the result
-     */
-    index: hooks.adminRequired(async (session, cb) => {
-        const punishmentModel = await db.runJob("GET_MODEL", {
-            modelName: "punishment",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    punishmentModel.find({}, next);
-                },
-            ],
-            async (err, punishments) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "PUNISHMENTS_INDEX",
-                        `Indexing punishments failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "PUNISHMENTS_INDEX",
-                    "Indexing punishments successful."
-                );
-                cb({ status: "success", data: punishments });
-            }
-        );
-    }),
+export default {
+	/**
+	 * Gets all punishments
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {Function} cb - gets called with the result
+	 */
+	index: isAdminRequired(async (session, cb) => {
+		const punishmentModel = await db.runJob("GET_MODEL", {
+			modelName: "punishment"
+		});
+		async.waterfall(
+			[
+				next => {
+					punishmentModel.find({}, next);
+				}
+			],
+			async (err, punishments) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "PUNISHMENTS_INDEX", `Indexing punishments failed. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "PUNISHMENTS_INDEX", "Indexing punishments successful.");
+				return cb({ status: "success", data: punishments });
+			}
+		);
+	}),
 
-    /**
-     * Bans an IP address
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} value - the ip address that is going to be banned
-     * @param {String} reason - the reason for the ban
-     * @param {String} expiresAt - the time the ban expires
-     * @param {Function} cb - gets called with the result
-     */
-    banIP: hooks.adminRequired((session, value, reason, expiresAt, cb) => {
-        async.waterfall(
-            [
-                (next) => {
-                    if (!value)
-                        return next("You must provide an IP address to ban.");
-                    else if (!reason)
-                        return next("You must provide a reason for the ban.");
-                    else return next();
-                },
+	/**
+	 * Bans an IP address
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} value - the ip address that is going to be banned
+	 * @param {string} reason - the reason for the ban
+	 * @param {string} expiresAt - the time the ban expires
+	 * @param {Function} cb - gets called with the result
+	 */
+	banIP: isAdminRequired((session, value, reason, expiresAt, cb) => {
+		async.waterfall(
+			[
+				next => {
+					if (!value) return next("You must provide an IP address to ban.");
+					if (!reason) return next("You must provide a reason for the ban.");
+					return next();
+				},
 
-                (next) => {
-                    if (!expiresAt || typeof expiresAt !== "string")
-                        return next("Invalid expire date.");
-                    let date = new Date();
-                    switch (expiresAt) {
-                        case "1h":
-                            expiresAt = date.setHours(date.getHours() + 1);
-                            break;
-                        case "12h":
-                            expiresAt = date.setHours(date.getHours() + 12);
-                            break;
-                        case "1d":
-                            expiresAt = date.setDate(date.getDate() + 1);
-                            break;
-                        case "1w":
-                            expiresAt = date.setDate(date.getDate() + 7);
-                            break;
-                        case "1m":
-                            expiresAt = date.setMonth(date.getMonth() + 1);
-                            break;
-                        case "3m":
-                            expiresAt = date.setMonth(date.getMonth() + 3);
-                            break;
-                        case "6m":
-                            expiresAt = date.setMonth(date.getMonth() + 6);
-                            break;
-                        case "1y":
-                            expiresAt = date.setFullYear(
-                                date.getFullYear() + 1
-                            );
-                            break;
-                        case "never":
-                            expiresAt = new Date(3093527980800000);
-                            break;
-                        default:
-                            return next("Invalid expire date.");
-                    }
+				next => {
+					if (!expiresAt || typeof expiresAt !== "string") return next("Invalid expire date.");
+					const date = new Date();
+					switch (expiresAt) {
+						case "1h":
+							expiresAt = date.setHours(date.getHours() + 1);
+							break;
+						case "12h":
+							expiresAt = date.setHours(date.getHours() + 12);
+							break;
+						case "1d":
+							expiresAt = date.setDate(date.getDate() + 1);
+							break;
+						case "1w":
+							expiresAt = date.setDate(date.getDate() + 7);
+							break;
+						case "1m":
+							expiresAt = date.setMonth(date.getMonth() + 1);
+							break;
+						case "3m":
+							expiresAt = date.setMonth(date.getMonth() + 3);
+							break;
+						case "6m":
+							expiresAt = date.setMonth(date.getMonth() + 6);
+							break;
+						case "1y":
+							expiresAt = date.setFullYear(date.getFullYear() + 1);
+							break;
+						case "never":
+							expiresAt = new Date(3093527980800000);
+							break;
+						default:
+							return next("Invalid expire date.");
+					}
 
-                    next();
-                },
+					return next();
+				},
 
-                (next) => {
-                    punishments
-                        .runJob("ADD_PUNISHMENT", {
-                            type: "banUserIp",
-                            value,
-                            reason,
-                            expiresAt,
-                            punishedBy: session.userId,
-                        })
-                        .then((punishment) => {
-                            next(null, punishment);
-                        })
-                        .catch(next);
-                },
-            ],
-            async (err, punishment) => {
-                if (err && err !== true) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "BAN_IP",
-                        `User ${session.userId} failed to ban IP address ${value} with the reason ${reason}. '${err}'`
-                    );
-                    cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "BAN_IP",
-                    `User ${session.userId} has successfully banned IP address ${value} with the reason ${reason}.`
-                );
-                cache.runJob("PUB", {
-                    channel: "ip.ban",
-                    value: { ip: value, punishment },
-                });
-                return cb({
-                    status: "success",
-                    message: "Successfully banned IP address.",
-                });
-            }
-        );
-    }),
+				next => {
+					punishments
+						.runJob("ADD_PUNISHMENT", {
+							type: "banUserIp",
+							value,
+							reason,
+							expiresAt,
+							punishedBy: session.userId
+						})
+						.then(punishment => {
+							next(null, punishment);
+						})
+						.catch(next);
+				}
+			],
+			async (err, punishment) => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"BAN_IP",
+						`User ${session.userId} failed to ban IP address ${value} with the reason ${reason}. '${err}'`
+					);
+					cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"BAN_IP",
+					`User ${session.userId} has successfully banned IP address ${value} with the reason ${reason}.`
+				);
+				cache.runJob("PUB", {
+					channel: "ip.ban",
+					value: { ip: value, punishment }
+				});
+				return cb({
+					status: "success",
+					message: "Successfully banned IP address."
+				});
+			}
+		);
+	})
 };

+ 350 - 368
backend/logic/actions/queueSongs.js

@@ -1,397 +1,379 @@
-"use strict";
+import config from "config";
 
-const config = require("config");
-const async = require("async");
-const request = require("request");
+import async from "async";
 
-const hooks = require("./hooks");
+import { isAdminRequired, isLoginRequired, isOwnerRequired } from "./hooks";
 
-const db = require("../db");
-const utils = require("../utils");
-const cache = require("../cache");
+import db from "../db";
+
+import utils from "../utils";
+
+import cache from "../cache";
 // const logger = moduleManager.modules["logger"];
 
 cache.runJob("SUB", {
-    channel: "queue.newSong",
-    cb: async (songId) => {
-        const queueSongModel = await db.runJob("GET_MODEL", {
-            modelName: "queueSong",
-        });
-        queueSongModel.findOne({ _id: songId }, (err, song) => {
-            utils.runJob("EMIT_TO_ROOM", {
-                room: "admin.queue",
-                args: ["event:admin.queueSong.added", song],
-            });
-        });
-    },
+	channel: "queue.newSong",
+	cb: async songId => {
+		const queueSongModel = await db.runJob("GET_MODEL", {
+			modelName: "queueSong"
+		});
+		queueSongModel.findOne({ _id: songId }, (err, song) => {
+			utils.runJob("EMIT_TO_ROOM", {
+				room: "admin.queue",
+				args: ["event:admin.queueSong.added", song]
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "queue.removedSong",
-    cb: (songId) => {
-        utils.runJob("EMIT_TO_ROOM", {
-            room: "admin.queue",
-            args: ["event:admin.queueSong.removed", songId],
-        });
-    },
+	channel: "queue.removedSong",
+	cb: songId => {
+		utils.runJob("EMIT_TO_ROOM", {
+			room: "admin.queue",
+			args: ["event:admin.queueSong.removed", songId]
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "queue.update",
-    cb: async (songId) => {
-        const queueSongModel = await db.runJob("GET_MODEL", {
-            modelName: "queueSong",
-        });
-        queueSongModel.findOne({ _id: songId }, (err, song) => {
-            utils.runJob("EMIT_TO_ROOM", {
-                room: "admin.queue",
-                args: ["event:admin.queueSong.updated", song],
-            });
-        });
-    },
+	channel: "queue.update",
+	cb: async songId => {
+		const queueSongModel = await db.runJob("GET_MODEL", {
+			modelName: "queueSong"
+		});
+		queueSongModel.findOne({ _id: songId }, (err, song) => {
+			utils.runJob("EMIT_TO_ROOM", {
+				room: "admin.queue",
+				args: ["event:admin.queueSong.updated", song]
+			});
+		});
+	}
 });
 
-let lib = {
-    /**
-     * Returns the length of the queue songs list
-     *
-     * @param session
-     * @param cb
-     */
-    length: hooks.adminRequired(async (session, cb) => {
-        const queueSongModel = await db.runJob("GET_MODEL", {
-            modelName: "queueSong",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    queueSongModel.countDocuments({}, next);
-                },
-            ],
-            async (err, count) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "QUEUE_SONGS_LENGTH",
-                        `Failed to get length from queue songs. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "QUEUE_SONGS_LENGTH",
-                    `Got length from queue songs successfully.`
-                );
-                cb(count);
-            }
-        );
-    }),
+const lib = {
+	/**
+	 * Returns the length of the queue songs list
+	 *
+	 * @param session
+	 * @param cb
+	 */
+	length: isAdminRequired(async (session, cb) => {
+		const queueSongModel = await db.runJob("GET_MODEL", {
+			modelName: "queueSong"
+		});
+		async.waterfall(
+			[
+				next => {
+					queueSongModel.countDocuments({}, next);
+				}
+			],
+			async (err, count) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "QUEUE_SONGS_LENGTH", `Failed to get length from queue songs. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "QUEUE_SONGS_LENGTH", `Got length from queue songs successfully.`);
+				return cb(count);
+			}
+		);
+	}),
+
+	/**
+	 * Gets a set of queue songs
+	 *
+	 * @param session
+	 * @param set - the set number to return
+	 * @param cb
+	 */
+	getSet: isAdminRequired(async (session, set, cb) => {
+		const queueSongModel = await db.runJob("GET_MODEL", {
+			modelName: "queueSong"
+		});
+		async.waterfall(
+			[
+				next => {
+					queueSongModel
+						.find({})
+						.skip(15 * (set - 1))
+						.limit(15)
+						.exec(next);
+				}
+			],
+			async (err, songs) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "QUEUE_SONGS_GET_SET", `Failed to get set from queue songs. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "QUEUE_SONGS_GET_SET", `Got set from queue songs successfully.`);
+				return cb(songs);
+			}
+		);
+	}),
+
+	/**
+	 * Updates a queuesong
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} songId - the id of the queuesong that gets updated
+	 * @param {object} updatedSong - the object of the updated queueSong
+	 * @param {Function} cb - gets called with the result
+	 */
+	update: isAdminRequired(async (session, songId, updatedSong, cb) => {
+		const queueSongModel = await db.runJob("GET_MODEL", {
+			modelName: "queueSong"
+		});
+		async.waterfall(
+			[
+				next => {
+					queueSongModel.findOne({ _id: songId }, next);
+				},
 
-    /**
-     * Gets a set of queue songs
-     *
-     * @param session
-     * @param set - the set number to return
-     * @param cb
-     */
-    getSet: hooks.adminRequired(async (session, set, cb) => {
-        const queueSongModel = await db.runJob("GET_MODEL", {
-            modelName: "queueSong",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    queueSongModel
-                        .find({})
-                        .skip(15 * (set - 1))
-                        .limit(15)
-                        .exec(next);
-                },
-            ],
-            async (err, songs) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "QUEUE_SONGS_GET_SET",
-                        `Failed to get set from queue songs. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "QUEUE_SONGS_GET_SET",
-                    `Got set from queue songs successfully.`
-                );
-                cb(songs);
-            }
-        );
-    }),
+				(song, next) => {
+					if (!song) return next("Song not found");
 
-    /**
-     * Updates a queuesong
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} songId - the id of the queuesong that gets updated
-     * @param {Object} updatedSong - the object of the updated queueSong
-     * @param {Function} cb - gets called with the result
-     */
-    update: hooks.adminRequired(async (session, songId, updatedSong, cb) => {
-        const queueSongModel = await db.runJob("GET_MODEL", {
-            modelName: "queueSong",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    queueSongModel.findOne({ _id: songId }, next);
-                },
+					let updated = false;
 
-                (song, next) => {
-                    if (!song) return next("Song not found");
-                    let updated = false;
-                    let $set = {};
-                    for (let prop in updatedSong)
-                        if (updatedSong[prop] !== song[prop])
-                            $set[prop] = updatedSong[prop];
-                    updated = true;
-                    if (!updated) return next("No properties changed");
-                    queueSongModel.updateOne(
-                        { _id: songId },
-                        { $set },
-                        { runValidators: true },
-                        next
-                    );
-                },
-            ],
-            async (err) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "QUEUE_UPDATE",
-                        `Updating queuesong "${songId}" failed for user ${session.userId}. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                cache.runJob("PUB", { channel: "queue.update", value: songId });
-                console.log(
-                    "SUCCESS",
-                    "QUEUE_UPDATE",
-                    `User "${session.userId}" successfully update queuesong "${songId}".`
-                );
-                return cb({
-                    status: "success",
-                    message: "Successfully updated song.",
-                });
-            }
-        );
-    }),
+					const $set = {};
+					for (let prop = 0, songKeys = Object.keys(updatedSong); prop < songKeys.length; prop += 1) {
+						if (updatedSong[prop] !== song[prop]) $set[prop] = updatedSong[prop];
+					}
+
+					updated = true;
+					if (!updated) return next("No properties changed");
+
+					return queueSongModel.updateOne({ _id: songId }, { $set }, { runValidators: true }, next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"QUEUE_UPDATE",
+						`Updating queuesong "${songId}" failed for user ${session.userId}. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				cache.runJob("PUB", { channel: "queue.update", value: songId });
+				console.log(
+					"SUCCESS",
+					"QUEUE_UPDATE",
+					`User "${session.userId}" successfully update queuesong "${songId}".`
+				);
+				return cb({
+					status: "success",
+					message: "Successfully updated song."
+				});
+			}
+		);
+	}),
 
-    /**
-     * Removes a queuesong
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} songId - the id of the queuesong that gets removed
-     * @param {Function} cb - gets called with the result
-     */
-    remove: hooks.adminRequired(async (session, songId, cb, userId) => {
-        const queueSongModel = await db.runJob("GET_MODEL", {
-            modelName: "queueSong",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    queueSongModel.deleteOne({ _id: songId }, next);
-                },
-            ],
-            async (err) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "QUEUE_REMOVE",
-                        `Removing queuesong "${songId}" failed for user ${session.userId}. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                cache.runJob("PUB", {
-                    channel: "queue.removedSong",
-                    value: songId,
-                });
-                console.log(
-                    "SUCCESS",
-                    "QUEUE_REMOVE",
-                    `User "${session.userId}" successfully removed queuesong "${songId}".`
-                );
-                return cb({
-                    status: "success",
-                    message: "Successfully updated song.",
-                });
-            }
-        );
-    }),
+	/**
+	 * Removes a queuesong
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} songId - the id of the queuesong that gets removed
+	 * @param {Function} cb - gets called with the result
+	 */
+	remove: isAdminRequired(async (session, songId, cb) => {
+		const queueSongModel = await db.runJob("GET_MODEL", {
+			modelName: "queueSong"
+		});
+		async.waterfall(
+			[
+				next => {
+					queueSongModel.deleteOne({ _id: songId }, next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"QUEUE_REMOVE",
+						`Removing queuesong "${songId}" failed for user ${session.userId}. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				cache.runJob("PUB", {
+					channel: "queue.removedSong",
+					value: songId
+				});
+				console.log(
+					"SUCCESS",
+					"QUEUE_REMOVE",
+					`User "${session.userId}" successfully removed queuesong "${songId}".`
+				);
+				return cb({
+					status: "success",
+					message: "Successfully updated song."
+				});
+			}
+		);
+	}),
 
-    /**
-     * Creates a queuesong
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} songId - the id of the song that gets added
-     * @param {Function} cb - gets called with the result
-     */
-    add: hooks.loginRequired(async (session, songId, cb) => {
-        let requestedAt = Date.now();
-        const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        const queueSongModel = await db.runJob("GET_MODEL", {
-            modelName: "queueSong",
-        });
+	/**
+	 * Creates a queuesong
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} songId - the id of the song that gets added
+	 * @param {Function} cb - gets called with the result
+	 */
+	add: isLoginRequired(async (session, songId, cb) => {
+		const requestedAt = Date.now();
+		const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		const QueueSongModel = await db.runJob("GET_MODEL", {
+			modelName: "queueSong"
+		});
 
-        async.waterfall(
-            [
-                (next) => {
-                    queueSongModel.findOne({ songId }, next);
-                },
+		async.waterfall(
+			[
+				next => {
+					QueueSongModel.findOne({ songId }, next);
+				},
 
-                (song, next) => {
-                    if (song) return next("This song is already in the queue.");
-                    songModel.findOne({ songId }, next);
-                },
+				(song, next) => {
+					if (song) return next("This song is already in the queue.");
+					return songModel.findOne({ songId }, next);
+				},
 
-                // Get YouTube data from id
-                (song, next) => {
-                    if (song) return next("This song has already been added.");
-                    //TODO Add err object as first param of callback
-                    utils
-                        .runJob("GET_SONG_FROM_YOUTUBE", { songId })
-                        .then((response) => {
-                            const song = response.song;
-                            song.duration = -1;
-                            song.artists = [];
-                            song.genres = [];
-                            song.skipDuration = 0;
-                            song.thumbnail = `${config.get(
-                                "domain"
-                            )}/assets/notes.png`;
-                            song.explicit = false;
-                            song.requestedBy = session.userId;
-                            song.requestedAt = requestedAt;
-                            next(null, song);
-                        })
-                        .catch(next);
-                },
-                /*(newSong, next) => {
+				// Get YouTube data from id
+				(song, next) => {
+					if (song) return next("This song has already been added.");
+					// TODO Add err object as first param of callback
+					return utils
+						.runJob("GET_SONG_FROM_YOUTUBE", { songId })
+						.then(response => {
+							const { song } = response;
+							song.duration = -1;
+							song.artists = [];
+							song.genres = [];
+							song.skipDuration = 0;
+							song.thumbnail = `${config.get("domain")}/assets/notes.png`;
+							song.explicit = false;
+							song.requestedBy = session.userId;
+							song.requestedAt = requestedAt;
+							next(null, song);
+						})
+						.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) => {
-                        if (err) return next(err);
-                        next(null, song);
-                    });
-                },
-                (newSong, next) => {
-                    userModel.findOne({ _id: session.userId }, (err, user) => {
-                        if (err) next(err, newSong);
-                        else {
-                            user.statistics.songsRequested =
-                                user.statistics.songsRequested + 1;
-                            user.save((err) => {
-                                if (err) return next(err, newSong);
-                                else next(null, newSong);
-                            });
-                        }
-                    });
-                },
-            ],
-            async (err, newSong) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "QUEUE_ADD",
-                        `Adding queuesong "${songId}" failed for user ${session.userId}. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                cache.runJob("PUB", {
-                    channel: "queue.newSong",
-                    value: newSong._id,
-                });
-                console.log(
-                    "SUCCESS",
-                    "QUEUE_ADD",
-                    `User "${session.userId}" successfully added queuesong "${songId}".`
-                );
-                return cb({
-                    status: "success",
-                    message: "Successfully added that song to the queue",
-                });
-            }
-        );
-    }),
+			}, */
+				(newSong, next) => {
+					const song = new QueueSongModel(newSong);
+					song.save({ validateBeforeSave: false }, (err, song) => {
+						if (err) return next(err);
+						return next(null, song);
+					});
+				},
+				(newSong, next) => {
+					userModel.findOne({ _id: session.userId }, (err, user) => {
+						if (err) return next(err, newSong);
 
-    /**
-     * Adds a set of songs to the queue
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} url - the url of the the YouTube playlist
-     * @param {Boolean} musicOnly - whether to only get music from the playlist
-     * @param {Function} cb - gets called with the result
-     */
-    addSetToQueue: hooks.loginRequired((session, url, musicOnly, cb) => {
-        async.waterfall(
-            [
-                (next) => {
-                    utils
-                        .runJob("GET_PLAYLIST_FROM_YOUTUBE", {
-                            url,
-                            musicOnly,
-                        })
-                        .then((res) => {
-                            next(null, res.songs);
-                        })
-                        .catch(next);
-                },
-                (songIds, next) => {
-                    let processed = 0;
-                    function checkDone() {
-                        if (processed === songIds.length) next();
-                    }
-                    songIds.forEach(songId => {
-                        lib.add(session, songId, () => {
-                            processed++;
-                            checkDone();
-                        });
-                    });
-                },
-            ],
-            async (err) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "QUEUE_IMPORT",
-                        `Importing a YouTube playlist to the queue failed for user "${session.userId}". "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "QUEUE_IMPORT",
-                        `Successfully imported a YouTube playlist to the queue for user "${session.userId}".`
-                    );
-                    cb({
-                        status: "success",
-                        message: "Playlist has been successfully imported.",
-                    });
-                }
-            }
-        );
-    }),
+						user.statistics.songsRequested += 1;
+
+						return user.save(err => {
+							if (err) return next(err, newSong);
+							return next(null, newSong);
+						});
+					});
+				}
+			],
+			async (err, newSong) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"QUEUE_ADD",
+						`Adding queuesong "${songId}" failed for user ${session.userId}. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				cache.runJob("PUB", {
+					channel: "queue.newSong",
+					value: newSong._id
+				});
+				console.log(
+					"SUCCESS",
+					"QUEUE_ADD",
+					`User "${session.userId}" successfully added queuesong "${songId}".`
+				);
+				return cb({
+					status: "success",
+					message: "Successfully added that song to the queue"
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Adds a set of songs to the queue
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} url - the url of the the YouTube playlist
+	 * @param {boolean} musicOnly - whether to only get music from the playlist
+	 * @param {Function} cb - gets called with the result
+	 */
+	addSetToQueue: isLoginRequired((session, url, musicOnly, cb) => {
+		async.waterfall(
+			[
+				next => {
+					utils
+						.runJob("GET_PLAYLIST_FROM_YOUTUBE", {
+							url,
+							musicOnly
+						})
+						.then(res => {
+							next(null, res.songs);
+						})
+						.catch(next);
+				},
+				(songIds, next) => {
+					let processed = 0;
+					/**
+					 *
+					 */
+					function checkDone() {
+						if (processed === songIds.length) next();
+					}
+					songIds.forEach(songId => {
+						lib.add(session, songId, () => {
+							processed += 1;
+							checkDone();
+						});
+					});
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"QUEUE_IMPORT",
+						`Importing a YouTube playlist to the queue failed for user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"QUEUE_IMPORT",
+					`Successfully imported a YouTube playlist to the queue for user "${session.userId}".`
+				);
+				return cb({
+					status: "success",
+					message: "Playlist has been successfully imported."
+				});
+			}
+		);
+	})
 };
 
-module.exports = lib;
+export default lib;

+ 281 - 331
backend/logic/actions/reports.js

@@ -1,361 +1,311 @@
-"use strict";
+import async from "async";
 
-const async = require("async");
+import { isAdminRequired, isLoginRequired, isOwnerRequired } from "./hooks";
 
-const hooks = require("./hooks");
-
-const moduleManager = require("../../index");
-
-const db = require("../db");
-const cache = require("../cache");
-const utils = require("../utils");
+import db from "../db";
+import utils from "../utils";
 // const logger = require("../logger");
-const songs = require("../songs");
+import songs from "../songs";
+
+import cache from "../cache";
 
 const reportableIssues = [
-    {
-        name: "Video",
-        reasons: [
-            "Doesn't exist",
-            "It's private",
-            "It's not available in my country",
-        ],
-    },
-    {
-        name: "Title",
-        reasons: ["Incorrect", "Inappropriate"],
-    },
-    {
-        name: "Duration",
-        reasons: [
-            "Skips too soon",
-            "Skips too late",
-            "Starts too soon",
-            "Skips too late",
-        ],
-    },
-    {
-        name: "Artists",
-        reasons: ["Incorrect", "Inappropriate"],
-    },
-    {
-        name: "Thumbnail",
-        reasons: ["Incorrect", "Inappropriate", "Doesn't exist"],
-    },
+	{
+		name: "Video",
+		reasons: ["Doesn't exist", "It's private", "It's not available in my country"]
+	},
+	{
+		name: "Title",
+		reasons: ["Incorrect", "Inappropriate"]
+	},
+	{
+		name: "Duration",
+		reasons: ["Skips too soon", "Skips too late", "Starts too soon", "Skips too late"]
+	},
+	{
+		name: "Artists",
+		reasons: ["Incorrect", "Inappropriate"]
+	},
+	{
+		name: "Thumbnail",
+		reasons: ["Incorrect", "Inappropriate", "Doesn't exist"]
+	}
 ];
 
 cache.runJob("SUB", {
-    channel: "report.resolve",
-    cb: (reportId) => {
-        utils.runJob("EMIT_TO_ROOM", {
-            room: "admin.reports",
-            args: ["event:admin.report.resolved", reportId],
-        });
-    },
+	channel: "report.resolve",
+	cb: reportId => {
+		utils.runJob("EMIT_TO_ROOM", {
+			room: "admin.reports",
+			args: ["event:admin.report.resolved", reportId]
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "report.create",
-    cb: (report) => {
-        utils.runJob("EMIT_TO_ROOM", {
-            room: "admin.reports",
-            args: ["event:admin.report.created", report],
-        });
-    },
+	channel: "report.create",
+	cb: report => {
+		utils.runJob("EMIT_TO_ROOM", {
+			room: "admin.reports",
+			args: ["event:admin.report.created", report]
+		});
+	}
 });
 
-module.exports = {
-    /**
-     * Gets all reports
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {Function} cb - gets called with the result
-     */
-    index: hooks.adminRequired(async (session, cb) => {
-        const reportModel = await db.runJob("GET_MODEL", {
-            modelName: "report",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    reportModel
-                        .find({ resolved: false })
-                        .sort({ released: "desc" })
-                        .exec(next);
-                },
-            ],
-            async (err, reports) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "REPORTS_INDEX",
-                        `Indexing reports failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "REPORTS_INDEX",
-                    "Indexing reports successful."
-                );
-                cb({ status: "success", data: reports });
-            }
-        );
-    }),
+export default {
+	/**
+	 * Gets all reports
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {Function} cb - gets called with the result
+	 */
+	index: isAdminRequired(async (session, cb) => {
+		const reportModel = await db.runJob("GET_MODEL", {
+			modelName: "report"
+		});
+		async.waterfall(
+			[
+				next => {
+					reportModel.find({ resolved: false }).sort({ released: "desc" }).exec(next);
+				}
+			],
+			async (err, reports) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "REPORTS_INDEX", `Indexing reports failed. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "REPORTS_INDEX", "Indexing reports successful.");
+				return cb({ status: "success", data: reports });
+			}
+		);
+	}),
 
-    /**
-     * Gets a specific report
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} reportId - the id of the report to return
-     * @param {Function} cb - gets called with the result
-     */
-    findOne: hooks.adminRequired(async (session, reportId, cb) => {
-        const reportModel = await db.runJob("GET_MODEL", {
-            modelName: "report",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    reportModel.findOne({ _id: reportId }).exec(next);
-                },
-            ],
-            async (err, report) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "REPORTS_FIND_ONE",
-                        `Finding report "${reportId}" failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "REPORTS_FIND_ONE",
-                    `Finding report "${reportId}" successful.`
-                );
-                cb({ status: "success", data: report });
-            }
-        );
-    }),
+	/**
+	 * Gets a specific report
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} reportId - the id of the report to return
+	 * @param {Function} cb - gets called with the result
+	 */
+	findOne: isAdminRequired(async (session, reportId, cb) => {
+		const reportModel = await db.runJob("GET_MODEL", {
+			modelName: "report"
+		});
+		async.waterfall(
+			[
+				next => {
+					reportModel.findOne({ _id: reportId }).exec(next);
+				}
+			],
+			async (err, report) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "REPORTS_FIND_ONE", `Finding report "${reportId}" failed. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "REPORTS_FIND_ONE", `Finding report "${reportId}" successful.`);
+				return cb({ status: "success", data: report });
+			}
+		);
+	}),
 
-    /**
-     * Gets all reports for a songId (_id)
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} songId - the id of the song to index reports for
-     * @param {Function} cb - gets called with the result
-     */
-    getReportsForSong: hooks.adminRequired(async (session, songId, cb) => {
-        const reportModel = await db.runJob("GET_MODEL", {
-            modelName: "report",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    reportModel
-                        .find({ song: { _id: songId }, resolved: false })
-                        .sort({ released: "desc" })
-                        .exec(next);
-                },
+	/**
+	 * Gets all reports for a songId (_id)
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} songId - the id of the song to index reports for
+	 * @param {Function} cb - gets called with the result
+	 */
+	getReportsForSong: isAdminRequired(async (session, songId, cb) => {
+		const reportModel = await db.runJob("GET_MODEL", {
+			modelName: "report"
+		});
+		async.waterfall(
+			[
+				next => {
+					reportModel
+						.find({ song: { _id: songId }, resolved: false })
+						.sort({ released: "desc" })
+						.exec(next);
+				},
 
-                (reports, next) => {
-                    let data = [];
-                    for (let i = 0; i < reports.length; i++) {
-                        data.push(reports[i]._id);
-                    }
-                    next(null, data);
-                },
-            ],
-            async (err, data) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "GET_REPORTS_FOR_SONG",
-                        `Indexing reports for song "${songId}" failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "GET_REPORTS_FOR_SONG",
-                        `Indexing reports for song "${songId}" successful.`
-                    );
-                    return cb({ status: "success", data });
-                }
-            }
-        );
-    }),
+				(reports, next) => {
+					const data = [];
+					for (let i = 0; i < reports.length; i += 1) {
+						data.push(reports[i]._id);
+					}
+					next(null, data);
+				}
+			],
+			async (err, data) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"GET_REPORTS_FOR_SONG",
+						`Indexing reports for song "${songId}" failed. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "GET_REPORTS_FOR_SONG", `Indexing reports for song "${songId}" successful.`);
+				return cb({ status: "success", data });
+			}
+		);
+	}),
 
-    /**
-     * Resolves a report
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} reportId - the id of the report that is getting resolved
-     * @param {Function} cb - gets called with the result
-     */
-    resolve: hooks.adminRequired(async (session, reportId, cb) => {
-        const reportModel = await db.runJob("GET_MODEL", {
-            modelName: "report",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    reportModel.findOne({ _id: reportId }).exec(next);
-                },
+	/**
+	 * Resolves a report
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} reportId - the id of the report that is getting resolved
+	 * @param {Function} cb - gets called with the result
+	 */
+	resolve: isAdminRequired(async (session, reportId, cb) => {
+		const reportModel = await db.runJob("GET_MODEL", {
+			modelName: "report"
+		});
+		async.waterfall(
+			[
+				next => {
+					reportModel.findOne({ _id: reportId }).exec(next);
+				},
 
-                (report, next) => {
-                    if (!report) return next("Report not found.");
-                    report.resolved = true;
-                    report.save((err) => {
-                        if (err) next(err.message);
-                        else next();
-                    });
-                },
-            ],
-            async (err) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "REPORTS_RESOLVE",
-                        `Resolving report "${reportId}" failed by user "${session.userId}". "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                } else {
-                    cache.runJob("PUB", {
-                        channel: "report.resolve",
-                        value: reportId,
-                    });
-                    console.log(
-                        "SUCCESS",
-                        "REPORTS_RESOLVE",
-                        `User "${session.userId}" resolved report "${reportId}".`
-                    );
-                    cb({
-                        status: "success",
-                        message: "Successfully resolved Report",
-                    });
-                }
-            }
-        );
-    }),
+				(report, next) => {
+					if (!report) return next("Report not found.");
+					report.resolved = true;
+					return report.save(err => {
+						if (err) return next(err.message);
+						return next();
+					});
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"REPORTS_RESOLVE",
+						`Resolving report "${reportId}" failed by user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				cache.runJob("PUB", {
+					channel: "report.resolve",
+					value: reportId
+				});
+				console.log("SUCCESS", "REPORTS_RESOLVE", `User "${session.userId}" resolved report "${reportId}".`);
+				return cb({
+					status: "success",
+					message: "Successfully resolved Report"
+				});
+			}
+		);
+	}),
 
-    /**
-     * Creates a new report
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {Object} data - the object of the report data
-     * @param {Function} cb - gets called with the result
-     */
-    create: hooks.loginRequired(async (session, data, cb) => {
-        const reportModel = await db.runJob("GET_MODEL", {
-            modelName: "report",
-        });
-        const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
-        async.waterfall(
-            [
-                (next) => {
-                    songModel.findOne({ songId: data.songId }).exec(next);
-                },
+	/**
+	 * Creates a new report
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} data - the object of the report data
+	 * @param {Function} cb - gets called with the result
+	 */
+	create: isLoginRequired(async (session, data, cb) => {
+		const reportModel = await db.runJob("GET_MODEL", {
+			modelName: "report"
+		});
+		const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
+		async.waterfall(
+			[
+				next => {
+					songModel.findOne({ songId: data.songId }).exec(next);
+				},
 
-                (song, next) => {
-                    if (!song) return next("Song not found.");
-                    songs
-                        .runJob("GET_SONG", { id: song._id })
-                        .then((response) => {
-                            next(null, response.song);
-                        })
-                        .catch(next);
-                },
+				(song, next) => {
+					if (!song) return next("Song not found.");
+					return songs
+						.runJob("GET_SONG", { id: song._id })
+						.then(response => {
+							next(null, response.song);
+						})
+						.catch(next);
+				},
 
-                (song, next) => {
-                    if (!song) return next("Song not found.");
+				(song, next) => {
+					if (!song) return next("Song not found.");
 
-                    delete data.songId;
-                    data.song = {
-                        _id: song._id,
-                        songId: song.songId,
-                    };
+					delete data.songId;
+					data.song = {
+						_id: song._id,
+						songId: song.songId
+					};
 
-                    for (let z = 0; z < data.issues.length; z++) {
-                        if (
-                            reportableIssues.filter((issue) => {
-                                return issue.name == data.issues[z].name;
-                            }).length > 0
-                        ) {
-                            for (let r = 0; r < reportableIssues.length; r++) {
-                                if (
-                                    reportableIssues[r].reasons.every(
-                                        (reason) =>
-                                            data.issues[z].reasons.indexOf(
-                                                reason
-                                            ) < -1
-                                    )
-                                ) {
-                                    return cb({
-                                        status: "failure",
-                                        message: "Invalid data",
-                                    });
-                                }
-                            }
-                        } else
-                            return cb({
-                                status: "failure",
-                                message: "Invalid data",
-                            });
-                    }
+					for (let z = 0; z < data.issues.length; z += 1) {
+						if (reportableIssues.filter(issue => issue.name === data.issues[z].name).length > 0) {
+							for (let r = 0; r < reportableIssues.length; r += 1) {
+								if (
+									reportableIssues[r].reasons.every(
+										reason => data.issues[z].reasons.indexOf(reason) < -1
+									)
+								) {
+									return cb({
+										status: "failure",
+										message: "Invalid data"
+									});
+								}
+							}
+						} else
+							return cb({
+								status: "failure",
+								message: "Invalid data"
+							});
+					}
 
-                    next();
-                },
+					return next();
+				},
 
-                (next) => {
-                    let issues = [];
+				next => {
+					const issues = [];
 
-                    for (let r = 0; r < data.issues.length; r++) {
-                        if (!data.issues[r].reasons.length <= 0)
-                            issues.push(data.issues[r]);
-                    }
+					for (let r = 0; r < data.issues.length; r += 1) {
+						if (!data.issues[r].reasons.length <= 0) issues.push(data.issues[r]);
+					}
 
-                    data.issues = issues;
+					data.issues = issues;
 
-                    next();
-                },
+					next();
+				},
 
-                (next) => {
-                    data.createdBy = session.userId;
-                    data.createdAt = Date.now();
-                    reportModel.create(data, next);
-                },
-            ],
-            async (err, report) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "REPORTS_CREATE",
-                        `Creating report for "${data.song._id}" failed by user "${session.userId}". "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                } else {
-                    cache.runJob("PUB", {
-                        channel: "report.create",
-                        value: report,
-                    });
-                    console.log(
-                        "SUCCESS",
-                        "REPORTS_CREATE",
-                        `User "${session.userId}" created report for "${data.songId}".`
-                    );
-                    return cb({
-                        status: "success",
-                        message: "Successfully created report",
-                    });
-                }
-            }
-        );
-    }),
+				next => {
+					data.createdBy = session.userId;
+					data.createdAt = Date.now();
+					reportModel.create(data, next);
+				}
+			],
+			async (err, report) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"REPORTS_CREATE",
+						`Creating report for "${data.song._id}" failed by user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				cache.runJob("PUB", {
+					channel: "report.create",
+					value: report
+				});
+				console.log(
+					"SUCCESS",
+					"REPORTS_CREATE",
+					`User "${session.userId}" created report for "${data.songId}".`
+				);
+				return cb({
+					status: "success",
+					message: "Successfully created report"
+				});
+			}
+		);
+	})
 };

+ 915 - 1027
backend/logic/actions/songs.js

@@ -1,1055 +1,943 @@
-"use strict";
+import async from "async";
 
-const async = require("async");
-
-const hooks = require("./hooks");
-const queueSongs = require("./queueSongs");
+import { isAdminRequired, isLoginRequired } from "./hooks";
 
 // const moduleManager = require("../../index");
 
-const db = require("../db");
-const songs = require("../songs");
-const cache = require("../cache");
-const utils = require("../utils");
-const activities = require("../activities");
+import db from "../db";
+import utils from "../utils";
+import cache from "../cache";
+
+import songs from "../songs";
+import queueSongs from "./queueSongs";
+import activities from "../activities";
 // const logger = moduleManager.modules["logger"];
 
 cache.runJob("SUB", {
-    channel: "song.removed",
-    cb: (songId) => {
-        utils.runJob("EMIT_TO_ROOM", {
-            room: "admin.songs",
-            args: ["event:admin.song.removed", songId],
-        });
-    },
+	channel: "song.removed",
+	cb: songId => {
+		utils.runJob("EMIT_TO_ROOM", {
+			room: "admin.songs",
+			args: ["event:admin.song.removed", songId]
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "song.added",
-    cb: async (songId) => {
-        const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
-        songModel.findOne({ _id: songId }, (err, song) => {
-            utils.runJob("EMIT_TO_ROOM", {
-                room: "admin.songs",
-                args: ["event:admin.song.added", song],
-            });
-        });
-    },
+	channel: "song.added",
+	cb: async songId => {
+		const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
+		songModel.findOne({ _id: songId }, (err, song) => {
+			utils.runJob("EMIT_TO_ROOM", {
+				room: "admin.songs",
+				args: ["event:admin.song.added", song]
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "song.updated",
-    cb: async (songId) => {
-        const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
-        songModel.findOne({ _id: songId }, (err, song) => {
-            utils.runJob("EMIT_TO_ROOM", {
-                room: "admin.songs",
-                args: ["event:admin.song.updated", song],
-            });
-        });
-    },
+	channel: "song.updated",
+	cb: async songId => {
+		const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
+		songModel.findOne({ _id: songId }, (err, song) => {
+			utils.runJob("EMIT_TO_ROOM", {
+				room: "admin.songs",
+				args: ["event:admin.song.updated", song]
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "song.like",
-    cb: (data) => {
-        utils.runJob("EMIT_TO_ROOM", {
-            room: `song.${data.songId}`,
-            args: [
-                "event:song.like",
-                {
-                    songId: data.songId,
-                    likes: data.likes,
-                    dislikes: data.dislikes,
-                },
-            ],
-        });
-        utils
-            .runJob("SOCKETS_FROM_USER", { userId: data.userId })
-            .then((response) => {
-                response.sockets.forEach((socket) => {
-                    socket.emit("event:song.newRatings", {
-                        songId: data.songId,
-                        liked: true,
-                        disliked: false,
-                    });
-                });
-            });
-    },
+	channel: "song.like",
+	cb: data => {
+		utils.runJob("EMIT_TO_ROOM", {
+			room: `song.${data.songId}`,
+			args: [
+				"event:song.like",
+				{
+					songId: data.songId,
+					likes: data.likes,
+					dislikes: data.dislikes
+				}
+			]
+		});
+		utils.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("event:song.newRatings", {
+					songId: data.songId,
+					liked: true,
+					disliked: false
+				});
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "song.dislike",
-    cb: (data) => {
-        utils.runJob("EMIT_TO_ROOM", {
-            room: `song.${data.songId}`,
-            args: [
-                "event:song.dislike",
-                {
-                    songId: data.songId,
-                    likes: data.likes,
-                    dislikes: data.dislikes,
-                },
-            ],
-        });
-        utils
-            .runJob("SOCKETS_FROM_USER", { userId: data.userId })
-            .then((response) => {
-                response.sockets.forEach((socket) => {
-                    socket.emit("event:song.newRatings", {
-                        songId: data.songId,
-                        liked: false,
-                        disliked: true,
-                    });
-                });
-            });
-    },
+	channel: "song.dislike",
+	cb: data => {
+		utils.runJob("EMIT_TO_ROOM", {
+			room: `song.${data.songId}`,
+			args: [
+				"event:song.dislike",
+				{
+					songId: data.songId,
+					likes: data.likes,
+					dislikes: data.dislikes
+				}
+			]
+		});
+		utils.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("event:song.newRatings", {
+					songId: data.songId,
+					liked: false,
+					disliked: true
+				});
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "song.unlike",
-    cb: (data) => {
-        utils.runJob("EMIT_TO_ROOM", {
-            room: `song.${data.songId}`,
-            args: [
-                "event:song.unlike",
-                {
-                    songId: data.songId,
-                    likes: data.likes,
-                    dislikes: data.dislikes,
-                },
-            ],
-        });
-        utils
-            .runJob("SOCKETS_FROM_USER", { userId: data.userId })
-            .then((response) => {
-                response.sockets.forEach((socket) => {
-                    socket.emit("event:song.newRatings", {
-                        songId: data.songId,
-                        liked: false,
-                        disliked: false,
-                    });
-                });
-            });
-    },
+	channel: "song.unlike",
+	cb: data => {
+		utils.runJob("EMIT_TO_ROOM", {
+			room: `song.${data.songId}`,
+			args: [
+				"event:song.unlike",
+				{
+					songId: data.songId,
+					likes: data.likes,
+					dislikes: data.dislikes
+				}
+			]
+		});
+		utils.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("event:song.newRatings", {
+					songId: data.songId,
+					liked: false,
+					disliked: false
+				});
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "song.undislike",
-    cb: (data) => {
-        utils.runJob("EMIT_TO_ROOM", {
-            room: `song.${data.songId}`,
-            args: [
-                "event:song.undislike",
-                {
-                    songId: data.songId,
-                    likes: data.likes,
-                    dislikes: data.dislikes,
-                },
-            ],
-        });
-        utils
-            .runJob("SOCKETS_FROM_USER", { userId: data.userId })
-            .then((response) => {
-                response.sockets.forEach((socket) => {
-                    socket.emit("event:song.newRatings", {
-                        songId: data.songId,
-                        liked: false,
-                        disliked: false,
-                    });
-                });
-            });
-    },
+	channel: "song.undislike",
+	cb: data => {
+		utils.runJob("EMIT_TO_ROOM", {
+			room: `song.${data.songId}`,
+			args: [
+				"event:song.undislike",
+				{
+					songId: data.songId,
+					likes: data.likes,
+					dislikes: data.dislikes
+				}
+			]
+		});
+		utils.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("event:song.newRatings", {
+					songId: data.songId,
+					liked: false,
+					disliked: false
+				});
+			});
+		});
+	}
 });
 
-module.exports = {
-    /**
-     * Returns the length of the songs list
-     *
-     * @param session
-     * @param cb
-     */
-    length: hooks.adminRequired(async (session, cb) => {
-        const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
-        async.waterfall(
-            [
-                (next) => {
-                    songModel.countDocuments({}, next);
-                },
-            ],
-            async (err, count) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "SONGS_LENGTH",
-                        `Failed to get length from songs. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "SONGS_LENGTH",
-                    `Got length from songs successfully.`
-                );
-                cb(count);
-            }
-        );
-    }),
-
-    /**
-     * Gets a set of songs
-     *
-     * @param session
-     * @param set - the set number to return
-     * @param cb
-     */
-    getSet: hooks.adminRequired(async (session, set, cb) => {
-        const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
-        async.waterfall(
-            [
-                (next) => {
-                    songModel
-                        .find({})
-                        .skip(15 * (set - 1))
-                        .limit(15)
-                        .exec(next);
-                },
-            ],
-            async (err, songs) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "SONGS_GET_SET",
-                        `Failed to get set from songs. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "SONGS_GET_SET",
-                    `Got set from songs successfully.`
-                );
-                cb(songs);
-            }
-        );
-    }),
-
-    /**
-     * Gets a song
-     *
-     * @param session
-     * @param songId - the song id
-     * @param cb
-     */
-    getSong: hooks.adminRequired((session, songId, cb) => {
-        async.waterfall(
-            [
-                (next) => {
-                    songs
-                        .runJob("GET_SONG_FROM_ID", { songId: songId })
-                        .then(song => {
-                            next(null, song);
-                        })
-                        .catch(err => {
-                            next(err);
-                        });
-                },
-            ],
-            async (err, song) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "SONGS_GET_SONG",
-                        `Failed to get song ${songId}. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "SONGS_GET_SONG",
-                        `Got song ${songId} successfully.`
-                    );
-                    cb({ status: "success", data: song });
-                }
-            }
-        );
-    }),
-
-    /**
-     * Obtains basic metadata of a song in order to format an activity
-     *
-     * @param session
-     * @param songId - the song id
-     * @param cb
-     */
-    getSongForActivity: (session, songId, cb) => {
-        async.waterfall(
-            [
-                (next) => {
-                    songs
-                        .runJob("GET_SONG_FROM_ID", { songId })
-                        .then((responsesong) => {
-                            next(null, response.song);
-                        })
-                        .catch(next);
-                },
-            ],
-            async (err, song) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "SONGS_GET_SONG_FOR_ACTIVITY",
-                        `Failed to obtain metadata of song ${songId} for activity formatting. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                } else {
-                    if (song) {
-                        console.log(
-                            "SUCCESS",
-                            "SONGS_GET_SONG_FOR_ACTIVITY",
-                            `Obtained metadata of song ${songId} for activity formatting successfully.`
-                        );
-                        cb({
-                            status: "success",
-                            data: {
-                                title: song.title,
-                                thumbnail: song.thumbnail,
-                            },
-                        });
-                    } else {
-                        console.log(
-                            "ERROR",
-                            "SONGS_GET_SONG_FOR_ACTIVITY",
-                            `Song ${songId} does not exist so failed to obtain for activity formatting.`
-                        );
-                        cb({ status: "failure" });
-                    }
-                }
-            }
-        );
-    },
-
-    /**
-     * Updates a song
-     *
-     * @param session
-     * @param songId - the song id
-     * @param song - the updated song object
-     * @param cb
-     */
-    update: hooks.adminRequired(async (session, songId, song, cb) => {
-        const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
-        async.waterfall(
-            [
-                (next) => {
-                    songModel.updateOne(
-                        { _id: songId },
-                        song,
-                        { runValidators: true },
-                        next
-                    );
-                },
-
-                (res, next) => {
-                    songs
-                        .runJob("UPDATE_SONG", { songId })
-                        .then((song) => {
-                            next(null, song);
-                        })
-                        .catch(next);
-                },
-            ],
-            async (err, song) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "SONGS_UPDATE",
-                        `Failed to update song "${songId}". "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "SONGS_UPDATE",
-                    `Successfully updated song "${songId}".`
-                );
-                cache.runJob("PUB", {
-                    channel: "song.updated",
-                    value: song.songId,
-                });
-                cb({
-                    status: "success",
-                    message: "Song has been successfully updated",
-                    data: song,
-                });
-            }
-        );
-    }),
-
-    /**
-     * Removes a song
-     *
-     * @param session
-     * @param songId - the song id
-     * @param cb
-     */
-    remove: hooks.adminRequired(async (session, songId, cb) => {
-        const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
-        async.waterfall(
-            [
-                (next) => {
-                    songModel.deleteOne({ _id: songId }, next);
-                },
-
-                (res, next) => {
-                    //TODO Check if res gets returned from above
-                    cache
-                        .runJob("HDEL", { table: "songs", key: songId })
-                        .then(() => {
-                            next();
-                        })
-                        .catch(next);
-                },
-            ],
-            async (err) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "SONGS_UPDATE",
-                        `Failed to remove song "${songId}". "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "SONGS_UPDATE",
-                    `Successfully remove song "${songId}".`
-                );
-                cache.runJob("PUB", { channel: "song.removed", value: songId });
-                cb({
-                    status: "success",
-                    message: "Song has been successfully updated",
-                });
-            }
-        );
-    }),
-
-    /**
-     * Adds a song
-     *
-     * @param session
-     * @param song - the song object
-     * @param cb
-     */
-    add: hooks.adminRequired(async (session, song, cb) => {
-        const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
-        async.waterfall(
-            [
-                (next) => {
-                    songModel.findOne({ songId: song.songId }, next);
-                },
-
-                (existingSong, next) => {
-                    if (existingSong)
-                        return next("Song is already in rotation.");
-                    next();
-                },
-
-                (next) => {
-                    const newSong = new songModel(song);
-                    newSong.acceptedBy = session.userId;
-                    newSong.acceptedAt = Date.now();
-                    newSong.save(next);
-                },
-
-                (res, next) => {
-                    queueSongs.remove(session, song._id, () => {
-                        next();
-                    });
-                },
-            ],
-            async (err) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "SONGS_ADD",
-                        `User "${session.userId}" failed to add song. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "SONGS_ADD",
-                    `User "${session.userId}" successfully added song "${song.songId}".`
-                );
-                cache.runJob("PUB", {
-                    channel: "song.added",
-                    value: song.songId,
-                });
-                cb({
-                    status: "success",
-                    message: "Song has been moved from the queue successfully.",
-                });
-            }
-        );
-        //TODO Check if video is in queue and Add the song to the appropriate stations
-    }),
-
-    /**
-     * Likes a song
-     *
-     * @param session
-     * @param songId - the song id
-     * @param cb
-     */
-    like: hooks.loginRequired(async (session, songId, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
-        async.waterfall(
-            [
-                (next) => {
-                    songModel.findOne({ songId }, next);
-                },
-
-                (song, next) => {
-                    if (!song) return next("No song found with that id.");
-                    next(null, song);
-                },
-            ],
-            async (err, song) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "SONGS_LIKE",
-                        `User "${session.userId}" failed to like song ${songId}. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                let oldSongId = songId;
-                songId = song._id;
-                userModel.findOne({ _id: session.userId }, (err, user) => {
-                    if (user.liked.indexOf(songId) !== -1)
-                        return cb({
-                            status: "failure",
-                            message: "You have already liked this song.",
-                        });
-                    userModel.updateOne(
-                        { _id: session.userId },
-                        {
-                            $push: { liked: songId },
-                            $pull: { disliked: songId },
-                        },
-                        (err) => {
-                            if (!err) {
-                                userModel.countDocuments(
-                                    { liked: songId },
-                                    (err, likes) => {
-                                        if (err)
-                                            return cb({
-                                                status: "failure",
-                                                message:
-                                                    "Something went wrong while liking this song.",
-                                            });
-                                        userModel.countDocuments(
-                                            { disliked: songId },
-                                            (err, dislikes) => {
-                                                if (err)
-                                                    return cb({
-                                                        status: "failure",
-                                                        message:
-                                                            "Something went wrong while liking this song.",
-                                                    });
-                                                songModel.update(
-                                                    { _id: songId },
-                                                    {
-                                                        $set: {
-                                                            likes: likes,
-                                                            dislikes: dislikes,
-                                                        },
-                                                    },
-                                                    (err) => {
-                                                        if (err)
-                                                            return cb({
-                                                                status:
-                                                                    "failure",
-                                                                message:
-                                                                    "Something went wrong while liking this song.",
-                                                            });
-                                                        songs.runJob(
-                                                            "UPDATE_SONG",
-                                                            { songId }
-                                                        );
-                                                        cache.runJob("PUB", {
-                                                            channel:
-                                                                "song.like",
-                                                            value: JSON.stringify(
-                                                                {
-                                                                    songId: oldSongId,
-                                                                    userId:
-                                                                        session.userId,
-                                                                    likes: likes,
-                                                                    dislikes: dislikes,
-                                                                }
-                                                            ),
-                                                        });
-                                                        activities.runJob(
-                                                            "ADD_ACTIVITY",
-                                                            {
-                                                                userId:
-                                                                    session.userId,
-                                                                activityType:
-                                                                    "liked_song",
-                                                                payload: [
-                                                                    songId,
-                                                                ],
-                                                            }
-                                                        );
-                                                        return cb({
-                                                            status: "success",
-                                                            message:
-                                                                "You have successfully liked this song.",
-                                                        });
-                                                    }
-                                                );
-                                            }
-                                        );
-                                    }
-                                );
-                            } else
-                                return cb({
-                                    status: "failure",
-                                    message:
-                                        "Something went wrong while liking this song.",
-                                });
-                        }
-                    );
-                });
-            }
-        );
-    }),
-
-    /**
-     * Dislikes a song
-     *
-     * @param session
-     * @param songId - the song id
-     * @param cb
-     */
-    dislike: hooks.loginRequired(async (session, songId, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
-        async.waterfall(
-            [
-                (next) => {
-                    songModel.findOne({ songId }, next);
-                },
-
-                (song, next) => {
-                    if (!song) return next("No song found with that id.");
-                    next(null, song);
-                },
-            ],
-            async (err, song) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "SONGS_DISLIKE",
-                        `User "${session.userId}" failed to like song ${songId}. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                let oldSongId = songId;
-                songId = song._id;
-                userModel.findOne({ _id: session.userId }, (err, user) => {
-                    if (user.disliked.indexOf(songId) !== -1)
-                        return cb({
-                            status: "failure",
-                            message: "You have already disliked this song.",
-                        });
-                    userModel.updateOne(
-                        { _id: session.userId },
-                        {
-                            $push: { disliked: songId },
-                            $pull: { liked: songId },
-                        },
-                        (err) => {
-                            if (!err) {
-                                userModel.countDocuments(
-                                    { liked: songId },
-                                    (err, likes) => {
-                                        if (err)
-                                            return cb({
-                                                status: "failure",
-                                                message:
-                                                    "Something went wrong while disliking this song.",
-                                            });
-                                        userModel.countDocuments(
-                                            { disliked: songId },
-                                            (err, dislikes) => {
-                                                if (err)
-                                                    return cb({
-                                                        status: "failure",
-                                                        message:
-                                                            "Something went wrong while disliking this song.",
-                                                    });
-                                                songModel.update(
-                                                    { _id: songId },
-                                                    {
-                                                        $set: {
-                                                            likes: likes,
-                                                            dislikes: dislikes,
-                                                        },
-                                                    },
-                                                    (err, res) => {
-                                                        if (err)
-                                                            return cb({
-                                                                status:
-                                                                    "failure",
-                                                                message:
-                                                                    "Something went wrong while disliking this song.",
-                                                            });
-                                                        songs.runJob(
-                                                            "UPDATE_SONG",
-                                                            { songId }
-                                                        );
-                                                        cache.runJob("PUB", {
-                                                            channel:
-                                                                "song.dislike",
-                                                            value: JSON.stringify(
-                                                                {
-                                                                    songId: oldSongId,
-                                                                    userId:
-                                                                        session.userId,
-                                                                    likes: likes,
-                                                                    dislikes: dislikes,
-                                                                }
-                                                            ),
-                                                        });
-                                                        return cb({
-                                                            status: "success",
-                                                            message:
-                                                                "You have successfully disliked this song.",
-                                                        });
-                                                    }
-                                                );
-                                            }
-                                        );
-                                    }
-                                );
-                            } else
-                                return cb({
-                                    status: "failure",
-                                    message:
-                                        "Something went wrong while disliking this song.",
-                                });
-                        }
-                    );
-                });
-            }
-        );
-    }),
-
-    /**
-     * Undislikes a song
-     *
-     * @param session
-     * @param songId - the song id
-     * @param cb
-     */
-    undislike: hooks.loginRequired(async (session, songId, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
-        async.waterfall(
-            [
-                (next) => {
-                    songModel.findOne({ songId }, next);
-                },
-
-                (song, next) => {
-                    if (!song) return next("No song found with that id.");
-                    next(null, song);
-                },
-            ],
-            async (err, song) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "SONGS_UNDISLIKE",
-                        `User "${session.userId}" failed to like song ${songId}. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                let oldSongId = songId;
-                songId = song._id;
-                userModel.findOne({ _id: session.userId }, (err, user) => {
-                    if (user.disliked.indexOf(songId) === -1)
-                        return cb({
-                            status: "failure",
-                            message: "You have not disliked this song.",
-                        });
-                    userModel.updateOne(
-                        { _id: session.userId },
-                        { $pull: { liked: songId, disliked: songId } },
-                        (err) => {
-                            if (!err) {
-                                userModel.countDocuments(
-                                    { liked: songId },
-                                    (err, likes) => {
-                                        if (err)
-                                            return cb({
-                                                status: "failure",
-                                                message:
-                                                    "Something went wrong while undisliking this song.",
-                                            });
-                                        userModel.countDocuments(
-                                            { disliked: songId },
-                                            (err, dislikes) => {
-                                                if (err)
-                                                    return cb({
-                                                        status: "failure",
-                                                        message:
-                                                            "Something went wrong while undisliking this song.",
-                                                    });
-                                                songModel.update(
-                                                    { _id: songId },
-                                                    {
-                                                        $set: {
-                                                            likes: likes,
-                                                            dislikes: dislikes,
-                                                        },
-                                                    },
-                                                    (err) => {
-                                                        if (err)
-                                                            return cb({
-                                                                status:
-                                                                    "failure",
-                                                                message:
-                                                                    "Something went wrong while undisliking this song.",
-                                                            });
-                                                        songs.runJob(
-                                                            "UPDATE_SONG",
-                                                            { songId }
-                                                        );
-                                                        cache.runJob("PUB", {
-                                                            channel:
-                                                                "song.undislike",
-                                                            value: JSON.stringify(
-                                                                {
-                                                                    songId: oldSongId,
-                                                                    userId:
-                                                                        session.userId,
-                                                                    likes: likes,
-                                                                    dislikes: dislikes,
-                                                                }
-                                                            ),
-                                                        });
-                                                        return cb({
-                                                            status: "success",
-                                                            message:
-                                                                "You have successfully undisliked this song.",
-                                                        });
-                                                    }
-                                                );
-                                            }
-                                        );
-                                    }
-                                );
-                            } else
-                                return cb({
-                                    status: "failure",
-                                    message:
-                                        "Something went wrong while undisliking this song.",
-                                });
-                        }
-                    );
-                });
-            }
-        );
-    }),
-
-    /**
-     * Unlikes a song
-     *
-     * @param session
-     * @param songId - the song id
-     * @param cb
-     */
-    unlike: hooks.loginRequired(async (session, songId, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
-        async.waterfall(
-            [
-                (next) => {
-                    songModel.findOne({ songId }, next);
-                },
-
-                (song, next) => {
-                    if (!song) return next("No song found with that id.");
-                    next(null, song);
-                },
-            ],
-            async (err, song) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "SONGS_UNLIKE",
-                        `User "${session.userId}" failed to like song ${songId}. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                let oldSongId = songId;
-                songId = song._id;
-                userModel.findOne({ _id: session.userId }, (err, user) => {
-                    if (user.liked.indexOf(songId) === -1)
-                        return cb({
-                            status: "failure",
-                            message: "You have not liked this song.",
-                        });
-                    userModel.updateOne(
-                        { _id: session.userId },
-                        { $pull: { liked: songId, disliked: songId } },
-                        (err) => {
-                            if (!err) {
-                                userModel.countDocuments(
-                                    { liked: songId },
-                                    (err, likes) => {
-                                        if (err)
-                                            return cb({
-                                                status: "failure",
-                                                message:
-                                                    "Something went wrong while unliking this song.",
-                                            });
-                                        userModel.countDocuments(
-                                            { disliked: songId },
-                                            (err, dislikes) => {
-                                                if (err)
-                                                    return cb({
-                                                        status: "failure",
-                                                        message:
-                                                            "Something went wrong while undiking this song.",
-                                                    });
-                                                songModel.updateOne(
-                                                    { _id: songId },
-                                                    {
-                                                        $set: {
-                                                            likes: likes,
-                                                            dislikes: dislikes,
-                                                        },
-                                                    },
-                                                    (err) => {
-                                                        if (err)
-                                                            return cb({
-                                                                status:
-                                                                    "failure",
-                                                                message:
-                                                                    "Something went wrong while unliking this song.",
-                                                            });
-                                                        songs.runJob(
-                                                            "UPDATE_SONG",
-                                                            { songId }
-                                                        );
-                                                        cache.runJob("PUB", {
-                                                            channel:
-                                                                "song.unlike",
-                                                            value: JSON.stringify(
-                                                                {
-                                                                    songId: oldSongId,
-                                                                    userId:
-                                                                        session.userId,
-                                                                    likes: likes,
-                                                                    dislikes: dislikes,
-                                                                }
-                                                            ),
-                                                        });
-                                                        return cb({
-                                                            status: "success",
-                                                            message:
-                                                                "You have successfully unliked this song.",
-                                                        });
-                                                    }
-                                                );
-                                            }
-                                        );
-                                    }
-                                );
-                            } else
-                                return cb({
-                                    status: "failure",
-                                    message:
-                                        "Something went wrong while unliking this song.",
-                                });
-                        }
-                    );
-                });
-            }
-        );
-    }),
-
-    /**
-     * Gets user's own song ratings
-     *
-     * @param session
-     * @param songId - the song id
-     * @param cb
-     */
-    getOwnSongRatings: hooks.loginRequired(async (session, songId, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
-        async.waterfall(
-            [
-                (next) => {
-                    songModel.findOne({ songId }, next);
-                },
-
-                (song, next) => {
-                    if (!song) return next("No song found with that id.");
-                    next(null, song);
-                },
-            ],
-            async (err, song) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "SONGS_GET_OWN_RATINGS",
-                        `User "${session.userId}" failed to get ratings for ${songId}. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                let newSongId = song._id;
-                userModel.findOne(
-                    { _id: session.userId },
-                    async (err, user) => {
-                        if (!err && user) {
-                            return cb({
-                                status: "success",
-                                songId: songId,
-                                liked: user.liked.indexOf(newSongId) !== -1,
-                                disliked:
-                                    user.disliked.indexOf(newSongId) !== -1,
-                            });
-                        } else {
-                            return cb({
-                                status: "failure",
-                                message: await utils.runJob("GET_ERROR", {
-                                    error: err,
-                                }),
-                            });
-                        }
-                    }
-                );
-            }
-        );
-    }),
+export default {
+	/**
+	 * Returns the length of the songs list
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param cb
+	 */
+	length: isAdminRequired(async (session, cb) => {
+		const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
+		async.waterfall(
+			[
+				next => {
+					songModel.countDocuments({}, next);
+				}
+			],
+			async (err, count) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "SONGS_LENGTH", `Failed to get length from songs. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "SONGS_LENGTH", `Got length from songs successfully.`);
+				return cb(count);
+			}
+		);
+	}),
+
+	/**
+	 * Gets a set of songs
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param set - the set number to return
+	 * @param cb
+	 */
+	getSet: isAdminRequired(async (session, set, cb) => {
+		const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
+		async.waterfall(
+			[
+				next => {
+					songModel
+						.find({})
+						.skip(15 * (set - 1))
+						.limit(15)
+						.exec(next);
+				}
+			],
+			async (err, songs) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "SONGS_GET_SET", `Failed to get set from songs. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "SONGS_GET_SET", `Got set from songs successfully.`);
+				return cb(songs);
+			}
+		);
+	}),
+
+	/**
+	 * Gets a song
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} songId - the song id
+	 * @param {Function} cb
+	 */
+	getSong: isAdminRequired((session, songId, cb) => {
+		async.waterfall(
+			[
+				next => {
+					songs
+						.runJob("GET_SONG_FROM_ID", { songId })
+						.then(song => {
+							next(null, song);
+						})
+						.catch(err => {
+							next(err);
+						});
+				}
+			],
+			async (err, song) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "SONGS_GET_SONG", `Failed to get song ${songId}. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "SONGS_GET_SONG", `Got song ${songId} successfully.`);
+				return cb({ status: "success", data: song });
+			}
+		);
+	}),
+
+	/**
+	 * Obtains basic metadata of a song in order to format an activity
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} songId - the song id
+	 * @param {Function} cb - callback
+	 */
+	getSongForActivity: (session, songId, cb) => {
+		async.waterfall(
+			[
+				next => {
+					songs
+						.runJob("GET_SONG_FROM_ID", { songId })
+						.then(response => next(null, response.song))
+						.catch(next);
+				}
+			],
+			async (err, song) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+
+					console.log(
+						"ERROR",
+						"SONGS_GET_SONG_FOR_ACTIVITY",
+						`Failed to obtain metadata of song ${songId} for activity formatting. "${err}"`
+					);
+
+					return cb({ status: "failure", message: err });
+				}
+
+				if (song) {
+					console.log(
+						"SUCCESS",
+						"SONGS_GET_SONG_FOR_ACTIVITY",
+						`Obtained metadata of song ${songId} for activity formatting successfully.`
+					);
+
+					return cb({
+						status: "success",
+						data: {
+							title: song.title,
+							thumbnail: song.thumbnail
+						}
+					});
+				}
+
+				console.log(
+					"ERROR",
+					"SONGS_GET_SONG_FOR_ACTIVITY",
+					`Song ${songId} does not exist so failed to obtain for activity formatting.`
+				);
+
+				return cb({ status: "failure" });
+			}
+		);
+	},
+
+	/**
+	 * Updates a song
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} songId - the song id
+	 * @param {object} song - the updated song object
+	 * @param {Function} cb
+	 */
+	update: isAdminRequired(async (session, songId, song, cb) => {
+		const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
+		async.waterfall(
+			[
+				next => {
+					songModel.updateOne({ _id: songId }, song, { runValidators: true }, next);
+				},
+
+				(res, next) => {
+					songs
+						.runJob("UPDATE_SONG", { songId })
+						.then(song => {
+							next(null, song);
+						})
+						.catch(next);
+				}
+			],
+			async (err, song) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+
+					console.log("ERROR", "SONGS_UPDATE", `Failed to update song "${songId}". "${err}"`);
+
+					return cb({ status: "failure", message: err });
+				}
+
+				console.log("SUCCESS", "SONGS_UPDATE", `Successfully updated song "${songId}".`);
+
+				cache.runJob("PUB", {
+					channel: "song.updated",
+					value: song.songId
+				});
+
+				return cb({
+					status: "success",
+					message: "Song has been successfully updated",
+					data: song
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Removes a song
+	 *
+	 * @param session
+	 * @param songId - the song id
+	 * @param cb
+	 */
+	remove: isAdminRequired(async (session, songId, cb) => {
+		const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
+		async.waterfall(
+			[
+				next => {
+					songModel.deleteOne({ _id: songId }, next);
+				},
+
+				(res, next) => {
+					// TODO Check if res gets returned from above
+					cache
+						.runJob("HDEL", { table: "songs", key: songId })
+						.then(() => {
+							next();
+						})
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+
+					console.log("ERROR", "SONGS_UPDATE", `Failed to remove song "${songId}". "${err}"`);
+
+					return cb({ status: "failure", message: err });
+				}
+
+				console.log("SUCCESS", "SONGS_UPDATE", `Successfully remove song "${songId}".`);
+
+				cache.runJob("PUB", { channel: "song.removed", value: songId });
+
+				return cb({
+					status: "success",
+					message: "Song has been successfully updated"
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Adds a song
+	 *
+	 * @param session
+	 * @param song - the song object
+	 * @param cb
+	 */
+	add: isAdminRequired(async (session, song, cb) => {
+		const SongModel = await db.runJob("GET_MODEL", { modelName: "song" });
+		async.waterfall(
+			[
+				next => {
+					SongModel.findOne({ songId: song.songId }, next);
+				},
+
+				(existingSong, next) => {
+					if (existingSong) return next("Song is already in rotation.");
+					return next();
+				},
+
+				next => {
+					const newSong = new SongModel(song);
+					newSong.acceptedBy = session.userId;
+					newSong.acceptedAt = Date.now();
+					newSong.save(next);
+				},
+
+				(res, next) => {
+					queueSongs.remove(session, song._id, () => {
+						next();
+					});
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+
+					console.log("ERROR", "SONGS_ADD", `User "${session.userId}" failed to add song. "${err}"`);
+
+					return cb({ status: "failure", message: err });
+				}
+
+				console.log(
+					"SUCCESS",
+					"SONGS_ADD",
+					`User "${session.userId}" successfully added song "${song.songId}".`
+				);
+
+				cache.runJob("PUB", {
+					channel: "song.added",
+					value: song.songId
+				});
+
+				return cb({
+					status: "success",
+					message: "Song has been moved from the queue successfully."
+				});
+			}
+		);
+		// TODO Check if video is in queue and Add the song to the appropriate stations
+	}),
+
+	/**
+	 * Likes a song
+	 *
+	 * @param session
+	 * @param songId - the song id
+	 * @param cb
+	 */
+	like: isLoginRequired(async (session, songId, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
+		async.waterfall(
+			[
+				next => {
+					songModel.findOne({ songId }, next);
+				},
+
+				(song, next) => {
+					if (!song) return next("No song found with that id.");
+					return next(null, song);
+				}
+			],
+			async (err, song) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"SONGS_LIKE",
+						`User "${session.userId}" failed to like song ${songId}. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+
+				const oldSongId = songId;
+				songId = song._id;
+
+				return userModel.findOne({ _id: session.userId }, (err, user) => {
+					if (user.liked.indexOf(songId) !== -1)
+						return cb({
+							status: "failure",
+							message: "You have already liked this song."
+						});
+
+					return userModel.updateOne(
+						{ _id: session.userId },
+						{
+							$push: { liked: songId },
+							$pull: { disliked: songId }
+						},
+						err => {
+							if (!err) {
+								return userModel.countDocuments({ liked: songId }, (err, likes) => {
+									if (err)
+										return cb({
+											status: "failure",
+											message: "Something went wrong while liking this song."
+										});
+
+									return userModel.countDocuments({ disliked: songId }, (err, dislikes) => {
+										if (err)
+											return cb({
+												status: "failure",
+												message: "Something went wrong while liking this song."
+											});
+
+										return songModel.update(
+											{ _id: songId },
+											{
+												$set: {
+													likes,
+													dislikes
+												}
+											},
+											err => {
+												if (err)
+													return cb({
+														status: "failure",
+														message: "Something went wrong while liking this song."
+													});
+
+												songs.runJob("UPDATE_SONG", { songId });
+
+												cache.runJob("PUB", {
+													channel: "song.like",
+													value: JSON.stringify({
+														songId: oldSongId,
+														userId: session.userId,
+														likes,
+														dislikes
+													})
+												});
+
+												activities.runJob("ADD_ACTIVITY", {
+													userId: session.userId,
+													activityType: "liked_song",
+													payload: [songId]
+												});
+
+												return cb({
+													status: "success",
+													message: "You have successfully liked this song."
+												});
+											}
+										);
+									});
+								});
+							}
+							return cb({
+								status: "failure",
+								message: "Something went wrong while liking this song."
+							});
+						}
+					);
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Dislikes a song
+	 *
+	 * @param session
+	 * @param songId - the song id
+	 * @param cb
+	 */
+	dislike: isLoginRequired(async (session, songId, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
+		async.waterfall(
+			[
+				next => {
+					songModel.findOne({ songId }, next);
+				},
+
+				(song, next) => {
+					if (!song) return next("No song found with that id.");
+					return next(null, song);
+				}
+			],
+			async (err, song) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"SONGS_DISLIKE",
+						`User "${session.userId}" failed to like song ${songId}. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				const oldSongId = songId;
+				songId = song._id;
+				return userModel.findOne({ _id: session.userId }, (err, user) => {
+					if (user.disliked.indexOf(songId) !== -1)
+						return cb({
+							status: "failure",
+							message: "You have already disliked this song."
+						});
+
+					return userModel.updateOne(
+						{ _id: session.userId },
+						{
+							$push: { disliked: songId },
+							$pull: { liked: songId }
+						},
+						err => {
+							if (!err) {
+								return userModel.countDocuments({ liked: songId }, (err, likes) => {
+									if (err)
+										return cb({
+											status: "failure",
+											message: "Something went wrong while disliking this song."
+										});
+
+									return userModel.countDocuments({ disliked: songId }, (err, dislikes) => {
+										if (err)
+											return cb({
+												status: "failure",
+												message: "Something went wrong while disliking this song."
+											});
+
+										return songModel.update(
+											{ _id: songId },
+											{
+												$set: {
+													likes,
+													dislikes
+												}
+											},
+											err => {
+												if (err)
+													return cb({
+														status: "failure",
+														message: "Something went wrong while disliking this song."
+													});
+
+												songs.runJob("UPDATE_SONG", { songId });
+												cache.runJob("PUB", {
+													channel: "song.dislike",
+													value: JSON.stringify({
+														songId: oldSongId,
+														userId: session.userId,
+														likes,
+														dislikes
+													})
+												});
+
+												return cb({
+													status: "success",
+													message: "You have successfully disliked this song."
+												});
+											}
+										);
+									});
+								});
+							}
+							return cb({
+								status: "failure",
+								message: "Something went wrong while disliking this song."
+							});
+						}
+					);
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Undislikes a song
+	 *
+	 * @param session
+	 * @param songId - the song id
+	 * @param cb
+	 */
+	undislike: isLoginRequired(async (session, songId, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
+		async.waterfall(
+			[
+				next => {
+					songModel.findOne({ songId }, next);
+				},
+
+				(song, next) => {
+					if (!song) return next("No song found with that id.");
+					return next(null, song);
+				}
+			],
+			async (err, song) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"SONGS_UNDISLIKE",
+						`User "${session.userId}" failed to like song ${songId}. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				const oldSongId = songId;
+				songId = song._id;
+				return userModel.findOne({ _id: session.userId }, (err, user) => {
+					if (user.disliked.indexOf(songId) === -1)
+						return cb({
+							status: "failure",
+							message: "You have not disliked this song."
+						});
+
+					return userModel.updateOne(
+						{ _id: session.userId },
+						{ $pull: { liked: songId, disliked: songId } },
+						err => {
+							if (!err) {
+								return userModel.countDocuments({ liked: songId }, (err, likes) => {
+									if (err)
+										return cb({
+											status: "failure",
+											message: "Something went wrong while undisliking this song."
+										});
+
+									return userModel.countDocuments({ disliked: songId }, (err, dislikes) => {
+										if (err)
+											return cb({
+												status: "failure",
+												message: "Something went wrong while undisliking this song."
+											});
+
+										return songModel.update(
+											{ _id: songId },
+											{
+												$set: {
+													likes,
+													dislikes
+												}
+											},
+											err => {
+												if (err)
+													return cb({
+														status: "failure",
+														message: "Something went wrong while undisliking this song."
+													});
+												songs.runJob("UPDATE_SONG", { songId });
+												cache.runJob("PUB", {
+													channel: "song.undislike",
+													value: JSON.stringify({
+														songId: oldSongId,
+														userId: session.userId,
+														likes,
+														dislikes
+													})
+												});
+												return cb({
+													status: "success",
+													message: "You have successfully undisliked this song."
+												});
+											}
+										);
+									});
+								});
+							}
+							return cb({
+								status: "failure",
+								message: "Something went wrong while undisliking this song."
+							});
+						}
+					);
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Unlikes a song
+	 *
+	 * @param session
+	 * @param songId - the song id
+	 * @param cb
+	 */
+	unlike: isLoginRequired(async (session, songId, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
+		async.waterfall(
+			[
+				next => {
+					songModel.findOne({ songId }, next);
+				},
+
+				(song, next) => {
+					if (!song) return next("No song found with that id.");
+					return next(null, song);
+				}
+			],
+			async (err, song) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"SONGS_UNLIKE",
+						`User "${session.userId}" failed to like song ${songId}. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				const oldSongId = songId;
+				songId = song._id;
+				return userModel.findOne({ _id: session.userId }, (err, user) => {
+					if (user.liked.indexOf(songId) === -1)
+						return cb({
+							status: "failure",
+							message: "You have not liked this song."
+						});
+
+					return userModel.updateOne(
+						{ _id: session.userId },
+						{ $pull: { liked: songId, disliked: songId } },
+						err => {
+							if (!err) {
+								return userModel.countDocuments({ liked: songId }, (err, likes) => {
+									if (err)
+										return cb({
+											status: "failure",
+											message: "Something went wrong while unliking this song."
+										});
+
+									return userModel.countDocuments({ disliked: songId }, (err, dislikes) => {
+										if (err)
+											return cb({
+												status: "failure",
+												message: "Something went wrong while undiking this song."
+											});
+										return songModel.updateOne(
+											{ _id: songId },
+											{
+												$set: {
+													likes,
+													dislikes
+												}
+											},
+											err => {
+												if (err)
+													return cb({
+														status: "failure",
+														message: "Something went wrong while unliking this song."
+													});
+												songs.runJob("UPDATE_SONG", { songId });
+												cache.runJob("PUB", {
+													channel: "song.unlike",
+													value: JSON.stringify({
+														songId: oldSongId,
+														userId: session.userId,
+														likes,
+														dislikes
+													})
+												});
+												return cb({
+													status: "success",
+													message: "You have successfully unliked this song."
+												});
+											}
+										);
+									});
+								});
+							}
+							return cb({
+								status: "failure",
+								message: "Something went wrong while unliking this song."
+							});
+						}
+					);
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Gets user's own song ratings
+	 *
+	 * @param session
+	 * @param songId - the song id
+	 * @param cb
+	 */
+	getOwnSongRatings: isLoginRequired(async (session, songId, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		const songModel = await db.runJob("GET_MODEL", { modelName: "song" });
+		async.waterfall(
+			[
+				next => {
+					songModel.findOne({ songId }, next);
+				},
+
+				(song, next) => {
+					if (!song) return next("No song found with that id.");
+					return next(null, song);
+				}
+			],
+			async (err, song) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"SONGS_GET_OWN_RATINGS",
+						`User "${session.userId}" failed to get ratings for ${songId}. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				const newSongId = song._id;
+				return userModel.findOne({ _id: session.userId }, async (err, user) => {
+					if (!err && user) {
+						return cb({
+							status: "success",
+							songId,
+							liked: user.liked.indexOf(newSongId) !== -1,
+							disliked: user.disliked.indexOf(newSongId) !== -1
+						});
+					}
+					return cb({
+						status: "failure",
+						message: await utils.runJob("GET_ERROR", {
+							error: err
+						})
+					});
+				});
+			}
+		);
+	})
 };

+ 2113 - 2391
backend/logic/actions/stations.js

@@ -1,2438 +1,2160 @@
-"use strict";
+import async from "async";
+import underscore from "underscore";
 
-const async = require("async");
-const _ = require("underscore")._;
+import { isLoginRequired, isOwnerRequired } from "./hooks";
+import db from "../db";
 
-const hooks = require("./hooks");
+import utils from "../utils";
+import songs from "../songs";
+import cache from "../cache";
+import notifications from "../notifications";
+import stations from "../stations";
+import activities from "../activities";
 
-const db = require("../db");
-const cache = require("../cache");
-const notifications = require("../notifications");
-const utils = require("../utils");
-const stations = require("../stations");
-const songs = require("../songs");
-const activities = require("../activities");
-const user = require("../db/schemas/user");
+const { _ } = underscore;
 
 // const logger = moduleManager.modules["logger"];
 
-let userList = {};
+const userList = {};
 let usersPerStation = {};
 let usersPerStationCount = {};
 
 // Temporarily disabled until the messages in console can be limited
 setInterval(async () => {
-    let stationsCountUpdated = [];
-    let stationsUpdated = [];
-
-    let oldUsersPerStation = usersPerStation;
-    usersPerStation = {};
-
-    let oldUsersPerStationCount = usersPerStationCount;
-    usersPerStationCount = {};
-
-    const userModel = await db.runJob("GET_MODEL", {
-        modelName: "user",
-    });
-
-    async.each(
-        Object.keys(userList),
-        function(socketId, next) {
-            utils.runJob("SOCKET_FROM_SESSION", { socketId }, { isQuiet: true }).then((socket) => {
-                let 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]++;
-                if (!usersPerStation[stationId])
-                    usersPerStation[stationId] = [];
-
-                async.waterfall(
-                    [
-                        (next) => {
-                            if (!socket.session || !socket.session.sessionId)
-                                return next("No session found.");
-                            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.");
-                            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.");
-                            next(null, user.username);
-                        },
-                    ],
-                    (err, username) => {
-                        if (!err) {
-                            usersPerStation[stationId].push(username);
-                        }
-                        next();
-                    }
-                );
-            });
-            //TODO Code to show users
-        },
-        (err) => {
-            for (let stationId in usersPerStationCount) {
-                if (
-                    oldUsersPerStationCount[stationId] !==
-                    usersPerStationCount[stationId]
-                ) {
-                    if (stationsCountUpdated.indexOf(stationId) === -1)
-                        stationsCountUpdated.push(stationId);
-                }
-            }
-
-            for (let stationId in usersPerStation) {
-                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);
-        }
-    );
+	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);
 
 cache.runJob("SUB", {
-    channel: "station.updateUsers",
-    cb: (stationId) => {
-        let list = usersPerStation[stationId] || [];
-        utils.runJob("EMIT_TO_ROOM", {
-            room: `station.${stationId}`,
-            args: ["event:users.updated", list],
-        });
-    },
+	channel: "station.updateUsers",
+	cb: stationId => {
+		const list = usersPerStation[stationId] || [];
+		utils.runJob("EMIT_TO_ROOM", {
+			room: `station.${stationId}`,
+			args: ["event:users.updated", list]
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "station.updateUserCount",
-    cb: (stationId) => {
-        let count = usersPerStationCount[stationId] || 0;
-        utils.runJob("EMIT_TO_ROOM", {
-            room: `station.${stationId}`,
-            args: ["event:userCount.updated", count],
-        });
-        stations.runJob("GET_STATION", { stationId }).then(async (station) => {
-            if (station.privacy === "public")
-                utils.runJob("EMIT_TO_ROOM", {
-                    room: "home",
-                    args: ["event:userCount.updated", stationId, count],
-                });
-            else {
-                let sockets = await utils.runJob("GET_ROOM_SOCKETS", {
-                    room: "home",
-                });
-                for (let socketId in sockets) {
-                    let socket = sockets[socketId];
-                    let session = sockets[socketId].session;
-                    if (session.sessionId) {
-                        cache
-                            .runJob("HGET", {
-                                table: "sessions",
-                                key: session.sessionId,
-                            })
-                            .then((session) => {
-                                if (session)
-                                    db.runJob("GET_MODEL", {
-                                        modelName: "user",
-                                    }).then((userModel) =>
-                                        userModel.findOne(
-                                            { _id: session.userId },
-                                            (err, user) => {
-                                                if (user.role === "admin")
-                                                    socket.emit(
-                                                        "event:userCount.updated",
-                                                        stationId,
-                                                        count
-                                                    );
-                                                else if (
-                                                    station.type ===
-                                                        "community" &&
-                                                    station.owner ===
-                                                        session.userId
-                                                )
-                                                    socket.emit(
-                                                        "event:userCount.updated",
-                                                        stationId,
-                                                        count
-                                                    );
-                                            }
-                                        )
-                                    );
-                            });
-                    }
-                }
-            }
-        });
-    },
+	channel: "station.updateUserCount",
+	cb: stationId => {
+		const count = usersPerStationCount[stationId] || 0;
+		utils.runJob("EMIT_TO_ROOM", {
+			room: `station.${stationId}`,
+			args: ["event:userCount.updated", count]
+		});
+		stations.runJob("GET_STATION", { stationId }).then(async station => {
+			if (station.privacy === "public")
+				utils.runJob("EMIT_TO_ROOM", {
+					room: "home",
+					args: ["event:userCount.updated", stationId, count]
+				});
+			else {
+				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]];
+					if (session.sessionId) {
+						cache
+							.runJob("HGET", {
+								table: "sessions",
+								key: session.sessionId
+							})
+							.then(session => {
+								if (session)
+									db.runJob("GET_MODEL", {
+										modelName: "user"
+									}).then(userModel =>
+										userModel.findOne({ _id: session.userId }, (err, user) => {
+											if (user.role === "admin")
+												socket.emit("event:userCount.updated", stationId, count);
+											else if (station.type === "community" && station.owner === session.userId)
+												socket.emit("event:userCount.updated", stationId, count);
+										})
+									);
+							});
+					}
+				}
+			}
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "station.queueLockToggled",
-    cb: (data) => {
-        utils.runJob("EMIT_TO_ROOM", {
-            room: `station.${data.stationId}`,
-            args: ["event:queueLockToggled", data.locked],
-        });
-    },
+	channel: "station.queueLockToggled",
+	cb: data => {
+		utils.runJob("EMIT_TO_ROOM", {
+			room: `station.${data.stationId}`,
+			args: ["event:queueLockToggled", data.locked]
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "station.updatePartyMode",
-    cb: (data) => {
-        utils.runJob("EMIT_TO_ROOM", {
-            room: `station.${data.stationId}`,
-            args: ["event:partyMode.updated", data.partyMode],
-        });
-    },
+	channel: "station.updatePartyMode",
+	cb: data => {
+		utils.runJob("EMIT_TO_ROOM", {
+			room: `station.${data.stationId}`,
+			args: ["event:partyMode.updated", data.partyMode]
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "privatePlaylist.selected",
-    cb: (data) => {
-        utils.runJob("EMIT_TO_ROOM", {
-            room: `station.${data.stationId}`,
-            args: ["event:privatePlaylist.selected", data.playlistId],
-        });
-    },
+	channel: "privatePlaylist.selected",
+	cb: data => {
+		utils.runJob("EMIT_TO_ROOM", {
+			room: `station.${data.stationId}`,
+			args: ["event:privatePlaylist.selected", data.playlistId]
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "station.pause",
-    cb: (stationId) => {
-        stations.runJob("GET_STATION", { stationId }).then((station) => {
-            utils.runJob("EMIT_TO_ROOM", {
-                room: `station.${stationId}`,
-                args: ["event:stations.pause", { pausedAt: station.pausedAt }],
-            });
-        });
-    },
+	channel: "station.pause",
+	cb: stationId => {
+		stations.runJob("GET_STATION", { stationId }).then(station => {
+			utils.runJob("EMIT_TO_ROOM", {
+				room: `station.${stationId}`,
+				args: ["event:stations.pause", { pausedAt: station.pausedAt }]
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "station.resume",
-    cb: (stationId) => {
-        stations.runJob("GET_STATION", { stationId }).then((station) => {
-            utils.runJob("EMIT_TO_ROOM", {
-                room: `station.${stationId}`,
-                args: [
-                    "event:stations.resume",
-                    { timePaused: station.timePaused },
-                ],
-            });
-        });
-    },
+	channel: "station.resume",
+	cb: stationId => {
+		stations.runJob("GET_STATION", { stationId }).then(station => {
+			utils.runJob("EMIT_TO_ROOM", {
+				room: `station.${stationId}`,
+				args: ["event:stations.resume", { timePaused: station.timePaused }]
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "station.queueUpdate",
-    cb: (stationId) => {
-        stations.runJob("GET_STATION", { stationId }).then((station) => {
-            utils.runJob("EMIT_TO_ROOM", {
-                room: `station.${stationId}`,
-                args: ["event:queue.update", station.queue],
-            });
-        });
-    },
+	channel: "station.queueUpdate",
+	cb: stationId => {
+		stations.runJob("GET_STATION", { stationId }).then(station => {
+			utils.runJob("EMIT_TO_ROOM", {
+				room: `station.${stationId}`,
+				args: ["event:queue.update", station.queue]
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "station.voteSkipSong",
-    cb: (stationId) => {
-        utils.runJob("EMIT_TO_ROOM", {
-            room: `station.${stationId}`,
-            args: ["event:song.voteSkipSong"],
-        });
-    },
+	channel: "station.voteSkipSong",
+	cb: stationId => {
+		utils.runJob("EMIT_TO_ROOM", {
+			room: `station.${stationId}`,
+			args: ["event:song.voteSkipSong"]
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "station.remove",
-    cb: (stationId) => {
-        utils.runJob("EMIT_TO_ROOM", {
-            room: `station.${stationId}`,
-            args: ["event:stations.remove"],
-        });
-        utils.runJob("EMIT_TO_ROOM", {
-            room: "admin.stations",
-            args: ["event:admin.station.removed", stationId],
-        });
-    },
+	channel: "station.remove",
+	cb: stationId => {
+		utils.runJob("EMIT_TO_ROOM", {
+			room: `station.${stationId}`,
+			args: ["event:stations.remove"]
+		});
+		utils.runJob("EMIT_TO_ROOM", {
+			room: "admin.stations",
+			args: ["event:admin.station.removed", stationId]
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "station.create",
-    cb: async (stationId) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-
-        stations
-            .runJob("INITIALIZE_STATION", { stationId })
-            .then(async (response) => {
-                const station = response.station;
-                station.userCount = usersPerStationCount[stationId] || 0;
-                utils.runJob("EMIT_TO_ROOM", {
-                    room: "admin.stations",
-                    args: ["event:admin.station.added", station],
-                });
-                // TODO If community, check if on whitelist
-                if (station.privacy === "public")
-                    utils.runJob("EMIT_TO_ROOM", {
-                        room: "home",
-                        args: ["event:stations.created", station],
-                    });
-                else {
-                    let sockets = await utils.runJob("GET_ROOM_SOCKETS", {
-                        room: "home",
-                    });
-                    for (let socketId in sockets) {
-                        let socket = sockets[socketId];
-                        let session = sockets[socketId].session;
-                        if (session.sessionId) {
-                            cache
-                                .runJob("HGET", {
-                                    table: "sessions",
-                                    key: session.sessionId,
-                                })
-                                .then((session) => {
-                                    if (session) {
-                                        userModel.findOne(
-                                            { _id: session.userId },
-                                            (err, user) => {
-                                                if (user.role === "admin")
-                                                    socket.emit(
-                                                        "event:stations.created",
-                                                        station
-                                                    );
-                                                else if (
-                                                    station.type ===
-                                                        "community" &&
-                                                    station.owner ===
-                                                        session.userId
-                                                )
-                                                    socket.emit(
-                                                        "event:stations.created",
-                                                        station
-                                                    );
-                                            }
-                                        );
-                                    }
-                                });
-                        }
-                    }
-                }
-            });
-    },
+	channel: "station.create",
+	cb: async stationId => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+
+		stations.runJob("INITIALIZE_STATION", { stationId }).then(async response => {
+			const { station } = response;
+			station.userCount = usersPerStationCount[stationId] || 0;
+			utils.runJob("EMIT_TO_ROOM", {
+				room: "admin.stations",
+				args: ["event:admin.station.added", station]
+			});
+			// TODO If community, check if on whitelist
+			if (station.privacy === "public")
+				utils.runJob("EMIT_TO_ROOM", {
+					room: "home",
+					args: ["event:stations.created", station]
+				});
+			else {
+				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]];
+					if (session.sessionId) {
+						cache
+							.runJob("HGET", {
+								table: "sessions",
+								key: session.sessionId
+							})
+							.then(session => {
+								if (session) {
+									userModel.findOne({ _id: session.userId }, (err, user) => {
+										if (user.role === "admin") socket.emit("event:stations.created", station);
+										else if (station.type === "community" && station.owner === session.userId)
+											socket.emit("event:stations.created", station);
+									});
+								}
+							});
+					}
+				}
+			}
+		});
+	}
 });
 
-module.exports = {
-    /**
-     * Get a list of all the stations
-     *
-     * @param session
-     * @param cb
-     * @return {{ status: String, stations: Array }}
-     */
-    index: (session, cb) => {
-        async.waterfall(
-            [
-                (next) => {
-                    cache
-                        .runJob("HGETALL", { table: "stations" })
-                        .then((stations) => {
-                            next(null, stations);
-                        });
-                },
-
-                (stations, next) => {
-                    let resultStations = [];
-                    for (let id in stations) {
-                        resultStations.push(stations[id]);
-                    }
-                    next(null, stations);
-                },
-
-                (stationsArray, next) => {
-                    let resultStations = [];
-                    async.each(
-                        stationsArray,
-                        (station, next) => {
-                            async.waterfall(
-                                [
-                                    (next) => {
-                                        stations
-                                            .runJob("CAN_USER_VIEW_STATION", {
-                                                station,
-                                                userId: session.userId,
-                                                hideUnlisted: true
-                                            })
-                                            .then((exists) => {
-                                                next(null, exists);
-                                            })
-                                            .catch(next);
-                                    },
-                                ],
-                                (err, exists) => {
-                                    if (err) console.log(err);
-                                    station.userCount =
-                                        usersPerStationCount[station._id] || 0;
-                                    if (exists) resultStations.push(station);
-                                    next();
-                                }
-                            );
-                        },
-                        () => {
-                            next(null, resultStations);
-                        }
-                    );
-                },
-            ],
-            async (err, stations) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "STATIONS_INDEX",
-                        `Indexing stations failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "STATIONS_INDEX",
-                    `Indexing stations successful.`,
-                    false
-                );
-                return cb({ status: "success", stations: stations });
-            }
-        );
-    },
-
-    /**
-     * Obtains basic metadata of a station in order to format an activity
-     *
-     * @param session
-     * @param stationId - the station id
-     * @param cb
-     */
-    getStationForActivity: (session, stationId, cb) => {
-        async.waterfall(
-            [
-                (next) => {
-                    stations
-                        .runJob("GET_STATION", { stationId })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-            ],
-            async (err, station) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "STATIONS_GET_STATION_FOR_ACTIVITY",
-                        `Failed to obtain metadata of station ${stationId} for activity formatting. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "STATIONS_GET_STATION_FOR_ACTIVITY",
-                        `Obtained metadata of station ${stationId} for activity formatting successfully.`
-                    );
-                    return cb({
-                        status: "success",
-                        data: {
-                            title: station.displayName,
-                            thumbnail: station.currentSong
-                                ? station.currentSong.thumbnail
-                                : "",
-                        },
-                    });
-                }
-            }
-        );
-    },
-
-    /**
-     * Verifies that a station exists
-     *
-     * @param session
-     * @param stationName - the station name
-     * @param cb
-     */
-    existsByName: (session, stationName, cb) => {
-        async.waterfall(
-            [
-                (next) => {
-                    stations
-                        .runJob("GET_STATION_BY_NAME", { stationName })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-
-                (station, next) => {
-                    if (!station) return next(null, false);
-                    stations
-                        .runJob("CAN_USER_VIEW_STATION", {
-                            station,
-                            userId: session.userId,
-                        })
-                        .then((exists) => {
-                            next(null, exists);
-                        })
-                        .catch(next);
-                },
-            ],
-            async (err, exists) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "STATION_EXISTS_BY_NAME",
-                        `Checking if station "${stationName}" exists failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "STATION_EXISTS_BY_NAME",
-                    `Station "${stationName}" exists successfully.` /*, false*/
-                );
-                cb({ status: "success", exists });
-            }
-        );
-    },
-
-    /**
-     * Gets the official playlist for a station
-     *
-     * @param session
-     * @param stationId - the station id
-     * @param cb
-     */
-    getPlaylist: (session, stationId, cb) => {
-        async.waterfall(
-            [
-                (next) => {
-                    stations
-                        .runJob("GET_STATION", { stationId })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-
-                (station, next) => {
-                    stations
-                        .runJob("CAN_USER_VIEW_STATION", {
-                            station,
-                            userId: session.userId,
-                        })
-                        .then((canView) => {
-                            if (canView) return next(null, station);
-                            return next("Insufficient permissions.");
-                        })
-                        .catch((err) => {
-                            return next(err);
-                        });
-                },
-
-                (station, next) => {
-                    if (!station) return next("Station not found.");
-                    else if (station.type !== "official")
-                        return next("This is not an official station.");
-                    else next();
-                },
-
-                (next) => {
-                    cache
-                        .runJob("HGET", {
-                            table: "officialPlaylists",
-                            key: stationId,
-                        })
-                        .then((playlist) => {
-                            next(null, playlist);
-                        })
-                        .catch(next);
-                },
-
-                (playlist, next) => {
-                    if (!playlist) return next("Playlist not found.");
-                    next(null, playlist);
-                },
-            ],
-            async (err, playlist) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "STATIONS_GET_PLAYLIST",
-                        `Getting playlist for station "${stationId}" failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "STATIONS_GET_PLAYLIST",
-                        `Got playlist for station "${stationId}" successfully.`,
-                        false
-                    );
-                    cb({ status: "success", data: playlist.songs });
-                }
-            }
-        );
-    },
-
-    /**
-     * Joins the station by its name
-     *
-     * @param session
-     * @param stationName - the station name
-     * @param cb
-     * @return {{ status: String, userCount: Integer }}
-     */
-    join: (session, stationName, cb) => {
-        async.waterfall(
-            [
-                (next) => {
-                    stations
-                        .runJob("GET_STATION_BY_NAME", { stationName })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-
-                (station, next) => {
-                    if (!station) return next("Station not found.");
-                    stations
-                        .runJob("CAN_USER_VIEW_STATION", {
-                            station,
-                            userId: session.userId,
-                        })
-                        .then((canView) => {
-                            if (!canView) next("Not allowed to join station.");
-                            else next(null, station);
-                        })
-                        .catch((err) => {
-                            return next(err);
-                        });
-                },
-
-                (station, next) => {
-                    utils.runJob("SOCKET_JOIN_ROOM", {
-                        socketId: session.socketId,
-                        room: `station.${station._id}`,
-                    });
-                    let data = {
-                        _id: station._id,
-                        type: station.type,
-                        currentSong: station.currentSong,
-                        startedAt: station.startedAt,
-                        paused: station.paused,
-                        timePaused: station.timePaused,
-                        pausedAt: station.pausedAt,
-                        description: station.description,
-                        displayName: station.displayName,
-                        privacy: station.privacy,
-                        locked: station.locked,
-                        partyMode: station.partyMode,
-                        owner: station.owner,
-                        privatePlaylist: station.privatePlaylist,
-                    };
-                    userList[session.socketId] = station._id;
-                    next(null, data);
-                },
-
-                (data, next) => {
-                    data = JSON.parse(JSON.stringify(data));
-                    data.userCount = usersPerStationCount[data._id] || 0;
-                    data.users = usersPerStation[data._id] || [];
-                    if (!data.currentSong || !data.currentSong.title)
-                        return next(null, data);
-                    utils.runJob("SOCKET_JOIN_SONG_ROOM", {
-                        socketId: session.socketId,
-                        room: `song.${data.currentSong.songId}`,
-                    });
-                    data.currentSong.skipVotes =
-                        data.currentSong.skipVotes.length;
-                    songs
-                        .runJob("GET_SONG_FROM_ID", {
-                            songId: data.currentSong.songId,
-                        })
-                        .then((response) => {
-                            const song = response.song;
-                            if (song) {
-                                data.currentSong.likes = song.likes;
-                                data.currentSong.dislikes = song.dislikes;
-                            } else {
-                                data.currentSong.likes = -1;
-                                data.currentSong.dislikes = -1;
-                            }
-                        })
-                        .catch((err) => {
-                            data.currentSong.likes = -1;
-                            data.currentSong.dislikes = -1;
-                        })
-                        .finally(() => {
-                            next(null, data);
-                        });
-                },
-            ],
-            async (err, data) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "STATIONS_JOIN",
-                        `Joining station "${stationName}" failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "STATIONS_JOIN",
-                    `Joined station "${data._id}" successfully.`
-                );
-                cb({ status: "success", data });
-            }
-        );
-    },
-
-    /**
-     * Toggles if a station is locked
-     *
-     * @param session
-     * @param stationId - the station id
-     * @param cb
-     */
-    toggleLock: hooks.ownerRequired(async (session, stationId, cb) => {
-        const stationModel = await db.runJob("GET_MODEL", {
-            modelName: "station",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    stations
-                        .runJob("GET_STATION", { stationId })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-
-                (station, next) => {
-                    stationModel.updateOne(
-                        { _id: stationId },
-                        { $set: { locked: !station.locked } },
-                        next
-                    );
-                },
-
-                (res, next) => {
-                    stations
-                        .runJob("UPDATE_STATION", { stationId })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-            ],
-            async (err, station) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "STATIONS_UPDATE_LOCKED_STATUS",
-                        `Toggling the queue lock for station "${stationId}" failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "STATIONS_UPDATE_LOCKED_STATUS",
-                        `Toggled the queue lock for station "${stationId}" successfully to "${station.locked}".`
-                    );
-                    cache.runJob("PUB", {
-                        channel: "station.queueLockToggled",
-                        value: {
-                            stationId,
-                            locked: station.locked,
-                        },
-                    });
-                    return cb({ status: "success", data: station.locked });
-                }
-            }
-        );
-    }),
-
-    /**
-     * Votes to skip a station
-     *
-     * @param session
-     * @param stationId - the station id
-     * @param cb
-     */
-    voteSkip: hooks.loginRequired(async (session, stationId, cb) => {
-        const stationModel = await db.runJob("GET_MODEL", {
-            modelName: "station",
-        });
-
-        let skipVotes = 0;
-        let shouldSkip = false;
-
-        async.waterfall(
-            [
-                (next) => {
-                    stations
-                        .runJob("GET_STATION", { stationId })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-
-                (station, next) => {
-                    if (!station) return next("Station not found.");
-                    stations
-                        .runJob("CAN_USER_VIEW_STATION", {
-                            station,
-                            userId: session.userId,
-                        })
-                        .then((canView) => {
-                            if (canView) return next(null, station);
-                            return next("Insufficient permissions.");
-                        })
-                        .catch((err) => {
-                            return next(err);
-                        });
-                },
-
-                (station, next) => {
-                    if (!station.currentSong)
-                        return next("There is currently no song to skip.");
-                    if (
-                        station.currentSong.skipVotes.indexOf(
-                            session.userId
-                        ) !== -1
-                    )
-                        return next(
-                            "You have already voted to skip this song."
-                        );
-                    next(null, station);
-                },
-
-                (station, next) => {
-                    stationModel.updateOne(
-                        { _id: stationId },
-                        { $push: { "currentSong.skipVotes": session.userId } },
-                        next
-                    );
-                },
-
-                (res, next) => {
-                    stations
-                        .runJob("UPDATE_STATION", { stationId })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-
-                (station, next) => {
-                    if (!station) return next("Station not found.");
-                    next(null, station);
-                },
-
-                (station, next) => {
-                    skipVotes = station.currentSong.skipVotes.length;
-                    utils
-                        .runJob("GET_ROOM_SOCKETS", {
-                            room: `station.${stationId}`,
-                        })
-                        .then((sockets) => {
-                            next(null, sockets);
-                        })
-                        .catch(next);
-                },
-
-                (sockets, next) => {
-                    if (sockets.length <= skipVotes) shouldSkip = true;
-                    next();
-                },
-            ],
-            async (err, station) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "STATIONS_VOTE_SKIP",
-                        `Vote skipping station "${stationId}" failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "STATIONS_VOTE_SKIP",
-                    `Vote skipping "${stationId}" successful.`
-                );
-                cache.runJob("PUB", {
-                    channel: "station.voteSkipSong",
-                    value: stationId,
-                });
-                cb({
-                    status: "success",
-                    message: "Successfully voted to skip the song.",
-                });
-                if (shouldSkip) stations.runJob("SKIP_STATION", { stationId });
-            }
-        );
-    }),
-
-    /**
-     * Force skips a station
-     *
-     * @param session
-     * @param stationId - the station id
-     * @param cb
-     */
-    forceSkip: hooks.ownerRequired((session, stationId, cb) => {
-        async.waterfall(
-            [
-                (next) => {
-                    stations
-                        .runJob("GET_STATION", { stationId })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-
-                (station, next) => {
-                    if (!station) return next("Station not found.");
-                    next();
-                },
-            ],
-            async (err) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "STATIONS_FORCE_SKIP",
-                        `Force skipping station "${stationId}" failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                notifications.runJob("UNSCHEDULE", {
-                    name: `stations.nextSong?id=${stationId}`,
-                });
-                stations.runJob("SKIP_STATION", { stationId });
-                console.log(
-                    "SUCCESS",
-                    "STATIONS_FORCE_SKIP",
-                    `Force skipped station "${stationId}" successfully.`
-                );
-                return cb({
-                    status: "success",
-                    message: "Successfully skipped station.",
-                });
-            }
-        );
-    }),
-
-    /**
-     * Leaves the user's current station
-     *
-     * @param session
-     * @param stationId
-     * @param cb
-     * @return {{ status: String, userCount: Integer }}
-     */
-    leave: (session, stationId, cb) => {
-        async.waterfall(
-            [
-                (next) => {
-                    stations
-                        .runJob("GET_STATION", { stationId })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-
-                (station, next) => {
-                    if (!station) return next("Station not found.");
-                    next();
-                },
-            ],
-            async (err, userCount) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "STATIONS_LEAVE",
-                        `Leaving station "${stationId}" failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "STATIONS_LEAVE",
-                    `Left station "${stationId}" successfully.`
-                );
-                utils.runJob("SOCKET_LEAVE_ROOMS", { socketId: session });
-                delete userList[session.socketId];
-                return cb({
-                    status: "success",
-                    message: "Successfully left station.",
-                    userCount,
-                });
-            }
-        );
-    },
-
-    /**
-     * Updates a station's name
-     *
-     * @param session
-     * @param stationId - the station id
-     * @param newName - the new station name
-     * @param cb
-     */
-    updateName: hooks.ownerRequired(async (session, stationId, newName, cb) => {
-        const stationModel = await db.runJob("GET_MODEL", {
-            modelName: "station",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    stationModel.updateOne(
-                        { _id: stationId },
-                        { $set: { name: newName } },
-                        { runValidators: true },
-                        next
-                    );
-                },
-
-                (res, next) => {
-                    stations
-                        .runJob("UPDATE_STATION", { stationId })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-            ],
-            async (err) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "STATIONS_UPDATE_NAME",
-                        `Updating station "${stationId}" name to "${newName}" failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "STATIONS_UPDATE_NAME",
-                    `Updated station "${stationId}" name to "${newName}" successfully.`
-                );
-                return cb({
-                    status: "success",
-                    message: "Successfully updated the name.",
-                });
-            }
-        );
-    }),
-
-    /**
-     * Updates a station's display name
-     *
-     * @param session
-     * @param stationId - the station id
-     * @param newDisplayName - the new station display name
-     * @param cb
-     */
-    updateDisplayName: hooks.ownerRequired(
-        async (session, stationId, newDisplayName, cb) => {
-            const stationModel = await db.runJob("GET_MODEL", {
-                modelName: "station",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        stationModel.updateOne(
-                            { _id: stationId },
-                            { $set: { displayName: newDisplayName } },
-                            { runValidators: true },
-                            next
-                        );
-                    },
-
-                    (res, next) => {
-                        stations
-                            .runJob("UPDATE_STATION", { stationId })
-                            .then((station) => {
-                                next(null, station);
-                            })
-                            .catch(next);
-                    },
-                ],
-                async (err) => {
-                    if (err) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "STATIONS_UPDATE_DISPLAY_NAME",
-                            `Updating station "${stationId}" displayName to "${newDisplayName}" failed. "${err}"`
-                        );
-                        return cb({ status: "failure", message: err });
-                    }
-                    console.log(
-                        "SUCCESS",
-                        "STATIONS_UPDATE_DISPLAY_NAME",
-                        `Updated station "${stationId}" displayName to "${newDisplayName}" successfully.`
-                    );
-                    return cb({
-                        status: "success",
-                        message: "Successfully updated the display name.",
-                    });
-                }
-            );
-        }
-    ),
-
-    /**
-     * Updates a station's description
-     *
-     * @param session
-     * @param stationId - the station id
-     * @param newDescription - the new station description
-     * @param cb
-     */
-    updateDescription: hooks.ownerRequired(
-        async (session, stationId, newDescription, cb) => {
-            const stationModel = await db.runJob("GET_MODEL", {
-                modelName: "station",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        stationModel.updateOne(
-                            { _id: stationId },
-                            { $set: { description: newDescription } },
-                            { runValidators: true },
-                            next
-                        );
-                    },
-
-                    (res, next) => {
-                        stations
-                            .runJob("UPDATE_STATION", { stationId })
-                            .then((station) => {
-                                next(null, station);
-                            })
-                            .catch(next);
-                    },
-                ],
-                async (err) => {
-                    if (err) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "STATIONS_UPDATE_DESCRIPTION",
-                            `Updating station "${stationId}" description to "${newDescription}" failed. "${err}"`
-                        );
-                        return cb({ status: "failure", message: err });
-                    }
-                    console.log(
-                        "SUCCESS",
-                        "STATIONS_UPDATE_DESCRIPTION",
-                        `Updated station "${stationId}" description to "${newDescription}" successfully.`
-                    );
-                    return cb({
-                        status: "success",
-                        message: "Successfully updated the description.",
-                    });
-                }
-            );
-        }
-    ),
-
-    /**
-     * Updates a station's privacy
-     *
-     * @param session
-     * @param stationId - the station id
-     * @param newPrivacy - the new station privacy
-     * @param cb
-     */
-    updatePrivacy: hooks.ownerRequired(
-        async (session, stationId, newPrivacy, cb) => {
-            const stationModel = await db.runJob("GET_MODEL", {
-                modelName: "station",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        stationModel.updateOne(
-                            { _id: stationId },
-                            { $set: { privacy: newPrivacy } },
-                            { runValidators: true },
-                            next
-                        );
-                    },
-
-                    (res, next) => {
-                        stations
-                            .runJob("UPDATE_STATION", { stationId })
-                            .then((station) => {
-                                next(null, station);
-                            })
-                            .catch(next);
-                    },
-                ],
-                async (err) => {
-                    if (err) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "STATIONS_UPDATE_PRIVACY",
-                            `Updating station "${stationId}" privacy to "${newPrivacy}" failed. "${err}"`
-                        );
-                        return cb({ status: "failure", message: err });
-                    }
-                    console.log(
-                        "SUCCESS",
-                        "STATIONS_UPDATE_PRIVACY",
-                        `Updated station "${stationId}" privacy to "${newPrivacy}" successfully.`
-                    );
-                    return cb({
-                        status: "success",
-                        message: "Successfully updated the privacy.",
-                    });
-                }
-            );
-        }
-    ),
-
-    /**
-     * Updates a station's genres
-     *
-     * @param session
-     * @param stationId - the station id
-     * @param newGenres - the new station genres
-     * @param cb
-     */
-    updateGenres: hooks.ownerRequired(
-        async (session, stationId, newGenres, cb) => {
-            const stationModel = await db.runJob("GET_MODEL", {
-                modelName: "station",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        stationModel.updateOne(
-                            { _id: stationId },
-                            { $set: { genres: newGenres } },
-                            { runValidators: true },
-                            next
-                        );
-                    },
-
-                    (res, next) => {
-                        stations
-                            .runJob("UPDATE_STATION", { stationId })
-                            .then((station) => {
-                                next(null, station);
-                            })
-                            .catch(next);
-                    },
-                ],
-                async (err) => {
-                    if (err) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "STATIONS_UPDATE_GENRES",
-                            `Updating station "${stationId}" genres to "${newGenres}" failed. "${err}"`
-                        );
-                        return cb({ status: "failure", message: err });
-                    }
-                    console.log(
-                        "SUCCESS",
-                        "STATIONS_UPDATE_GENRES",
-                        `Updated station "${stationId}" genres to "${newGenres}" successfully.`
-                    );
-                    return cb({
-                        status: "success",
-                        message: "Successfully updated the genres.",
-                    });
-                }
-            );
-        }
-    ),
-
-    /**
-     * Updates a station's blacklisted genres
-     *
-     * @param session
-     * @param stationId - the station id
-     * @param newBlacklistedGenres - the new station blacklisted genres
-     * @param cb
-     */
-    updateBlacklistedGenres: hooks.ownerRequired(
-        async (session, stationId, newBlacklistedGenres, cb) => {
-            const stationModel = await db.runJob("GET_MODEL", {
-                modelName: "station",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        stationModel.updateOne(
-                            { _id: stationId },
-                            {
-                                $set: {
-                                    blacklistedGenres: newBlacklistedGenres,
-                                },
-                            },
-                            { runValidators: true },
-                            next
-                        );
-                    },
-
-                    (res, next) => {
-                        stations
-                            .runJob("UPDATE_STATION", { stationId })
-                            .then((station) => {
-                                next(null, station);
-                            })
-                            .catch(next);
-                    },
-                ],
-                async (err) => {
-                    if (err) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "STATIONS_UPDATE_BLACKLISTED_GENRES",
-                            `Updating station "${stationId}" blacklisted genres to "${newBlacklistedGenres}" failed. "${err}"`
-                        );
-                        return cb({ status: "failure", message: err });
-                    }
-                    console.log(
-                        "SUCCESS",
-                        "STATIONS_UPDATE_BLACKLISTED_GENRES",
-                        `Updated station "${stationId}" blacklisted genres to "${newBlacklistedGenres}" successfully.`
-                    );
-                    return cb({
-                        status: "success",
-                        message: "Successfully updated the blacklisted genres.",
-                    });
-                }
-            );
-        }
-    ),
-
-    /**
-     * Updates a station's party mode
-     *
-     * @param session
-     * @param stationId - the station id
-     * @param newPartyMode - the new station party mode
-     * @param cb
-     */
-    updatePartyMode: hooks.ownerRequired(
-        async (session, stationId, newPartyMode, cb) => {
-            const stationModel = await db.runJob("GET_MODEL", {
-                modelName: "station",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        stations
-                            .runJob("GET_STATION", { stationId })
-                            .then((station) => {
-                                next(null, station);
-                            })
-                            .catch(next);
-                    },
-
-                    (station, next) => {
-                        if (!station) return next("Station not found.");
-                        if (station.partyMode === newPartyMode)
-                            return next(
-                                "The party mode was already " +
-                                    (newPartyMode ? "enabled." : "disabled.")
-                            );
-                        stationModel.updateOne(
-                            { _id: stationId },
-                            { $set: { partyMode: newPartyMode } },
-                            { runValidators: true },
-                            next
-                        );
-                    },
-
-                    (res, next) => {
-                        stations
-                            .runJob("UPDATE_STATION", { stationId })
-                            .then((station) => {
-                                next(null, station);
-                            })
-                            .catch(next);
-                    },
-                ],
-                async (err) => {
-                    if (err) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "STATIONS_UPDATE_PARTY_MODE",
-                            `Updating station "${stationId}" party mode to "${newPartyMode}" failed. "${err}"`
-                        );
-                        return cb({ status: "failure", message: err });
-                    }
-                    console.log(
-                        "SUCCESS",
-                        "STATIONS_UPDATE_PARTY_MODE",
-                        `Updated station "${stationId}" party mode to "${newPartyMode}" successfully.`
-                    );
-                    cache.runJob("PUB", {
-                        channel: "station.updatePartyMode",
-                        value: {
-                            stationId: stationId,
-                            partyMode: newPartyMode,
-                        },
-                    });
-                    stations.runJob("SKIP_STATION", { stationId });
-                    return cb({
-                        status: "success",
-                        message: "Successfully updated the party mode.",
-                    });
-                }
-            );
-        }
-    ),
-
-    /**
-     * Pauses a station
-     *
-     * @param session
-     * @param stationId - the station id
-     * @param cb
-     */
-    pause: hooks.ownerRequired(async (session, stationId, cb) => {
-        const stationModel = await db.runJob("GET_MODEL", {
-            modelName: "station",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    stations
-                        .runJob("GET_STATION", { stationId })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-
-                (station, next) => {
-                    if (!station) return next("Station not found.");
-                    if (station.paused)
-                        return next("That station was already paused.");
-                    stationModel.updateOne(
-                        { _id: stationId },
-                        { $set: { paused: true, pausedAt: Date.now() } },
-                        next
-                    );
-                },
-
-                (res, next) => {
-                    stations
-                        .runJob("UPDATE_STATION", { stationId })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-            ],
-            async (err) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "STATIONS_PAUSE",
-                        `Pausing station "${stationId}" failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "STATIONS_PAUSE",
-                    `Paused station "${stationId}" successfully.`
-                );
-                cache.runJob("PUB", {
-                    channel: "station.pause",
-                    value: stationId,
-                });
-                notifications.runJob("UNSCHEDULE", {
-                    name: `stations.nextSong?id=${stationId}`,
-                });
-                return cb({
-                    status: "success",
-                    message: "Successfully paused.",
-                });
-            }
-        );
-    }),
-
-    /**
-     * Resumes a station
-     *
-     * @param session
-     * @param stationId - the station id
-     * @param cb
-     */
-    resume: hooks.ownerRequired(async (session, stationId, cb) => {
-        const stationModel = await db.runJob("GET_MODEL", {
-            modelName: "station",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    stations
-                        .runJob("GET_STATION", { stationId })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-
-                (station, next) => {
-                    if (!station) return next("Station not found.");
-                    if (!station.paused)
-                        return next("That station is not paused.");
-                    station.timePaused += Date.now() - station.pausedAt;
-                    stationModel.updateOne(
-                        { _id: stationId },
-                        {
-                            $set: { paused: false },
-                            $inc: { timePaused: Date.now() - station.pausedAt },
-                        },
-                        next
-                    );
-                },
-
-                (res, next) => {
-                    stations
-                        .runJob("UPDATE_STATION", { stationId })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-            ],
-            async (err) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "STATIONS_RESUME",
-                        `Resuming station "${stationId}" failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "STATIONS_RESUME",
-                    `Resuming station "${stationId}" successfully.`
-                );
-                cache.runJob("PUB", {
-                    channel: "station.resume",
-                    value: stationId,
-                });
-                return cb({
-                    status: "success",
-                    message: "Successfully resumed.",
-                });
-            }
-        );
-    }),
-
-    /**
-     * Removes a station
-     *
-     * @param session
-     * @param stationId - the station id
-     * @param cb
-     */
-    remove: hooks.ownerRequired(async (session, stationId, cb) => {
-        const stationModel = await db.runJob("GET_MODEL", {
-            modelName: "station",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    stationModel.deleteOne({ _id: stationId }, (err) =>
-                        next(err)
-                    );
-                },
-
-                (next) => {
-                    cache
-                        .runJob("HDEL", { table: "stations", key: stationId })
-                        .then(next)
-                        .catch(next);
-                },
-            ],
-            async (err) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "STATIONS_REMOVE",
-                        `Removing station "${stationId}" failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "STATIONS_REMOVE",
-                    `Removing station "${stationId}" successfully.`
-                );
-                cache.runJob("PUB", {
-                    channel: "station.remove",
-                    value: stationId,
-                });
-                activities.runJob("ADD_ACTIVITY", {
-                    userId: session.userId,
-                    activityType: "deleted_station",
-                    payload: [stationId],
-                });
-                return cb({
-                    status: "success",
-                    message: "Successfully removed.",
-                });
-            }
-        );
-    }),
-
-    /**
-     * Create a station
-     *
-     * @param session
-     * @param data - the station data
-     * @param cb
-     */
-    create: hooks.loginRequired(async (session, data, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        const stationModel = await db.runJob("GET_MODEL", {
-            modelName: "station",
-        });
-
-        data.name = data.name.toLowerCase();
-        let blacklist = [
-            "country",
-            "edm",
-            "musare",
-            "hip-hop",
-            "rap",
-            "top-hits",
-            "todays-hits",
-            "old-school",
-            "christmas",
-            "about",
-            "support",
-            "staff",
-            "help",
-            "news",
-            "terms",
-            "privacy",
-            "profile",
-            "c",
-            "community",
-            "tos",
-            "login",
-            "register",
-            "p",
-            "official",
-            "o",
-            "trap",
-            "faq",
-            "team",
-            "donate",
-            "buy",
-            "shop",
-            "forums",
-            "explore",
-            "settings",
-            "admin",
-            "auth",
-            "reset_password",
-        ];
-        async.waterfall(
-            [
-                (next) => {
-                    if (!data) return next("Invalid data.");
-                    next();
-                },
-
-                (next) => {
-                    stationModel.findOne(
-                        {
-                            $or: [
-                                { name: data.name },
-                                {
-                                    displayName: new RegExp(
-                                        `^${data.displayName}$`,
-                                        "i"
-                                    ),
-                                },
-                            ],
-                        },
-                        next
-                    );
-                },
-
-                (station, next) => {
-                    if (station)
-                        return next(
-                            "A station with that name or display name already exists."
-                        );
-                    const {
-                        name,
-                        displayName,
-                        description,
-                        genres,
-                        playlist,
-                        type,
-                        blacklistedGenres,
-                    } = data;
-                    if (type === "official") {
-                        userModel.findOne(
-                            { _id: session.userId },
-                            (err, user) => {
-                                if (err) return next(err);
-                                if (!user) return next("User not found.");
-                                if (user.role !== "admin")
-                                    return next("Admin required.");
-                                stationModel.create(
-                                    {
-                                        name,
-                                        displayName,
-                                        description,
-                                        type,
-                                        privacy: "private",
-                                        playlist,
-                                        genres,
-                                        blacklistedGenres,
-                                        currentSong: stations.defaultSong,
-                                    },
-                                    next
-                                );
-                            }
-                        );
-                    } else if (type === "community") {
-                        if (blacklist.indexOf(name) !== -1)
-                            return next(
-                                "That name is blacklisted. Please use a different name."
-                            );
-                        stationModel.create(
-                            {
-                                name,
-                                displayName,
-                                description,
-                                type,
-                                privacy: "private",
-                                owner: session.userId,
-                                queue: [],
-                                currentSong: null,
-                            },
-                            next
-                        );
-                    }
-                },
-            ],
-            async (err, station) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "STATIONS_CREATE",
-                        `Creating station failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "STATIONS_CREATE",
-                    `Created station "${station._id}" successfully.`
-                );
-                cache.runJob("PUB", {
-                    channel: "station.create",
-                    value: station._id,
-                });
-                activities.runJob("ADD_ACTIVITY", {
-                    userId: session.userId,
-                    activityType: "created_station",
-                    payload: [station._id],
-                });
-                return cb({
-                    status: "success",
-                    message: "Successfully created station.",
-                });
-            }
-        );
-    }),
-
-    /**
-     * Adds song to station queue
-     *
-     * @param session
-     * @param stationId - the station id
-     * @param songId - the song id
-     * @param cb
-     */
-    addToQueue: hooks.loginRequired(async (session, stationId, songId, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        const stationModel = await db.runJob("GET_MODEL", {
-            modelName: "station",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    stations
-                        .runJob("GET_STATION", { stationId })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-
-                (station, next) => {
-                    if (!station) return next("Station not found.");
-                    if (station.locked) {
-                        userModel.findOne(
-                            { _id: session.userId },
-                            (err, user) => {
-                                if (
-                                    user.role !== "admin" &&
-                                    station.owner !== session.userId
-                                )
-                                    return next(
-                                        "Only owners and admins can add songs to a locked queue."
-                                    );
-                                else return next(null, station);
-                            }
-                        );
-                    } else {
-                        return next(null, station);
-                    }
-                },
-
-                (station, next) => {
-                    if (station.type !== "community")
-                        return next("That station is not a community station.");
-                    stations
-                        .runJob("CAN_USER_VIEW_STATION", {
-                            station,
-                            userId: session.userId,
-                        })
-                        .then((canView) => {
-                            if (canView) return next(null, station);
-                            return next("Insufficient permissions.");
-                        })
-                        .catch((err) => {
-                            return next(err);
-                        });
-                },
-
-                (station, next) => {
-                    if (
-                        station.currentSong &&
-                        station.currentSong.songId === songId
-                    )
-                        return next("That song is currently playing.");
-                    async.each(
-                        station.queue,
-                        (queueSong, next) => {
-                            if (queueSong.songId === songId)
-                                return next(
-                                    "That song is already in the queue."
-                                );
-                            next();
-                        },
-                        (err) => {
-                            next(err, station);
-                        }
-                    );
-                },
-
-                (station, next) => {
-                    // songs
-                    //     .runJob("GET_SONG", { id: songId })
-                    //     .then((song) => {
-                    //         if (song) return next(null, song, station);
-                    //         else {
-                    utils
-                        .runJob("GET_SONG_FROM_YOUTUBE", { songId })
-                        .then((response) => {
-                            const song = response.song;
-                            song.artists = [];
-                            song.skipDuration = 0;
-                            song.likes = -1;
-                            song.dislikes = -1;
-                            song.thumbnail = "empty";
-                            song.explicit = false;
-                            next(null, song, station);
-                        })
-                        .catch((err) => {
-                            next(err);
-                        });
-                    //     }
-                    // })
-                    // .catch((err) => {
-                    //     next(err);
-                    // });
-                },
-
-                (song, station, next) => {
-                    let queue = station.queue;
-                    song.requestedBy = session.userId;
-                    queue.push(song);
-
-                    let totalDuration = 0;
-                    queue.forEach((song) => {
-                        totalDuration += song.duration;
-                    });
-                    if (totalDuration >= 3600 * 3)
-                        return next("The max length of the queue is 3 hours.");
-                    next(null, song, station);
-                },
-
-                (song, station, next) => {
-                    let queue = station.queue;
-                    if (queue.length === 0) return next(null, song, station);
-                    let totalDuration = 0;
-                    const userId = queue[queue.length - 1].requestedBy;
-                    station.queue.forEach((song) => {
-                        if (userId === song.requestedBy) {
-                            totalDuration += song.duration;
-                        }
-                    });
-
-                    if (totalDuration >= 900)
-                        return next(
-                            "The max length of songs per user is 15 minutes."
-                        );
-                    next(null, song, station);
-                },
-
-                (song, station, next) => {
-                    let queue = station.queue;
-                    if (queue.length === 0) return next(null, song);
-                    let totalSongs = 0;
-                    const userId = queue[queue.length - 1].requestedBy;
-                    queue.forEach((song) => {
-                        if (userId === song.requestedBy) {
-                            totalSongs++;
-                        }
-                    });
-
-                    if (totalSongs <= 2) return next(null, song);
-                    if (totalSongs > 3)
-                        return next(
-                            "The max amount of songs per user is 3, and only 2 in a row is allowed."
-                        );
-                    if (
-                        queue[queue.length - 2].requestedBy !== userId ||
-                        queue[queue.length - 3] !== userId
-                    )
-                        return next(
-                            "The max amount of songs per user is 3, and only 2 in a row is allowed."
-                        );
-                    next(null, song);
-                },
-
-                (song, next) => {
-                    stationModel.updateOne(
-                        { _id: stationId },
-                        { $push: { queue: song } },
-                        { runValidators: true },
-                        next
-                    );
-                },
-
-                (res, next) => {
-                    stations
-                        .runJob("UPDATE_STATION", { stationId })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-            ],
-            async (err, station) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "STATIONS_ADD_SONG_TO_QUEUE",
-                        `Adding song "${songId}" to station "${stationId}" queue failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "STATIONS_ADD_SONG_TO_QUEUE",
-                    `Added song "${songId}" to station "${stationId}" successfully.`
-                );
-                cache.runJob("PUB", {
-                    channel: "station.queueUpdate",
-                    value: stationId,
-                });
-                return cb({
-                    status: "success",
-                    message: "Successfully added song to queue.",
-                });
-            }
-        );
-    }),
-
-    /**
-     * Removes song from station queue
-     *
-     * @param session
-     * @param stationId - the station id
-     * @param songId - the song id
-     * @param cb
-     */
-    removeFromQueue: hooks.ownerRequired(
-        async (session, stationId, songId, cb) => {
-            const stationModel = await db.runJob("GET_MODEL", {
-                modelName: "station",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        if (!songId) return next("Invalid song id.");
-                        stations
-                            .runJob("GET_STATION", { stationId })
-                            .then((station) => {
-                                next(null, station);
-                            })
-                            .catch(next);
-                    },
-
-                    (station, next) => {
-                        if (!station) return next("Station not found.");
-                        if (station.type !== "community")
-                            return next("Station is not a community station.");
-                        async.each(
-                            station.queue,
-                            (queueSong, next) => {
-                                if (queueSong.songId === songId)
-                                    return next(true);
-                                next();
-                            },
-                            (err) => {
-                                if (err === true) return next();
-                                next("Song is not currently in the queue.");
-                            }
-                        );
-                    },
-
-                    (next) => {
-                        stationModel.updateOne(
-                            { _id: stationId },
-                            { $pull: { queue: { songId: songId } } },
-                            next
-                        );
-                    },
-
-                    (res, next) => {
-                        stations
-                            .runJob("UPDATE_STATION", { stationId })
-                            .then((station) => {
-                                next(null, station);
-                            })
-                            .catch(next);
-                    },
-                ],
-                async (err, station) => {
-                    if (err) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "STATIONS_REMOVE_SONG_TO_QUEUE",
-                            `Removing song "${songId}" from station "${stationId}" queue failed. "${err}"`
-                        );
-                        return cb({ status: "failure", message: err });
-                    }
-                    console.log(
-                        "SUCCESS",
-                        "STATIONS_REMOVE_SONG_TO_QUEUE",
-                        `Removed song "${songId}" from station "${stationId}" successfully.`
-                    );
-                    cache.runJob("PUB", {
-                        channel: "station.queueUpdate",
-                        value: stationId,
-                    });
-                    return cb({
-                        status: "success",
-                        message: "Successfully removed song from queue.",
-                    });
-                }
-            );
-        }
-    ),
-
-    /**
-     * Gets the queue from a station
-     *
-     * @param session
-     * @param stationId - the station id
-     * @param cb
-     */
-    getQueue: (session, stationId, cb) => {
-        async.waterfall(
-            [
-                (next) => {
-                    stations
-                        .runJob("GET_STATION", { stationId })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-
-                (station, next) => {
-                    if (!station) return next("Station not found.");
-                    if (station.type !== "community")
-                        return next("Station is not a community station.");
-                    next(null, station);
-                },
-
-                (station, next) => {
-                    stations
-                        .runJob("CAN_USER_VIEW_STATION", {
-                            station,
-                            userId: session.userId,
-                        })
-                        .then((canView) => {
-                            if (canView) return next(null, station);
-                            return next("Insufficient permissions.");
-                        })
-                        .catch((err) => {
-                            return next(err);
-                        });
-                },
-            ],
-            async (err, station) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "STATIONS_GET_QUEUE",
-                        `Getting queue for station "${stationId}" failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "STATIONS_GET_QUEUE",
-                    `Got queue for station "${stationId}" successfully.`
-                );
-                return cb({
-                    status: "success",
-                    message: "Successfully got queue.",
-                    queue: station.queue,
-                });
-            }
-        );
-    },
-
-    /**
-     * Selects a private playlist for a station
-     *
-     * @param session
-     * @param stationId - the station id
-     * @param playlistId - the private playlist id
-     * @param cb
-     */
-    selectPrivatePlaylist: hooks.ownerRequired(
-        async (session, stationId, playlistId, cb) => {
-            const stationModel = await db.runJob("GET_MODEL", {
-                modelName: "station",
-            });
-            const playlistModel = await db.runJob("GET_MODEL", {
-                modelName: "playlist",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        stations
-                            .runJob("GET_STATION", { stationId })
-                            .then((station) => {
-                                next(null, station);
-                            })
-                            .catch(next);
-                    },
-
-                    (station, next) => {
-                        if (!station) return next("Station not found.");
-                        if (station.type !== "community")
-                            return next("Station is not a community station.");
-                        if (station.privatePlaylist === playlistId)
-                            return next(
-                                "That private playlist is already selected."
-                            );
-                        playlistModel.findOne({ _id: playlistId }, next);
-                    },
-
-                    (playlist, next) => {
-                        if (!playlist) return next("Playlist not found.");
-                        let currentSongIndex =
-                            playlist.songs.length > 0
-                                ? playlist.songs.length - 1
-                                : 0;
-                        stationModel.updateOne(
-                            { _id: stationId },
-                            {
-                                $set: {
-                                    privatePlaylist: playlistId,
-                                    currentSongIndex: currentSongIndex,
-                                },
-                            },
-                            { runValidators: true },
-                            next
-                        );
-                    },
-
-                    (res, next) => {
-                        stations
-                            .runJob("UPDATE_STATION", { stationId })
-                            .then((station) => {
-                                next(null, station);
-                            })
-                            .catch(next);
-                    },
-                ],
-                async (err, station) => {
-                    if (err) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "STATIONS_SELECT_PRIVATE_PLAYLIST",
-                            `Selecting private playlist "${playlistId}" for station "${stationId}" failed. "${err}"`
-                        );
-                        return cb({ status: "failure", message: err });
-                    }
-                    console.log(
-                        "SUCCESS",
-                        "STATIONS_SELECT_PRIVATE_PLAYLIST",
-                        `Selected private playlist "${playlistId}" for station "${stationId}" successfully.`
-                    );
-                    notifications.runJob("UNSCHEDULE", {
-                        name: `stations.nextSong?id${stationId}`,
-                    });
-                    if (!station.partyMode)
-                        stations.runJob("SKIP_STATION", { stationId });
-                    cache.runJob("PUB", {
-                        channel: "privatePlaylist.selected",
-                        value: {
-                            playlistId,
-                            stationId,
-                        },
-                    });
-                    return cb({
-                        status: "success",
-                        message: "Successfully selected playlist.",
-                    });
-                }
-            );
-        }
-    ),
-
-    favoriteStation: hooks.loginRequired(async (session, stationId, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        async.waterfall(
-            [
-                (next) => {
-                    stations
-                        .runJob("GET_STATION", { stationId })
-                        .then((station) => {
-                            next(null, station);
-                        })
-                        .catch(next);
-                },
-
-                (station, next) => {
-                    if (!station) return next("Station not found.");
-                    stations
-                        .runJob("CAN_USER_VIEW_STATION", {
-                            station,
-                            userId: session.userId,
-                        })
-                        .then((canView) => {
-                            if (canView) return next();
-                            return next("Insufficient permissions.");
-                        })
-                        .catch((err) => {
-                            return next(err);
-                        });
-                },
-
-                (next) => {
-                    userModel.updateOne(
-                        { _id: session.userId },
-                        { $addToSet: { favoriteStations: stationId } },
-                        next
-                    );
-                },
-
-                (res, next) => {
-                    if (res.nModified === 0)
-                        return next("The station was already favorited.");
-                    next();
-                },
-            ],
-            async (err) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "FAVORITE_STATION",
-                        `Favoriting station "${stationId}" failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "FAVORITE_STATION",
-                    `Favorited station "${stationId}" successfully.`
-                );
-                cache.runJob("PUB", {
-                    channel: "user.favoritedStation",
-                    value: {
-                        userId: session.userId,
-                        stationId,
-                    },
-                });
-                return cb({
-                    status: "success",
-                    message: "Succesfully favorited station.",
-                });
-            }
-        );
-    }),
-
-    unfavoriteStation: hooks.loginRequired(async (session, stationId, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        async.waterfall(
-            [
-                (next) => {
-                    userModel.updateOne(
-                        { _id: session.userId },
-                        { $pull: { favoriteStations: stationId } },
-                        next
-                    );
-                },
-
-                (res, next) => {
-                    if (res.nModified === 0)
-                        return next("The station wasn't favorited.");
-                    next();
-                },
-            ],
-            async (err) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "UNFAVORITE_STATION",
-                        `Unfavoriting station "${stationId}" failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "UNFAVORITE_STATION",
-                    `Unfavorited station "${stationId}" successfully.`
-                );
-                cache.runJob("PUB", {
-                    channel: "user.unfavoritedStation",
-                    value: {
-                        userId: session.userId,
-                        stationId,
-                    },
-                });
-                return cb({
-                    status: "success",
-                    message: "Succesfully unfavorited station.",
-                });
-            }
-        );
-    }),
+export default {
+	/**
+	 * Get a list of all the stations
+	 *
+	 * @param {object} session - user session
+	 * @param {Function} cb - callback
+	 */
+	index: (session, cb) => {
+		async.waterfall(
+			[
+				next => {
+					cache.runJob("HGETALL", { table: "stations" }).then(stations => {
+						next(null, stations);
+					});
+				},
+
+				(items, next) => {
+					const filteredStations = [];
+					async.each(
+						items,
+						(station, next) => {
+							async.waterfall(
+								[
+									next => {
+										stations
+											.runJob("CAN_USER_VIEW_STATION", {
+												station,
+												userId: session.userId,
+												hideUnlisted: true
+											})
+											.then(exists => {
+												next(null, exists);
+											})
+											.catch(next);
+									}
+								],
+								(err, exists) => {
+									if (err) console.log(err);
+									station.userCount = usersPerStationCount[station._id] || 0;
+									if (exists) filteredStations.push(station);
+									next();
+								}
+							);
+						},
+						() => next(null, filteredStations)
+					);
+				}
+			],
+			async (err, stations) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "STATIONS_INDEX", `Indexing stations failed. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "STATIONS_INDEX", `Indexing stations successful.`, false);
+				return cb({ status: "success", stations });
+			}
+		);
+	},
+
+	/**
+	 * Obtains basic metadata of a station in order to format an activity
+	 *
+	 * @param {object} session - user session
+	 * @param {string} stationId - the station id
+	 * @param {Function} cb - callback
+	 */
+	getStationForActivity: (session, stationId, cb) => {
+		async.waterfall(
+			[
+				next => {
+					stations
+						.runJob("GET_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				}
+			],
+			async (err, station) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"STATIONS_GET_STATION_FOR_ACTIVITY",
+						`Failed to obtain metadata of station ${stationId} for activity formatting. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"STATIONS_GET_STATION_FOR_ACTIVITY",
+					`Obtained metadata of station ${stationId} for activity formatting successfully.`
+				);
+				return cb({
+					status: "success",
+					data: {
+						title: station.displayName,
+						thumbnail: station.currentSong ? station.currentSong.thumbnail : ""
+					}
+				});
+			}
+		);
+	},
+
+	/**
+	 * Verifies that a station exists
+	 *
+	 * @param {object} session - user session
+	 * @param {string} stationName - the station name
+	 * @param {Function} cb - callback
+	 */
+	existsByName: (session, stationName, cb) => {
+		async.waterfall(
+			[
+				next => {
+					stations
+						.runJob("GET_STATION_BY_NAME", { stationName })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				},
+
+				(station, next) => {
+					if (!station) return next(null, false);
+					return stations
+						.runJob("CAN_USER_VIEW_STATION", {
+							station,
+							userId: session.userId
+						})
+						.then(exists => {
+							next(null, exists);
+						})
+						.catch(next);
+				}
+			],
+			async (err, exists) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"STATION_EXISTS_BY_NAME",
+						`Checking if station "${stationName}" exists failed. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"STATION_EXISTS_BY_NAME",
+					`Station "${stationName}" exists successfully.` /* , false */
+				);
+				return cb({ status: "success", exists });
+			}
+		);
+	},
+
+	/**
+	 * Gets the official playlist for a station
+	 *
+	 * @param {object} session - user session
+	 * @param {string} stationId - the station id
+	 * @param {Function} cb - callback
+	 */
+	getPlaylist: (session, stationId, cb) => {
+		async.waterfall(
+			[
+				next => {
+					stations
+						.runJob("GET_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				},
+
+				(station, next) => {
+					stations
+						.runJob("CAN_USER_VIEW_STATION", {
+							station,
+							userId: session.userId
+						})
+						.then(canView => {
+							if (canView) return next(null, station);
+							return next("Insufficient permissions.");
+						})
+						.catch(err => next(err));
+				},
+
+				(station, next) => {
+					if (!station) return next("Station not found.");
+					if (station.type !== "official") return next("This is not an official station.");
+					return next();
+				},
+
+				next => {
+					cache
+						.runJob("HGET", {
+							table: "officialPlaylists",
+							key: stationId
+						})
+						.then(playlist => {
+							next(null, playlist);
+						})
+						.catch(next);
+				},
+
+				(playlist, next) => {
+					if (!playlist) return next("Playlist not found.");
+					return next(null, playlist);
+				}
+			],
+			async (err, playlist) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"STATIONS_GET_PLAYLIST",
+						`Getting playlist for station "${stationId}" failed. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"STATIONS_GET_PLAYLIST",
+					`Got playlist for station "${stationId}" successfully.`,
+					false
+				);
+				return cb({ status: "success", data: playlist.songs });
+			}
+		);
+	},
+
+	/**
+	 * Joins the station by its name
+	 *
+	 * @param {object} session - user session
+	 * @param {string} stationName - the station name
+	 * @param {Function} cb - callback
+	 */
+	join: (session, stationName, cb) => {
+		async.waterfall(
+			[
+				next => {
+					stations
+						.runJob("GET_STATION_BY_NAME", { stationName })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				},
+
+				(station, next) => {
+					if (!station) return next("Station not found.");
+					return stations
+						.runJob("CAN_USER_VIEW_STATION", {
+							station,
+							userId: session.userId
+						})
+						.then(canView => {
+							if (!canView) next("Not allowed to join station.");
+							else next(null, station);
+						})
+						.catch(err => next(err));
+				},
+
+				(station, next) => {
+					utils.runJob("SOCKET_JOIN_ROOM", {
+						socketId: session.socketId,
+						room: `station.${station._id}`
+					});
+					const data = {
+						_id: station._id,
+						type: station.type,
+						currentSong: station.currentSong,
+						startedAt: station.startedAt,
+						paused: station.paused,
+						timePaused: station.timePaused,
+						pausedAt: station.pausedAt,
+						description: station.description,
+						displayName: station.displayName,
+						privacy: station.privacy,
+						locked: station.locked,
+						partyMode: station.partyMode,
+						owner: station.owner,
+						privatePlaylist: station.privatePlaylist
+					};
+					userList[session.socketId] = station._id;
+					next(null, data);
+				},
+
+				(data, next) => {
+					data = JSON.parse(JSON.stringify(data));
+					data.userCount = usersPerStationCount[data._id] || 0;
+					data.users = usersPerStation[data._id] || [];
+					if (!data.currentSong || !data.currentSong.title) return next(null, data);
+					utils.runJob("SOCKET_JOIN_SONG_ROOM", {
+						socketId: session.socketId,
+						room: `song.${data.currentSong.songId}`
+					});
+					data.currentSong.skipVotes = data.currentSong.skipVotes.length;
+					return songs
+						.runJob("GET_SONG_FROM_ID", {
+							songId: data.currentSong.songId
+						})
+						.then(response => {
+							const { song } = response;
+							if (song) {
+								data.currentSong.likes = song.likes;
+								data.currentSong.dislikes = song.dislikes;
+							} else {
+								data.currentSong.likes = -1;
+								data.currentSong.dislikes = -1;
+							}
+						})
+						.catch(() => {
+							data.currentSong.likes = -1;
+							data.currentSong.dislikes = -1;
+						})
+						.finally(() => {
+							next(null, data);
+						});
+				}
+			],
+			async (err, data) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "STATIONS_JOIN", `Joining station "${stationName}" failed. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "STATIONS_JOIN", `Joined station "${data._id}" successfully.`);
+				return cb({ status: "success", data });
+			}
+		);
+	},
+
+	/**
+	 * Toggles if a station is locked
+	 *
+	 * @param session
+	 * @param stationId - the station id
+	 * @param cb
+	 */
+	toggleLock: isOwnerRequired(async (session, stationId, cb) => {
+		const stationModel = await db.runJob("GET_MODEL", {
+			modelName: "station"
+		});
+		async.waterfall(
+			[
+				next => {
+					stations
+						.runJob("GET_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				},
+
+				(station, next) => {
+					stationModel.updateOne({ _id: stationId }, { $set: { locked: !station.locked } }, next);
+				},
+
+				(res, next) => {
+					stations
+						.runJob("UPDATE_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				}
+			],
+			async (err, station) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"STATIONS_UPDATE_LOCKED_STATUS",
+						`Toggling the queue lock for station "${stationId}" failed. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"STATIONS_UPDATE_LOCKED_STATUS",
+					`Toggled the queue lock for station "${stationId}" successfully to "${station.locked}".`
+				);
+				cache.runJob("PUB", {
+					channel: "station.queueLockToggled",
+					value: {
+						stationId,
+						locked: station.locked
+					}
+				});
+				return cb({ status: "success", data: station.locked });
+			}
+		);
+	}),
+
+	/**
+	 * Votes to skip a station
+	 *
+	 * @param session
+	 * @param stationId - the station id
+	 * @param cb
+	 */
+	voteSkip: isLoginRequired(async (session, stationId, cb) => {
+		const stationModel = await db.runJob("GET_MODEL", {
+			modelName: "station"
+		});
+
+		let skipVotes = 0;
+		let shouldSkip = false;
+
+		async.waterfall(
+			[
+				next => {
+					stations
+						.runJob("GET_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				},
+
+				(station, next) => {
+					if (!station) return next("Station not found.");
+					return stations
+						.runJob("CAN_USER_VIEW_STATION", {
+							station,
+							userId: session.userId
+						})
+						.then(canView => {
+							if (canView) return next(null, station);
+							return next("Insufficient permissions.");
+						})
+						.catch(err => next(err));
+				},
+
+				(station, next) => {
+					if (!station.currentSong) return next("There is currently no song to skip.");
+					if (station.currentSong.skipVotes.indexOf(session.userId) !== -1)
+						return next("You have already voted to skip this song.");
+					return next(null, station);
+				},
+
+				(station, next) => {
+					stationModel.updateOne(
+						{ _id: stationId },
+						{ $push: { "currentSong.skipVotes": session.userId } },
+						next
+					);
+				},
+
+				(res, next) => {
+					stations
+						.runJob("UPDATE_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				},
+
+				(station, next) => {
+					if (!station) return next("Station not found.");
+					return next(null, station);
+				},
+
+				(station, next) => {
+					skipVotes = station.currentSong.skipVotes.length;
+					utils
+						.runJob("GET_ROOM_SOCKETS", {
+							room: `station.${stationId}`
+						})
+						.then(sockets => {
+							next(null, sockets);
+						})
+						.catch(next);
+				},
+
+				(sockets, next) => {
+					if (sockets.length <= skipVotes) shouldSkip = true;
+					next();
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "STATIONS_VOTE_SKIP", `Vote skipping station "${stationId}" failed. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "STATIONS_VOTE_SKIP", `Vote skipping "${stationId}" successful.`);
+				cache.runJob("PUB", {
+					channel: "station.voteSkipSong",
+					value: stationId
+				});
+
+				if (shouldSkip) stations.runJob("SKIP_STATION", { stationId });
+
+				return cb({
+					status: "success",
+					message: "Successfully voted to skip the song."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Force skips a station
+	 *
+	 * @param session
+	 * @param stationId - the station id
+	 * @param cb
+	 */
+	forceSkip: isOwnerRequired((session, stationId, cb) => {
+		async.waterfall(
+			[
+				next => {
+					stations
+						.runJob("GET_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				},
+
+				(station, next) => {
+					if (!station) return next("Station not found.");
+					return next();
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"STATIONS_FORCE_SKIP",
+						`Force skipping station "${stationId}" failed. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				notifications.runJob("UNSCHEDULE", {
+					name: `stations.nextSong?id=${stationId}`
+				});
+				stations.runJob("SKIP_STATION", { stationId });
+				console.log("SUCCESS", "STATIONS_FORCE_SKIP", `Force skipped station "${stationId}" successfully.`);
+				return cb({
+					status: "success",
+					message: "Successfully skipped station."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Leaves the user's current station
+	 *
+	 * @param {object} session - user session
+	 * @param {string} stationId - id of station to leave
+	 * @param {Function} cb - callback
+	 */
+	leave: (session, stationId, cb) => {
+		async.waterfall(
+			[
+				next => {
+					stations
+						.runJob("GET_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				},
+
+				(station, next) => {
+					if (!station) return next("Station not found.");
+					return next();
+				}
+			],
+			async (err, userCount) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "STATIONS_LEAVE", `Leaving station "${stationId}" failed. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "STATIONS_LEAVE", `Left station "${stationId}" successfully.`);
+				utils.runJob("SOCKET_LEAVE_ROOMS", { socketId: session });
+				delete userList[session.socketId];
+				return cb({
+					status: "success",
+					message: "Successfully left station.",
+					userCount
+				});
+			}
+		);
+	},
+
+	/**
+	 * Updates a station's name
+	 *
+	 * @param session
+	 * @param stationId - the station id
+	 * @param newName - the new station name
+	 * @param cb
+	 */
+	updateName: isOwnerRequired(async (session, stationId, newName, cb) => {
+		const stationModel = await db.runJob("GET_MODEL", {
+			modelName: "station"
+		});
+		async.waterfall(
+			[
+				next => {
+					stationModel.updateOne(
+						{ _id: stationId },
+						{ $set: { name: newName } },
+						{ runValidators: true },
+						next
+					);
+				},
+
+				(res, next) => {
+					stations
+						.runJob("UPDATE_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"STATIONS_UPDATE_NAME",
+						`Updating station "${stationId}" name to "${newName}" failed. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"STATIONS_UPDATE_NAME",
+					`Updated station "${stationId}" name to "${newName}" successfully.`
+				);
+				return cb({
+					status: "success",
+					message: "Successfully updated the name."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Updates a station's display name
+	 *
+	 * @param session
+	 * @param stationId - the station id
+	 * @param newDisplayName - the new station display name
+	 * @param cb
+	 */
+	updateDisplayName: isOwnerRequired(async (session, stationId, newDisplayName, cb) => {
+		const stationModel = await db.runJob("GET_MODEL", {
+			modelName: "station"
+		});
+
+		async.waterfall(
+			[
+				next => {
+					stationModel.updateOne(
+						{ _id: stationId },
+						{ $set: { displayName: newDisplayName } },
+						{ runValidators: true },
+						next
+					);
+				},
+
+				(res, next) => {
+					stations
+						.runJob("UPDATE_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"STATIONS_UPDATE_DISPLAY_NAME",
+						`Updating station "${stationId}" displayName to "${newDisplayName}" failed. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"STATIONS_UPDATE_DISPLAY_NAME",
+					`Updated station "${stationId}" displayName to "${newDisplayName}" successfully.`
+				);
+				return cb({
+					status: "success",
+					message: "Successfully updated the display name."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Updates a station's description
+	 *
+	 * @param session
+	 * @param stationId - the station id
+	 * @param newDescription - the new station description
+	 * @param cb
+	 */
+	updateDescription: isOwnerRequired(async (session, stationId, newDescription, cb) => {
+		const stationModel = await db.runJob("GET_MODEL", {
+			modelName: "station"
+		});
+
+		async.waterfall(
+			[
+				next => {
+					stationModel.updateOne(
+						{ _id: stationId },
+						{ $set: { description: newDescription } },
+						{ runValidators: true },
+						next
+					);
+				},
+
+				(res, next) => {
+					stations
+						.runJob("UPDATE_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"STATIONS_UPDATE_DESCRIPTION",
+						`Updating station "${stationId}" description to "${newDescription}" failed. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"STATIONS_UPDATE_DESCRIPTION",
+					`Updated station "${stationId}" description to "${newDescription}" successfully.`
+				);
+				return cb({
+					status: "success",
+					message: "Successfully updated the description."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Updates a station's privacy
+	 *
+	 * @param session
+	 * @param stationId - the station id
+	 * @param newPrivacy - the new station privacy
+	 * @param cb
+	 */
+	updatePrivacy: isOwnerRequired(async (session, stationId, newPrivacy, cb) => {
+		const stationModel = await db.runJob("GET_MODEL", {
+			modelName: "station"
+		});
+		async.waterfall(
+			[
+				next => {
+					stationModel.updateOne(
+						{ _id: stationId },
+						{ $set: { privacy: newPrivacy } },
+						{ runValidators: true },
+						next
+					);
+				},
+
+				(res, next) => {
+					stations
+						.runJob("UPDATE_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"STATIONS_UPDATE_PRIVACY",
+						`Updating station "${stationId}" privacy to "${newPrivacy}" failed. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"STATIONS_UPDATE_PRIVACY",
+					`Updated station "${stationId}" privacy to "${newPrivacy}" successfully.`
+				);
+				return cb({
+					status: "success",
+					message: "Successfully updated the privacy."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Updates a station's genres
+	 *
+	 * @param session
+	 * @param stationId - the station id
+	 * @param newGenres - the new station genres
+	 * @param cb
+	 */
+	updateGenres: isOwnerRequired(async (session, stationId, newGenres, cb) => {
+		const stationModel = await db.runJob("GET_MODEL", {
+			modelName: "station"
+		});
+		async.waterfall(
+			[
+				next => {
+					stationModel.updateOne(
+						{ _id: stationId },
+						{ $set: { genres: newGenres } },
+						{ runValidators: true },
+						next
+					);
+				},
+
+				(res, next) => {
+					stations
+						.runJob("UPDATE_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"STATIONS_UPDATE_GENRES",
+						`Updating station "${stationId}" genres to "${newGenres}" failed. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"STATIONS_UPDATE_GENRES",
+					`Updated station "${stationId}" genres to "${newGenres}" successfully.`
+				);
+				return cb({
+					status: "success",
+					message: "Successfully updated the genres."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Updates a station's blacklisted genres
+	 *
+	 * @param session
+	 * @param stationId - the station id
+	 * @param newBlacklistedGenres - the new station blacklisted genres
+	 * @param cb
+	 */
+	updateBlacklistedGenres: isOwnerRequired(async (session, stationId, newBlacklistedGenres, cb) => {
+		const stationModel = await db.runJob("GET_MODEL", {
+			modelName: "station"
+		});
+		async.waterfall(
+			[
+				next => {
+					stationModel.updateOne(
+						{ _id: stationId },
+						{
+							$set: {
+								blacklistedGenres: newBlacklistedGenres
+							}
+						},
+						{ runValidators: true },
+						next
+					);
+				},
+
+				(res, next) => {
+					stations
+						.runJob("UPDATE_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"STATIONS_UPDATE_BLACKLISTED_GENRES",
+						`Updating station "${stationId}" blacklisted genres to "${newBlacklistedGenres}" failed. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"STATIONS_UPDATE_BLACKLISTED_GENRES",
+					`Updated station "${stationId}" blacklisted genres to "${newBlacklistedGenres}" successfully.`
+				);
+				return cb({
+					status: "success",
+					message: "Successfully updated the blacklisted genres."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Updates a station's party mode
+	 *
+	 * @param session
+	 * @param stationId - the station id
+	 * @param newPartyMode - the new station party mode
+	 * @param cb
+	 */
+	updatePartyMode: isOwnerRequired(async (session, stationId, newPartyMode, cb) => {
+		const stationModel = await db.runJob("GET_MODEL", {
+			modelName: "station"
+		});
+		async.waterfall(
+			[
+				next => {
+					stations
+						.runJob("GET_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				},
+
+				(station, next) => {
+					if (!station) return next("Station not found.");
+					if (station.partyMode === newPartyMode)
+						return next(`The party mode was already ${newPartyMode ? "enabled." : "disabled."}`);
+					return stationModel.updateOne(
+						{ _id: stationId },
+						{ $set: { partyMode: newPartyMode } },
+						{ runValidators: true },
+						next
+					);
+				},
+
+				(res, next) => {
+					stations
+						.runJob("UPDATE_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"STATIONS_UPDATE_PARTY_MODE",
+						`Updating station "${stationId}" party mode to "${newPartyMode}" failed. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"STATIONS_UPDATE_PARTY_MODE",
+					`Updated station "${stationId}" party mode to "${newPartyMode}" successfully.`
+				);
+				cache.runJob("PUB", {
+					channel: "station.updatePartyMode",
+					value: {
+						stationId,
+						partyMode: newPartyMode
+					}
+				});
+				stations.runJob("SKIP_STATION", { stationId });
+				return cb({
+					status: "success",
+					message: "Successfully updated the party mode."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Pauses a station
+	 *
+	 * @param session
+	 * @param stationId - the station id
+	 * @param cb
+	 */
+	pause: isOwnerRequired(async (session, stationId, cb) => {
+		const stationModel = await db.runJob("GET_MODEL", {
+			modelName: "station"
+		});
+		async.waterfall(
+			[
+				next => {
+					stations
+						.runJob("GET_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				},
+
+				(station, next) => {
+					if (!station) return next("Station not found.");
+					if (station.paused) return next("That station was already paused.");
+					return stationModel.updateOne(
+						{ _id: stationId },
+						{ $set: { paused: true, pausedAt: Date.now() } },
+						next
+					);
+				},
+
+				(res, next) => {
+					stations
+						.runJob("UPDATE_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "STATIONS_PAUSE", `Pausing station "${stationId}" failed. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "STATIONS_PAUSE", `Paused station "${stationId}" successfully.`);
+				cache.runJob("PUB", {
+					channel: "station.pause",
+					value: stationId
+				});
+				notifications.runJob("UNSCHEDULE", {
+					name: `stations.nextSong?id=${stationId}`
+				});
+				return cb({
+					status: "success",
+					message: "Successfully paused."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Resumes a station
+	 *
+	 * @param session
+	 * @param stationId - the station id
+	 * @param cb
+	 */
+	resume: isOwnerRequired(async (session, stationId, cb) => {
+		const stationModel = await db.runJob("GET_MODEL", {
+			modelName: "station"
+		});
+		async.waterfall(
+			[
+				next => {
+					stations
+						.runJob("GET_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				},
+
+				(station, next) => {
+					if (!station) return next("Station not found.");
+					if (!station.paused) return next("That station is not paused.");
+					station.timePaused += Date.now() - station.pausedAt;
+					return stationModel.updateOne(
+						{ _id: stationId },
+						{
+							$set: { paused: false },
+							$inc: { timePaused: Date.now() - station.pausedAt }
+						},
+						next
+					);
+				},
+
+				(res, next) => {
+					stations
+						.runJob("UPDATE_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "STATIONS_RESUME", `Resuming station "${stationId}" failed. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "STATIONS_RESUME", `Resuming station "${stationId}" successfully.`);
+				cache.runJob("PUB", {
+					channel: "station.resume",
+					value: stationId
+				});
+				return cb({
+					status: "success",
+					message: "Successfully resumed."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Removes a station
+	 *
+	 * @param session
+	 * @param stationId - the station id
+	 * @param cb
+	 */
+	remove: isOwnerRequired(async (session, stationId, cb) => {
+		const stationModel = await db.runJob("GET_MODEL", {
+			modelName: "station"
+		});
+
+		async.waterfall(
+			[
+				next => {
+					stationModel.deleteOne({ _id: stationId }, err => next(err));
+				},
+
+				next => {
+					cache.runJob("HDEL", { table: "stations", key: stationId }).then(next).catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "STATIONS_REMOVE", `Removing station "${stationId}" failed. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "STATIONS_REMOVE", `Removing station "${stationId}" successfully.`);
+				cache.runJob("PUB", {
+					channel: "station.remove",
+					value: stationId
+				});
+				activities.runJob("ADD_ACTIVITY", {
+					userId: session.userId,
+					activityType: "deleted_station",
+					payload: [stationId]
+				});
+				return cb({
+					status: "success",
+					message: "Successfully removed."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Create a station
+	 *
+	 * @param session
+	 * @param data - the station data
+	 * @param cb
+	 */
+	create: isLoginRequired(async (session, data, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		const stationModel = await db.runJob("GET_MODEL", {
+			modelName: "station"
+		});
+
+		data.name = data.name.toLowerCase();
+
+		const blacklist = [
+			"country",
+			"edm",
+			"musare",
+			"hip-hop",
+			"rap",
+			"top-hits",
+			"todays-hits",
+			"old-school",
+			"christmas",
+			"about",
+			"support",
+			"staff",
+			"help",
+			"news",
+			"terms",
+			"privacy",
+			"profile",
+			"c",
+			"community",
+			"tos",
+			"login",
+			"register",
+			"p",
+			"official",
+			"o",
+			"trap",
+			"faq",
+			"team",
+			"donate",
+			"buy",
+			"shop",
+			"forums",
+			"explore",
+			"settings",
+			"admin",
+			"auth",
+			"reset_password"
+		];
+		async.waterfall(
+			[
+				next => {
+					if (!data) return next("Invalid data.");
+					return next();
+				},
+
+				next => {
+					stationModel.findOne(
+						{
+							$or: [
+								{ name: data.name },
+								{
+									displayName: new RegExp(`^${data.displayName}$`, "i")
+								}
+							]
+						},
+						next
+					);
+				},
+
+				// eslint-disable-next-line consistent-return
+				(station, next) => {
+					console.log(station);
+
+					if (station) return next("A station with that name or display name already exists.");
+					const { name, displayName, description, genres, playlist, type, blacklistedGenres } = data;
+					if (type === "official") {
+						return userModel.findOne({ _id: session.userId }, (err, user) => {
+							if (err) return next(err);
+							if (!user) return next("User not found.");
+							if (user.role !== "admin") return next("Admin required.");
+							return stationModel.create(
+								{
+									name,
+									displayName,
+									description,
+									type,
+									privacy: "private",
+									playlist,
+									genres,
+									blacklistedGenres,
+									currentSong: stations.defaultSong
+								},
+								next
+							);
+						});
+					}
+					if (type === "community") {
+						if (blacklist.indexOf(name) !== -1)
+							return next("That name is blacklisted. Please use a different name.");
+						return stationModel.create(
+							{
+								name,
+								displayName,
+								description,
+								type,
+								privacy: "private",
+								owner: session.userId,
+								queue: [],
+								currentSong: null
+							},
+							next
+						);
+					}
+				}
+			],
+			async (err, station) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "STATIONS_CREATE", `Creating station failed. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "STATIONS_CREATE", `Created station "${station._id}" successfully.`);
+				cache.runJob("PUB", {
+					channel: "station.create",
+					value: station._id
+				});
+				activities.runJob("ADD_ACTIVITY", {
+					userId: session.userId,
+					activityType: "created_station",
+					payload: [station._id]
+				});
+				return cb({
+					status: "success",
+					message: "Successfully created station."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Adds song to station queue
+	 *
+	 * @param session
+	 * @param stationId - the station id
+	 * @param songId - the song id
+	 * @param cb
+	 */
+	addToQueue: isLoginRequired(async (session, stationId, songId, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		const stationModel = await db.runJob("GET_MODEL", {
+			modelName: "station"
+		});
+		async.waterfall(
+			[
+				next => {
+					stations
+						.runJob("GET_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				},
+
+				(station, next) => {
+					if (!station) return next("Station not found.");
+					if (station.locked) {
+						return userModel.findOne({ _id: session.userId }, (err, user) => {
+							if (user.role !== "admin" && station.owner !== session.userId)
+								return next("Only owners and admins can add songs to a locked queue.");
+							return next(null, station);
+						});
+					}
+					return next(null, station);
+				},
+
+				(station, next) => {
+					if (station.type !== "community") return next("That station is not a community station.");
+					return stations
+						.runJob("CAN_USER_VIEW_STATION", {
+							station,
+							userId: session.userId
+						})
+						.then(canView => {
+							if (canView) return next(null, station);
+							return next("Insufficient permissions.");
+						})
+						.catch(err => next(err));
+				},
+
+				(station, next) => {
+					if (station.currentSong && station.currentSong.songId === songId)
+						return next("That song is currently playing.");
+					return async.each(
+						station.queue,
+						(queueSong, next) => {
+							if (queueSong.songId === songId) return next("That song is already in the queue.");
+							return next();
+						},
+						err => next(err, station)
+					);
+				},
+
+				(station, next) => {
+					// songs
+					//     .runJob("GET_SONG", { id: songId })
+					//     .then((song) => {
+					//         if (song) return next(null, song, station);
+					//         else {
+					utils
+						.runJob("GET_SONG_FROM_YOUTUBE", { songId })
+						.then(response => {
+							const { song } = response;
+							song.artists = [];
+							song.skipDuration = 0;
+							song.likes = -1;
+							song.dislikes = -1;
+							song.thumbnail = "empty";
+							song.explicit = false;
+							next(null, song, station);
+						})
+						.catch(err => {
+							next(err);
+						});
+					//     }
+					// })
+					// .catch((err) => {
+					//     next(err);
+					// });
+				},
+
+				(song, station, next) => {
+					const { queue } = station;
+					song.requestedBy = session.userId;
+					queue.push(song);
+
+					let totalDuration = 0;
+					queue.forEach(song => {
+						totalDuration += song.duration;
+					});
+					if (totalDuration >= 3600 * 3) return next("The max length of the queue is 3 hours.");
+					return next(null, song, station);
+				},
+
+				(song, station, next) => {
+					const { queue } = station;
+					if (queue.length === 0) return next(null, song, station);
+					let totalDuration = 0;
+					const userId = queue[queue.length - 1].requestedBy;
+					station.queue.forEach(song => {
+						if (userId === song.requestedBy) {
+							totalDuration += song.duration;
+						}
+					});
+
+					if (totalDuration >= 900) return next("The max length of songs per user is 15 minutes.");
+					return next(null, song, station);
+				},
+
+				(song, station, next) => {
+					const { queue } = station;
+					if (queue.length === 0) return next(null, song);
+					let totalSongs = 0;
+					const userId = queue[queue.length - 1].requestedBy;
+					queue.forEach(song => {
+						if (userId === song.requestedBy) {
+							totalSongs += 1;
+						}
+					});
+
+					if (totalSongs <= 2) return next(null, song);
+					if (totalSongs > 3)
+						return next("The max amount of songs per user is 3, and only 2 in a row is allowed.");
+					if (queue[queue.length - 2].requestedBy !== userId || queue[queue.length - 3] !== userId)
+						return next("The max amount of songs per user is 3, and only 2 in a row is allowed.");
+
+					return next(null, song);
+				},
+
+				(song, next) => {
+					stationModel.updateOne(
+						{ _id: stationId },
+						{ $push: { queue: song } },
+						{ runValidators: true },
+						next
+					);
+				},
+
+				(res, next) => {
+					stations
+						.runJob("UPDATE_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"STATIONS_ADD_SONG_TO_QUEUE",
+						`Adding song "${songId}" to station "${stationId}" queue failed. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"STATIONS_ADD_SONG_TO_QUEUE",
+					`Added song "${songId}" to station "${stationId}" successfully.`
+				);
+				cache.runJob("PUB", {
+					channel: "station.queueUpdate",
+					value: stationId
+				});
+				return cb({
+					status: "success",
+					message: "Successfully added song to queue."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Removes song from station queue
+	 *
+	 * @param session
+	 * @param stationId - the station id
+	 * @param songId - the song id
+	 * @param cb
+	 */
+	removeFromQueue: isOwnerRequired(async (session, stationId, songId, cb) => {
+		const stationModel = await db.runJob("GET_MODEL", {
+			modelName: "station"
+		});
+		async.waterfall(
+			[
+				next => {
+					if (!songId) return next("Invalid song id.");
+					return stations
+						.runJob("GET_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				},
+
+				(station, next) => {
+					if (!station) return next("Station not found.");
+					if (station.type !== "community") return next("Station is not a community station.");
+					return async.each(
+						station.queue,
+						(queueSong, next) => {
+							if (queueSong.songId === songId) return next(true);
+							return next();
+						},
+						err => {
+							if (err === true) return next();
+							return next("Song is not currently in the queue.");
+						}
+					);
+				},
+
+				next => {
+					stationModel.updateOne({ _id: stationId }, { $pull: { queue: { songId } } }, next);
+				},
+
+				(res, next) => {
+					stations
+						.runJob("UPDATE_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"STATIONS_REMOVE_SONG_TO_QUEUE",
+						`Removing song "${songId}" from station "${stationId}" queue failed. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"STATIONS_REMOVE_SONG_TO_QUEUE",
+					`Removed song "${songId}" from station "${stationId}" successfully.`
+				);
+				cache.runJob("PUB", {
+					channel: "station.queueUpdate",
+					value: stationId
+				});
+				return cb({
+					status: "success",
+					message: "Successfully removed song from queue."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Gets the queue from a station
+	 *
+	 * @param {object} session - user session
+	 * @param {string} stationId - the station id
+	 * @param {Function} cb - callback
+	 */
+	getQueue: (session, stationId, cb) => {
+		async.waterfall(
+			[
+				next => {
+					stations
+						.runJob("GET_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				},
+
+				(station, next) => {
+					if (!station) return next("Station not found.");
+					if (station.type !== "community") return next("Station is not a community station.");
+					return next(null, station);
+				},
+
+				(station, next) => {
+					stations
+						.runJob("CAN_USER_VIEW_STATION", {
+							station,
+							userId: session.userId
+						})
+						.then(canView => {
+							if (canView) return next(null, station);
+							return next("Insufficient permissions.");
+						})
+						.catch(err => next(err));
+				}
+			],
+			async (err, station) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"STATIONS_GET_QUEUE",
+						`Getting queue for station "${stationId}" failed. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "STATIONS_GET_QUEUE", `Got queue for station "${stationId}" successfully.`);
+				return cb({
+					status: "success",
+					message: "Successfully got queue.",
+					queue: station.queue
+				});
+			}
+		);
+	},
+
+	/**
+	 * Selects a private playlist for a station
+	 *
+	 * @param session
+	 * @param stationId - the station id
+	 * @param playlistId - the private playlist id
+	 * @param cb
+	 */
+	selectPrivatePlaylist: isOwnerRequired(async (session, stationId, playlistId, cb) => {
+		const stationModel = await db.runJob("GET_MODEL", {
+			modelName: "station"
+		});
+		const playlistModel = await db.runJob("GET_MODEL", {
+			modelName: "playlist"
+		});
+		async.waterfall(
+			[
+				next => {
+					stations
+						.runJob("GET_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				},
+
+				(station, next) => {
+					if (!station) return next("Station not found.");
+					if (station.type !== "community") return next("Station is not a community station.");
+					if (station.privatePlaylist === playlistId)
+						return next("That private playlist is already selected.");
+					return playlistModel.findOne({ _id: playlistId }, next);
+				},
+
+				(playlist, next) => {
+					if (!playlist) return next("Playlist not found.");
+					const currentSongIndex = playlist.songs.length > 0 ? playlist.songs.length - 1 : 0;
+					return stationModel.updateOne(
+						{ _id: stationId },
+						{
+							$set: {
+								privatePlaylist: playlistId,
+								currentSongIndex
+							}
+						},
+						{ runValidators: true },
+						next
+					);
+				},
+
+				(res, next) => {
+					stations
+						.runJob("UPDATE_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				}
+			],
+			async (err, station) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"STATIONS_SELECT_PRIVATE_PLAYLIST",
+						`Selecting private playlist "${playlistId}" for station "${stationId}" failed. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log(
+					"SUCCESS",
+					"STATIONS_SELECT_PRIVATE_PLAYLIST",
+					`Selected private playlist "${playlistId}" for station "${stationId}" successfully.`
+				);
+				notifications.runJob("UNSCHEDULE", {
+					name: `stations.nextSong?id${stationId}`
+				});
+				if (!station.partyMode) stations.runJob("SKIP_STATION", { stationId });
+				cache.runJob("PUB", {
+					channel: "privatePlaylist.selected",
+					value: {
+						playlistId,
+						stationId
+					}
+				});
+				return cb({
+					status: "success",
+					message: "Successfully selected playlist."
+				});
+			}
+		);
+	}),
+
+	favoriteStation: isLoginRequired(async (session, stationId, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		async.waterfall(
+			[
+				next => {
+					stations
+						.runJob("GET_STATION", { stationId })
+						.then(station => {
+							next(null, station);
+						})
+						.catch(next);
+				},
+
+				(station, next) => {
+					if (!station) return next("Station not found.");
+					return stations
+						.runJob("CAN_USER_VIEW_STATION", {
+							station,
+							userId: session.userId
+						})
+						.then(canView => {
+							if (canView) return next();
+							return next("Insufficient permissions.");
+						})
+						.catch(err => next(err));
+				},
+
+				next => {
+					userModel.updateOne({ _id: session.userId }, { $addToSet: { favoriteStations: stationId } }, next);
+				},
+
+				(res, next) => {
+					if (res.nModified === 0) return next("The station was already favorited.");
+					return next();
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "FAVORITE_STATION", `Favoriting station "${stationId}" failed. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "FAVORITE_STATION", `Favorited station "${stationId}" successfully.`);
+				cache.runJob("PUB", {
+					channel: "user.favoritedStation",
+					value: {
+						userId: session.userId,
+						stationId
+					}
+				});
+				return cb({
+					status: "success",
+					message: "Succesfully favorited station."
+				});
+			}
+		);
+	}),
+
+	unfavoriteStation: isLoginRequired(async (session, stationId, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		async.waterfall(
+			[
+				next => {
+					userModel.updateOne({ _id: session.userId }, { $pull: { favoriteStations: stationId } }, next);
+				},
+
+				(res, next) => {
+					if (res.nModified === 0) return next("The station wasn't favorited.");
+					return next();
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "UNFAVORITE_STATION", `Unfavoriting station "${stationId}" failed. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "UNFAVORITE_STATION", `Unfavorited station "${stationId}" successfully.`);
+				cache.runJob("PUB", {
+					channel: "user.unfavoritedStation",
+					value: {
+						userId: session.userId,
+						stationId
+					}
+				});
+				return cb({
+					status: "success",
+					message: "Succesfully unfavorited station."
+				});
+			}
+		);
+	})
 };

+ 1852 - 2065
backend/logic/actions/users.js

@@ -1,2100 +1,1887 @@
-"use strict";
+import config from "config";
 
-const async = require("async");
-const config = require("config");
-const request = require("request");
-const bcrypt = require("bcrypt");
-const sha256 = require("sha256");
+import async from "async";
 
-const hooks = require("./hooks");
+import request from "request";
+import bcrypt from "bcrypt";
+import sha256 from "sha256";
+import { isAdminRequired, isLoginRequired } from "./hooks";
 
 // const moduleManager = require("../../index");
 
-const db = require("../db");
-const mail = require("../mail");
-const cache = require("../cache");
-const punishments = require("../punishments");
-const utils = require("../utils");
+import db from "../db";
+import utils from "../utils";
+import cache from "../cache";
+
+import mail from "../mail";
+import punishments from "../punishments";
 // const logger = require("../logger");
-const activities = require("../activities");
+import activities from "../activities";
 
 cache.runJob("SUB", {
-    channel: "user.updateUsername",
-    cb: (user) => {
-        utils.runJob("SOCKETS_FROM_USER", { userId: user._id }).then(response => {
-            response.sockets.forEach((socket) => {
-                socket.emit("event:user.username.changed", user.username);
-            });
-        });
-    },
+	channel: "user.updateUsername",
+	cb: user => {
+		utils.runJob("SOCKETS_FROM_USER", { userId: user._id }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("event:user.username.changed", user.username);
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "user.removeSessions",
-    cb: (userId) => {
-        utils.runJob("SOCKETS_FROM_USER_WITHOUT_CACHE", { userId }).then(response => {
-            response.sockets.forEach((socket) => {
-                socket.emit("keep.event:user.session.removed");
-            });
-        });
-    },
+	channel: "user.removeSessions",
+	cb: userId => {
+		utils.runJob("SOCKETS_FROM_USER_WITHOUT_CACHE", { userId }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("keep.event:user.session.removed");
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "user.linkPassword",
-    cb: (userId) => {
-        utils.runJob("SOCKETS_FROM_USER", { userId }).then(response => {
-            response.sockets.forEach((socket) => {
-                socket.emit("event:user.linkPassword");
-            });
-        });
-    },
+	channel: "user.linkPassword",
+	cb: userId => {
+		utils.runJob("SOCKETS_FROM_USER", { userId }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("event:user.linkPassword");
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "user.unlinkPassword",
-    cb: userId => {
-        utils.runJob("SOCKETS_FROM_USER", { userId }).then(response => {
-            response.sockets.forEach((socket) => {
-                socket.emit("event:user.unlinkPassword");
-            });
-        });
-    },
+	channel: "user.unlinkPassword",
+	cb: userId => {
+		utils.runJob("SOCKETS_FROM_USER", { userId }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("event:user.unlinkPassword");
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "user.linkGithub",
-    cb: (userId) => {
-        utils.runJob("SOCKETS_FROM_USER", { userId }).then(response => {
-            response.sockets.forEach((socket) => {
-                socket.emit("event:user.linkGithub");
-            });
-        });
-    },
+	channel: "user.linkGithub",
+	cb: userId => {
+		utils.runJob("SOCKETS_FROM_USER", { userId }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("event:user.linkGithub");
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "user.unlinkGithub",
-    cb: userId => {
-        utils.runJob("SOCKETS_FROM_USER", { userId }).then(response => {
-            response.sockets.forEach((socket) => {
-                socket.emit("event:user.unlinkGithub");
-            });
-        });
-    },
+	channel: "user.unlinkGithub",
+	cb: userId => {
+		utils.runJob("SOCKETS_FROM_USER", { userId }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("event:user.unlinkGithub");
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "user.ban",
-    cb: (data) => {
-        utils.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
-            response.sockets.forEach((socket) => {
-                socket.emit("keep.event:banned", data.punishment);
-                socket.disconnect(true);
-            });
-        });
-    },
+	channel: "user.ban",
+	cb: data => {
+		utils.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("keep.event:banned", data.punishment);
+				socket.disconnect(true);
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "user.favoritedStation",
-    cb: (data) => {
-        utils.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
-            response.sockets.forEach((socket) => {
-                socket.emit("event:user.favoritedStation", data.stationId);
-            });
-        });
-    },
+	channel: "user.favoritedStation",
+	cb: data => {
+		utils.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("event:user.favoritedStation", data.stationId);
+			});
+		});
+	}
 });
 
 cache.runJob("SUB", {
-    channel: "user.unfavoritedStation",
-    cb: (data) => {
-        utils.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
-            response.sockets.forEach((socket) => {
-                socket.emit(
-                    "event:user.unfavoritedStation",
-                    data.stationId
-                );
-            });
-        });
-    },
+	channel: "user.unfavoritedStation",
+	cb: data => {
+		utils.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("event:user.unfavoritedStation", data.stationId);
+			});
+		});
+	}
 });
 
-module.exports = {
-    /**
-     * Lists all Users
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {Function} cb - gets called with the result
-     */
-    index: hooks.adminRequired(async (session, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-
-        async.waterfall(
-            [
-                (next) => {
-                    userModel.find({}).exec(next);
-                },
-            ],
-            async (err, users) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "USER_INDEX",
-                        `Indexing users failed. "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "USER_INDEX",
-                        `Indexing users successful.`
-                    );
-                    let filteredUsers = [];
-                    users.forEach((user) => {
-                        filteredUsers.push({
-                            _id: user._id,
-                            username: user.username,
-                            role: user.role,
-                            liked: user.liked,
-                            disliked: user.disliked,
-                            songsRequested: user.statistics.songsRequested,
-                            email: {
-                                address: user.email.address,
-                                verified: user.email.verified,
-                            },
-                            hasPassword: !!user.services.password,
-                            services: { github: user.services.github },
-                        });
-                    });
-                    return cb({ status: "success", data: filteredUsers });
-                }
-            }
-        );
-    }),
-
-    /**
-     * Logs user in
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} identifier - the email of the user
-     * @param {String} password - the plaintext of the user
-     * @param {Function} cb - gets called with the result
-     */
-    login: async (session, identifier, password, cb) => {
-        identifier = identifier.toLowerCase();
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        const sessionSchema = await cache.runJob("GET_SCHEMA", {
-            schemaName: "session",
-        });
-
-        async.waterfall(
-            [
-                // check if a user with the requested identifier exists
-                (next) => {
-                    userModel.findOne(
-                        {
-                            $or: [{ "email.address": identifier }],
-                        },
-                        next
-                    );
-                },
-
-                // if the user doesn't exist, respond with a failure
-                // otherwise compare the requested password and the actual users password
-                (user, next) => {
-                    if (!user) return next("User not found");
-                    if (
-                        !user.services.password ||
-                        !user.services.password.password
-                    )
-                        return next(
-                            "The account you are trying to access uses GitHub to log in."
-                        );
-                    bcrypt.compare(
-                        sha256(password),
-                        user.services.password.password,
-                        (err, match) => {
-                            if (err) return next(err);
-                            if (!match) return next("Incorrect password");
-                            next(null, user);
-                        }
-                    );
-                },
-
-                (user, next) => {
-                    utils.runJob("GUID", {}).then((sessionId) => {
-                        next(null, user, sessionId);
-                    });
-                },
-
-                (user, sessionId, next) => {
-                    cache
-                        .runJob("HSET", {
-                            table: "sessions",
-                            key: sessionId,
-                            value: sessionSchema(sessionId, user._id),
-                        })
-                        .then(() => {
-                            next(null, sessionId);
-                        })
-                        .catch(next);
-                },
-            ],
-            async (err, sessionId) => {
-                if (err && err !== true) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "USER_PASSWORD_LOGIN",
-                        `Login failed with password for user "${identifier}". "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-                console.log(
-                    "SUCCESS",
-                    "USER_PASSWORD_LOGIN",
-                    `Login successful with password for user "${identifier}"`
-                );
-                cb({
-                    status: "success",
-                    message: "Login successful",
-                    user: {},
-                    SID: sessionId,
-                });
-            }
-        );
-    },
-
-    /**
-     * Registers a new user
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} username - the username for the new user
-     * @param {String} email - the email for the new user
-     * @param {String} password - the plaintext password for the new user
-     * @param {Object} recaptcha - the recaptcha data
-     * @param {Function} cb - gets called with the result
-     */
-    register: async function(
-        session,
-        username,
-        email,
-        password,
-        recaptcha,
-        cb
-    ) {
-        email = email.toLowerCase();
-        let verificationToken = await utils.runJob("GENERATE_RANDOM_STRING", {
-            length: 64,
-        });
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        const verifyEmailSchema = await mail.runJob("GET_SCHEMA", {
-            schemaName: "verifyEmail",
-        });
-
-        async.waterfall(
-            [
-                (next) => {
-                    if (config.get("registrationDisabled") === true)
-                        return next("Registration is not allowed at this time.");
-                    return next();
-                },
-
-                (next) => {
-                    if (!db.passwordValid(password))
-                        return next(
-                            "Invalid password. Check if it meets all the requirements."
-                        );
-                    return next();
-                },
-
-                // verify the request with google recaptcha
-                (next) => {
-                    if (config.get("apis.recaptcha.enabled") === true)
-                        request(
-                            {
-                                url:
-                                    "https://www.google.com/recaptcha/api/siteverify",
-                                method: "POST",
-                                form: {
-                                    secret: config.get("apis").recaptcha.secret,
-                                    response: recaptcha,
-                                },
-                            },
-                            next
-                        );
-                    else next(null, null, null);
-                },
-
-                // check if the response from Google recaptcha is successful
-                // if it is, we check if a user with the requested username already exists
-                (response, body, next) => {
-                    if (config.get("apis.recaptcha.enabled") === true) {
-                        let json = JSON.parse(body);
-                        if (json.success !== true)
-                            return next(
-                                "Response from recaptcha was not successful."
-                            );
-                    }
-                    userModel.findOne(
-                        { username: new RegExp(`^${username}$`, "i") },
-                        next
-                    );
-                },
-
-                // if the user already exists, respond with that
-                // otherwise check if a user with the requested email already exists
-                (user, next) => {
-                    if (user)
-                        return next(
-                            "A user with that username already exists."
-                        );
-                    userModel.findOne({ "email.address": email }, next);
-                },
-
-                // if the user already exists, respond with that
-                // otherwise, generate a salt to use with hashing the new users password
-                (user, next) => {
-                    if (user)
-                        return next("A user with that email already exists.");
-                    bcrypt.genSalt(10, next);
-                },
-
-                // hash the password
-                (salt, next) => {
-                    bcrypt.hash(sha256(password), salt, next);
-                },
-
-                (hash, next) => {
-                    utils
-                        .runJob("GENERATE_RANDOM_STRING", { length: 12 })
-                        .then((_id) => {
-                            next(null, hash, _id);
-                        });
-                },
-
-                // create the user object
-                (hash, _id, next) => {
-                    next(null, {
-                        _id,
-                        username,
-                        email: {
-                            address: email,
-                            verificationToken,
-                        },
-                        services: {
-                            password: {
-                                password: hash,
-                            },
-                        },
-                    });
-                },
-
-                // generate the url for gravatar avatar
-                (user, next) => {
-                    utils
-                        .runJob("CREATE_GRAVATAR", {
-                            email: user.email.address,
-                        })
-                        .then((url) => {
-                            user.avatar = url;
-                            next(null, user);
-                        });
-                },
-
-                // save the new user to the database
-                (user, next) => {
-                    userModel.create(user, next);
-                },
-
-                // respond with the new user
-                (newUser, next) => {
-                    verifyEmailSchema(
-                        email,
-                        username,
-                        verificationToken,
-                        () => {
-                            next(null, newUser);
-                        }
-                    );
-                },
-            ],
-            async (err, user) => {
-                if (err && err !== true) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "USER_PASSWORD_REGISTER",
-                        `Register failed with password for user "${username}"."${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                } else {
-                    module.exports.login(session, email, password, (result) => {
-                        let obj = {
-                            status: "success",
-                            message: "Successfully registered.",
-                        };
-                        if (result.status === "success") {
-                            obj.SID = result.SID;
-                        }
-                        activities.runJob("ADD_ACTIVITY", {
-                            userId: user._id,
-                            activityType: "created_account",
-                        });
-                        console.log(
-                            "SUCCESS",
-                            "USER_PASSWORD_REGISTER",
-                            `Register successful with password for user "${username}".`
-                        );
-                        return cb(obj);
-                    });
-                }
-            }
-        );
-    },
-
-    /**
-     * Logs out a user
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {Function} cb - gets called with the result
-     */
-    logout: (session, cb) => {
-        async.waterfall(
-            [
-                (next) => {
-                    cache
-                        .runJob("HGET", {
-                            table: "sessions",
-                            key: session.sessionId,
-                        })
-                        .then((session) => {
-                            next(null, session);
-                        })
-                        .catch(next);
-                },
-
-                (session, next) => {
-                    if (!session) return next("Session not found");
-                    next(null, session);
-                },
-
-                (session, next) => {
-                    cache
-                        .runJob("HDEL", {
-                            table: "sessions",
-                            key: session.sessionId,
-                        })
-                        .then(() => {
-                            next();
-                        })
-                        .catch(next);
-                },
-            ],
-            async (err) => {
-                if (err && err !== true) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "USER_LOGOUT",
-                        `Logout failed. "${err}" `
-                    );
-                    cb({ status: "failure", message: err });
-                } else {
-                    console.log("SUCCESS", "USER_LOGOUT", `Logout successful.`);
-                    cb({
-                        status: "success",
-                        message: "Successfully logged out.",
-                    });
-                }
-            }
-        );
-    },
-
-    /**
-     * Removes all sessions for a user
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} userId - the id of the user we are trying to delete the sessions of
-     * @param {Function} cb - gets called with the result
-     */
-    removeSessions: hooks.loginRequired(async (session, userId, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        async.waterfall(
-            [
-                (next) => {
-                    userModel.findOne({ _id: session.userId }, (err, user) => {
-                        if (err) return next(err);
-                        if (user.role !== "admin" && session.userId !== userId)
-                            return next(
-                                "Only admins and the owner of the account can remove their sessions."
-                            );
-                        else return next();
-                    });
-                },
-
-                (next) => {
-                    cache
-                        .runJob("HGETALL", { table: "sessions" })
-                        .then((sessions) => {
-                            next(null, sessions);
-                        })
-                        .catch(next);
-                },
-
-                (sessions, next) => {
-                    if (!sessions)
-                        return next(
-                            "There are no sessions for this user to remove."
-                        );
-                    else {
-                        let keys = Object.keys(sessions);
-                        next(null, keys, sessions);
-                    }
-                },
-
-                (keys, sessions, next) => {
-                    cache.runJob("PUB", {
-                        channel: "user.removeSessions",
-                        value: userId,
-                    });
-                    async.each(
-                        keys,
-                        (sessionId, callback) => {
-                            let session = sessions[sessionId];
-                            if (session.userId === userId) {
-                                cache
-                                    .runJob("HDEL", {
-                                        channel: "sessions",
-                                        key: sessionId,
-                                    })
-                                    .then(() => {
-                                        callback(null);
-                                    })
-                                    .catch(next);
-                            }
-                        },
-                        (err) => {
-                            next(err);
-                        }
-                    );
-                },
-            ],
-            async (err) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "REMOVE_SESSIONS_FOR_USER",
-                        `Couldn't remove all sessions for user "${userId}". "${err}"`
-                    );
-                    return cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "REMOVE_SESSIONS_FOR_USER",
-                        `Removed all sessions for user "${userId}".`
-                    );
-                    return cb({
-                        status: "success",
-                        message: "Successfully removed all sessions.",
-                    });
-                }
-            }
-        );
-    }),
-
-    /**
-     * Gets user object from username (only a few properties)
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} username - the username of the user we are trying to find
-     * @param {Function} cb - gets called with the result
-     */
-    findByUsername: async (session, username, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        async.waterfall(
-            [
-                (next) => {
-                    userModel.findOne(
-                        { username: new RegExp(`^${username}$`, "i") },
-                        next
-                    );
-                },
-
-                (account, next) => {
-                    if (!account) return next("User not found.");
-                    next(null, account);
-                },
-            ],
-            async (err, account) => {
-                if (err && err !== true) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "FIND_BY_USERNAME",
-                        `User not found for username "${username}". "${err}"`
-                    );
-                    cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "FIND_BY_USERNAME",
-                        `User found for username "${username}".`
-                    );
-                    return cb({
-                        status: "success",
-                        data: {
-                            _id: account._id,
-                            name: account.name,
-                            username: account.username,
-                            location: account.location,
-                            bio: account.bio,
-                            role: account.role,
-                            avatar: account.avatar,
-                            createdAt: account.createdAt,
-                        },
-                    });
-                }
-            }
-        );
-    },
-
-    /**
-     * Gets a username from an userId
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} userId - the userId of the person we are trying to get the username from
-     * @param {Function} cb - gets called with the result
-     */
-    getUsernameFromId: async (session, userId, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        userModel
-            .findById(userId)
-            .then((user) => {
-                if (user) {
-                    console.log(
-                        "SUCCESS",
-                        "GET_USERNAME_FROM_ID",
-                        `Found username for userId "${userId}".`
-                    );
-                    return cb({
-                        status: "success",
-                        data: user.username,
-                    });
-                } else {
-                    console.log(
-                        "ERROR",
-                        "GET_USERNAME_FROM_ID",
-                        `Getting the username from userId "${userId}" failed. User not found.`
-                    );
-                    cb({
-                        status: "failure",
-                        message: "Couldn't find the user.",
-                    });
-                }
-            })
-            .catch(async (err) => {
-                if (err && err !== true) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "GET_USERNAME_FROM_ID",
-                        `Getting the username from userId "${userId}" failed. "${err}"`
-                    );
-                    cb({ status: "failure", message: err });
-                }
-            });
-    },
-
-    //TODO Fix security issues
-    /**
-     * Gets user info from session
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {Function} cb - gets called with the result
-     */
-    findBySession: async (session, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        async.waterfall(
-            [
-                (next) => {
-                    cache
-                        .runJob("HGET", {
-                            table: "sessions",
-                            key: session.sessionId,
-                        })
-                        .then((session) => {
-                            next(null, session);
-                        })
-                        .catch(next);
-                },
-
-                (session, next) => {
-                    if (!session) return next("Session not found.");
-                    next(null, session);
-                },
-
-                (session, next) => {
-                    userModel.findOne({ _id: session.userId }, next);
-                },
-
-                (user, next) => {
-                    if (!user) return next("User not found.");
-                    next(null, user);
-                },
-            ],
-            async (err, user) => {
-                if (err && err !== true) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "FIND_BY_SESSION",
-                        `User not found. "${err}"`
-                    );
-                    cb({ status: "failure", message: err });
-                } else {
-                    let data = {
-                        email: {
-                            address: user.email.address,
-                        },
-                        avatar: user.avatar,
-                        username: user.username,
-                        name: user.name,
-                        location: user.location,
-                        bio: user.bio,
-                    };
-                    if (
-                        user.services.password &&
-                        user.services.password.password
-                    )
-                        data.password = true;
-                    if (user.services.github && user.services.github.id)
-                        data.github = true;
-                    console.log(
-                        "SUCCESS",
-                        "FIND_BY_SESSION",
-                        `User found. "${user.username}".`
-                    );
-                    return cb({
-                        status: "success",
-                        data,
-                    });
-                }
-            }
-        );
-    },
-
-    /**
-     * Updates a user's username
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} updatingUserId - the updating user's id
-     * @param {String} newUsername - the new username
-     * @param {Function} cb - gets called with the result
-     */
-    updateUsername: hooks.loginRequired(
-        async (session, updatingUserId, newUsername, cb) => {
-            const userModel = await db.runJob("GET_MODEL", {
-                modelName: "user",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        if (updatingUserId === session.userId)
-                            return next(null, true);
-                        userModel.findOne({ _id: session.userId }, next);
-                    },
-
-                    (user, next) => {
-                        if (user !== true && (!user || user.role !== "admin"))
-                            return next("Invalid permissions.");
-                        userModel.findOne({ _id: updatingUserId }, next);
-                    },
-
-                    (user, next) => {
-                        if (!user) return next("User not found.");
-                        if (user.username === newUsername)
-                            return next(
-                                "New username can't be the same as the old username."
-                            );
-                        next(null);
-                    },
-
-                    (next) => {
-                        userModel.findOne(
-                            { username: new RegExp(`^${newUsername}$`, "i") },
-                            next
-                        );
-                    },
-
-                    (user, next) => {
-                        if (!user) return next();
-                        if (user._id === updatingUserId) return next();
-                        next("That username is already in use.");
-                    },
-
-                    (next) => {
-                        userModel.updateOne(
-                            { _id: updatingUserId },
-                            { $set: { username: newUsername } },
-                            { runValidators: true },
-                            next
-                        );
-                    },
-                ],
-                async (err) => {
-                    if (err && err !== true) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "UPDATE_USERNAME",
-                            `Couldn't update username for user "${updatingUserId}" to username "${newUsername}". "${err}"`
-                        );
-                        cb({ status: "failure", message: err });
-                    } else {
-                        cache.runJob("PUB", {
-                            channel: "user.updateUsername",
-                            value: {
-                                username: newUsername,
-                                _id: updatingUserId,
-                            },
-                        });
-                        console.log(
-                            "SUCCESS",
-                            "UPDATE_USERNAME",
-                            `Updated username for user "${updatingUserId}" to username "${newUsername}".`
-                        );
-                        cb({
-                            status: "success",
-                            message: "Username updated successfully",
-                        });
-                    }
-                }
-            );
-        }
-    ),
-
-    /**
-     * Updates a user's email
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} updatingUserId - the updating user's id
-     * @param {String} newEmail - the new email
-     * @param {Function} cb - gets called with the result
-     */
-    updateEmail: hooks.loginRequired(
-        async (session, updatingUserId, newEmail, cb) => {
-            newEmail = newEmail.toLowerCase();
-            let verificationToken = await utils.runJob(
-                "GENERATE_RANDOM_STRING",
-                { length: 64 }
-            );
-            const userModel = await db.runJob("GET_MODEL", {
-                modelName: "user",
-            });
-            const verifyEmailSchema = await mail.runJob("GET_SCHEMA", {
-                schemaName: "verifyEmail",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        if (updatingUserId === session.userId)
-                            return next(null, true);
-                        userModel.findOne({ _id: session.userId }, next);
-                    },
-
-                    (user, next) => {
-                        if (user !== true && (!user || user.role !== "admin"))
-                            return next("Invalid permissions.");
-                        userModel.findOne({ _id: updatingUserId }, next);
-                    },
-
-                    (user, next) => {
-                        if (!user) return next("User not found.");
-                        if (user.email.address === newEmail)
-                            return next(
-                                "New email can't be the same as your the old email."
-                            );
-                        next();
-                    },
-
-                    (next) => {
-                        userModel.findOne({ "email.address": newEmail }, next);
-                    },
-
-                    (user, next) => {
-                        if (!user) return next();
-                        if (user._id === updatingUserId) return next();
-                        next("That email is already in use.");
-                    },
-
-                    // regenerate the url for gravatar avatar
-                    (next) => {
-                        utils
-                            .runJob("CREATE_GRAVATAR", { email: newEmail })
-                            .then((url) => {
-                                next(null, url);
-                            });
-                    },
-
-                    (avatar, next) => {
-                        userModel.updateOne(
-                            { _id: updatingUserId },
-                            {
-                                $set: {
-                                    avatar: avatar,
-                                    "email.address": newEmail,
-                                    "email.verified": false,
-                                    "email.verificationToken": verificationToken,
-                                },
-                            },
-                            { runValidators: true },
-                            next
-                        );
-                    },
-
-                    (res, next) => {
-                        userModel.findOne({ _id: updatingUserId }, next);
-                    },
-
-                    (user, next) => {
-                        verifyEmailSchema(
-                            newEmail,
-                            user.username,
-                            verificationToken,
-                            () => {
-                                next();
-                            }
-                        );
-                    },
-                ],
-                async (err) => {
-                    if (err && err !== true) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "UPDATE_EMAIL",
-                            `Couldn't update email for user "${updatingUserId}" to email "${newEmail}". '${err}'`
-                        );
-                        cb({ status: "failure", message: err });
-                    } else {
-                        console.log(
-                            "SUCCESS",
-                            "UPDATE_EMAIL",
-                            `Updated email for user "${updatingUserId}" to email "${newEmail}".`
-                        );
-                        cb({
-                            status: "success",
-                            message: "Email updated successfully.",
-                        });
-                    }
-                }
-            );
-        }
-    ),
-
-    /**
-     * Updates a user's name
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} updatingUserId - the updating user's id
-     * @param {String} newBio - the new name
-     * @param {Function} cb - gets called with the result
-     */
-    updateName: hooks.loginRequired(
-        async (session, updatingUserId, newName, cb) => {
-            const userModel = await db.runJob("GET_MODEL", {
-                modelName: "user",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        if (updatingUserId === session.userId)
-                            return next(null, true);
-                        userModel.findOne({ _id: session.userId }, next);
-                    },
-
-                    (user, next) => {
-                        if (user !== true && (!user || user.role !== "admin"))
-                            return next("Invalid permissions.");
-                        userModel.findOne({ _id: updatingUserId }, next);
-                    },
-
-                    (user, next) => {
-                        if (!user) return next("User not found.");
-                        userModel.updateOne(
-                            { _id: updatingUserId },
-                            { $set: { name: newName } },
-                            { runValidators: true },
-                            next
-                        );
-                    },
-                ],
-                async (err) => {
-                    if (err && err !== true) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "UPDATE_NAME",
-                            `Couldn't update name for user "${updatingUserId}" to name "${newName}". "${err}"`
-                        );
-                        cb({ status: "failure", message: err });
-                    } else {
-                        console.log(
-                            "SUCCESS",
-                            "UPDATE_NAME",
-                            `Updated name for user "${updatingUserId}" to name "${newName}".`
-                        );
-                        cb({
-                            status: "success",
-                            message: "Name updated successfully",
-                        });
-                    }
-                }
-            );
-        }
-    ),
-
-    /**
-     * Updates a user's location
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} updatingUserId - the updating user's id
-     * @param {String} newLocation - the new location
-     * @param {Function} cb - gets called with the result
-     */
-    updateLocation: hooks.loginRequired(
-        async (session, updatingUserId, newLocation, cb) => {
-            const userModel = await db.runJob("GET_MODEL", {
-                modelName: "user",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        if (updatingUserId === session.userId)
-                            return next(null, true);
-                        userModel.findOne({ _id: session.userId }, next);
-                    },
-
-                    (user, next) => {
-                        if (user !== true && (!user || user.role !== "admin"))
-                            return next("Invalid permissions.");
-                        userModel.findOne({ _id: updatingUserId }, next);
-                    },
-
-                    (user, next) => {
-                        if (!user) return next("User not found.");
-                        userModel.updateOne(
-                            { _id: updatingUserId },
-                            { $set: { location: newLocation } },
-                            { runValidators: true },
-                            next
-                        );
-                    },
-                ],
-                async (err) => {
-                    if (err && err !== true) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "UPDATE_LOCATION",
-                            `Couldn't update location for user "${updatingUserId}" to location "${newLocation}". "${err}"`
-                        );
-                        cb({ status: "failure", message: err });
-                    } else {
-                        console.log(
-                            "SUCCESS",
-                            "UPDATE_LOCATION",
-                            `Updated location for user "${updatingUserId}" to location "${newLocation}".`
-                        );
-                        cb({
-                            status: "success",
-                            message: "Location updated successfully",
-                        });
-                    }
-                }
-            );
-        }
-    ),
-
-    /**
-     * Updates a user's bio
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} updatingUserId - the updating user's id
-     * @param {String} newBio - the new bio
-     * @param {Function} cb - gets called with the result
-     */
-    updateBio: hooks.loginRequired(
-        async (session, updatingUserId, newBio, cb) => {
-            const userModel = await db.runJob("GET_MODEL", {
-                modelName: "user",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        if (updatingUserId === session.userId)
-                            return next(null, true);
-                        userModel.findOne({ _id: session.userId }, next);
-                    },
-
-                    (user, next) => {
-                        if (user !== true && (!user || user.role !== "admin"))
-                            return next("Invalid permissions.");
-                        userModel.findOne({ _id: updatingUserId }, next);
-                    },
-
-                    (user, next) => {
-                        if (!user) return next("User not found.");
-                        userModel.updateOne(
-                            { _id: updatingUserId },
-                            { $set: { bio: newBio } },
-                            { runValidators: true },
-                            next
-                        );
-                    },
-                ],
-                async (err) => {
-                    if (err && err !== true) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "UPDATE_BIO",
-                            `Couldn't update bio for user "${updatingUserId}" to bio "${newBio}". "${err}"`
-                        );
-                        cb({ status: "failure", message: err });
-                    } else {
-                        console.log(
-                            "SUCCESS",
-                            "UPDATE_BIO",
-                            `Updated bio for user "${updatingUserId}" to bio "${newBio}".`
-                        );
-                        cb({
-                            status: "success",
-                            message: "Bio updated successfully",
-                        });
-                    }
-                }
-            );
-        }
-    ),
-
-    /**
-     * Updates the type of a user's avatar
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} updatingUserId - the updating user's id
-     * @param {String} newType - the new type
-     * @param {Function} cb - gets called with the result
-     */
-    updateAvatarType: hooks.loginRequired(
-        async (session, updatingUserId, newType, cb) => {
-            const userModel = await db.runJob("GET_MODEL", {
-                modelName: "user",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        if (updatingUserId === session.userId)
-                            return next(null, true);
-                        userModel.findOne({ _id: session.userId }, next);
-                    },
-
-                    (user, next) => {
-                        if (user !== true && (!user || user.role !== "admin"))
-                            return next("Invalid permissions.");
-                        userModel.findOne({ _id: updatingUserId }, next);
-                    },
-
-                    (user, next) => {
-                        if (!user) return next("User not found.");
-                        userModel.updateOne(
-                            { _id: updatingUserId },
-                            { $set: { "avatar.type": newType } },
-                            { runValidators: true },
-                            next
-                        );
-                    },
-                ],
-                async (err) => {
-                    if (err && err !== true) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "UPDATE_AVATAR_TYPE",
-                            `Couldn't update avatar type for user "${updatingUserId}" to type "${newType}". "${err}"`
-                        );
-                        cb({ status: "failure", message: err });
-                    } else {
-                        console.log(
-                            "SUCCESS",
-                            "UPDATE_AVATAR_TYPE",
-                            `Updated avatar type for user "${updatingUserId}" to type "${newType}".`
-                        );
-                        cb({
-                            status: "success",
-                            message: "Avatar type updated successfully",
-                        });
-                    }
-                }
-            );
-        }
-    ),
-
-    /**
-     * Updates a user's role
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} updatingUserId - the updating user's id
-     * @param {String} newRole - the new role
-     * @param {Function} cb - gets called with the result
-     */
-    updateRole: hooks.adminRequired(
-        async (session, updatingUserId, newRole, cb) => {
-            newRole = newRole.toLowerCase();
-            const userModel = await db.runJob("GET_MODEL", {
-                modelName: "user",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        userModel.findOne({ _id: updatingUserId }, next);
-                    },
-
-                    (user, next) => {
-                        if (!user) return next("User not found.");
-                        else if (user.role === newRole)
-                            return next(
-                                "New role can't be the same as the old role."
-                            );
-                        else return next();
-                    },
-                    (next) => {
-                        userModel.updateOne(
-                            { _id: updatingUserId },
-                            { $set: { role: newRole } },
-                            { runValidators: true },
-                            next
-                        );
-                    },
-                ],
-                async (err) => {
-                    if (err && err !== true) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "UPDATE_ROLE",
-                            `User "${session.userId}" couldn't update role for user "${updatingUserId}" to role "${newRole}". "${err}"`
-                        );
-                        cb({ status: "failure", message: err });
-                    } else {
-                        console.log(
-                            "SUCCESS",
-                            "UPDATE_ROLE",
-                            `User "${session.userId}" updated the role of user "${updatingUserId}" to role "${newRole}".`
-                        );
-                        cb({
-                            status: "success",
-                            message: "Role successfully updated.",
-                        });
-                    }
-                }
-            );
-        }
-    ),
-
-    /**
-     * Updates a user's password
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} previousPassword - the previous password
-     * @param {String} newPassword - the new password
-     * @param {Function} cb - gets called with the result
-     */
-    updatePassword: hooks.loginRequired(async (session, previousPassword, newPassword, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-
-        async.waterfall(
-            [
-                (next) => {
-                    userModel.findOne({ _id: session.userId }, next);
-                },
-
-                (user, next) => {
-                    if (!user.services.password)
-                        return next(
-                            "This account does not have a password set."
-                        );
-                    return next(null, user.services.password.password);
-                },
-
-                (storedPassword, next) => {
-                    bcrypt.compare(sha256(previousPassword), storedPassword).then(res => {
-                        if (res) return next();
-                        else return next("Please enter the correct previous password.")
-                    });
-                },
-
-                (next) => {
-                    if (!db.passwordValid(newPassword))
-                        return next(
-                            "Invalid new password. Check if it meets all the requirements."
-                        );
-                    return next();
-                },
-
-                (next) => {
-                    bcrypt.genSalt(10, next);
-                },
-
-                // hash the password
-                (salt, next) => {
-                    bcrypt.hash(sha256(newPassword), salt, next);
-                },
-
-                (hashedPassword, next) => {
-                    userModel.updateOne(
-                        { _id: session.userId },
-                        {
-                            $set: {
-                                "services.password.password": hashedPassword,
-                            },
-                        },
-                        next
-                    );
-                },
-            ],
-            async (err) => {
-                if (err) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "UPDATE_PASSWORD",
-                        `Failed updating user password of user '${session.userId}'. '${err}'.`
-                    );
-                    return cb({ status: "failure", message: err });
-                }
-
-                console.log(
-                    "SUCCESS",
-                    "UPDATE_PASSWORD",
-                    `User '${session.userId}' updated their password.`
-                );
-                cb({
-                    status: "success",
-                    message: "Password successfully updated.",
-                });
-            }
-        );
-    }),
-
-    /**
-     * Requests a password for a session
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} email - the email of the user that requests a password reset
-     * @param {Function} cb - gets called with the result
-     */
-    requestPassword: hooks.loginRequired(async (session, cb) => {
-        let code = await utils.runJob("GENERATE_RANDOM_STRING", { length: 8 });
-        const passwordRequestSchema = await mail.runJob("GET_SCHEMA", {
-            schemaName: "passwordRequest",
-        });
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        async.waterfall(
-            [
-                (next) => {
-                    userModel.findOne({ _id: session.userId }, next);
-                },
-
-                (user, next) => {
-                    if (!user) return next("User not found.");
-                    if (
-                        user.services.password &&
-                        user.services.password.password
-                    )
-                        return next("You already have a password set.");
-                    next(null, user);
-                },
-
-                (user, next) => {
-                    let expires = new Date();
-                    expires.setDate(expires.getDate() + 1);
-                    userModel.findOneAndUpdate(
-                        { "email.address": user.email.address },
-                        {
-                            $set: {
-                                "services.password": {
-                                    set: { code: code, expires },
-                                },
-                            },
-                        },
-                        { runValidators: true },
-                        next
-                    );
-                },
-
-                (user, next) => {
-                    passwordRequestSchema(
-                        user.email.address,
-                        user.username,
-                        code,
-                        next
-                    );
-                },
-            ],
-            async (err) => {
-                if (err && err !== true) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "REQUEST_PASSWORD",
-                        `UserId '${session.userId}' failed to request password. '${err}'`
-                    );
-                    cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "REQUEST_PASSWORD",
-                        `UserId '${session.userId}' successfully requested a password.`
-                    );
-                    cb({
-                        status: "success",
-                        message: "Successfully requested password.",
-                    });
-                }
-            }
-        );
-    }),
-
-    /**
-     * Verifies a password code
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} code - the password code
-     * @param {Function} cb - gets called with the result
-     */
-    verifyPasswordCode: hooks.loginRequired(async (session, code, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        async.waterfall(
-            [
-                (next) => {
-                    if (!code || typeof code !== "string")
-                        return next("Invalid code.");
-                    userModel.findOne(
-                        {
-                            "services.password.set.code": code,
-                            _id: session.userId,
-                        },
-                        next
-                    );
-                },
-
-                (user, next) => {
-                    if (!user) return next("Invalid code.");
-                    if (user.services.password.set.expires < new Date())
-                        return next("That code has expired.");
-                    next(null);
-                },
-            ],
-            async (err) => {
-                if (err && err !== true) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "VERIFY_PASSWORD_CODE",
-                        `Code '${code}' failed to verify. '${err}'`
-                    );
-                    cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "VERIFY_PASSWORD_CODE",
-                        `Code '${code}' successfully verified.`
-                    );
-                    cb({
-                        status: "success",
-                        message: "Successfully verified password code.",
-                    });
-                }
-            }
-        );
-    }),
-
-    /**
-     * Adds a password to a user with a code
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} code - the password code
-     * @param {String} newPassword - the new password code
-     * @param {Function} cb - gets called with the result
-     */
-    changePasswordWithCode: hooks.loginRequired(
-        async (session, code, newPassword, cb) => {
-            const userModel = await db.runJob("GET_MODEL", {
-                modelName: "user",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        if (!code || typeof code !== "string")
-                            return next("Invalid code.");
-                        userModel.findOne(
-                            { "services.password.set.code": code },
-                            next
-                        );
-                    },
-
-                    (user, next) => {
-                        if (!user) return next("Invalid code.");
-                        if (!user.services.password.set.expires > new Date())
-                            return next("That code has expired.");
-                        next();
-                    },
-
-                    (next) => {
-                        if (!db.passwordValid(newPassword))
-                            return next(
-                                "Invalid password. Check if it meets all the requirements."
-                            );
-                        return next();
-                    },
-
-                    (next) => {
-                        bcrypt.genSalt(10, next);
-                    },
-
-                    // hash the password
-                    (salt, next) => {
-                        bcrypt.hash(sha256(newPassword), salt, next);
-                    },
-
-                    (hashedPassword, next) => {
-                        userModel.updateOne(
-                            { "services.password.set.code": code },
-                            {
-                                $set: {
-                                    "services.password.password": hashedPassword,
-                                },
-                                $unset: { "services.password.set": "" },
-                            },
-                            { runValidators: true },
-                            next
-                        );
-                    },
-                ],
-                async (err) => {
-                    if (err && err !== true) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "ADD_PASSWORD_WITH_CODE",
-                            `Code '${code}' failed to add password. '${err}'`
-                        );
-                        cb({ status: "failure", message: err });
-                    } else {
-                        console.log(
-                            "SUCCESS",
-                            "ADD_PASSWORD_WITH_CODE",
-                            `Code '${code}' successfully added password.`
-                        );
-                        cache.runJob("PUB", {
-                            channel: "user.linkPassword",
-                            value: session.userId,
-                        });
-                        cb({
-                            status: "success",
-                            message: "Successfully added password.",
-                        });
-                    }
-                }
-            );
-        }
-    ),
-
-    /**
-     * Unlinks password from user
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {Function} cb - gets called with the result
-     */
-    unlinkPassword: hooks.loginRequired(async (session, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        async.waterfall(
-            [
-                (next) => {
-                    userModel.findOne({ _id: session.userId }, next);
-                },
-
-                (user, next) => {
-                    if (!user) return next("Not logged in.");
-                    if (!user.services.github || !user.services.github.id)
-                        return next(
-                            "You can't remove password login without having GitHub login."
-                        );
-                    userModel.updateOne(
-                        { _id: session.userId },
-                        { $unset: { "services.password": "" } },
-                        next
-                    );
-                },
-            ],
-            async (err) => {
-                if (err && err !== true) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "UNLINK_PASSWORD",
-                        `Unlinking password failed for userId '${session.userId}'. '${err}'`
-                    );
-                    cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "UNLINK_PASSWORD",
-                        `Unlinking password successful for userId '${session.userId}'.`
-                    );
-                    cache.runJob("PUB", {
-                        channel: "user.unlinkPassword",
-                        value: session.userId,
-                    });
-                    cb({
-                        status: "success",
-                        message: "Successfully unlinked password.",
-                    });
-                }
-            }
-        );
-    }),
-
-    /**
-     * Unlinks GitHub from user
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {Function} cb - gets called with the result
-     */
-    unlinkGitHub: hooks.loginRequired(async (session, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        async.waterfall(
-            [
-                (next) => {
-                    userModel.findOne({ _id: session.userId }, next);
-                },
-
-                (user, next) => {
-                    if (!user) return next("Not logged in.");
-                    if (
-                        !user.services.password ||
-                        !user.services.password.password
-                    )
-                        return next(
-                            "You can't remove GitHub login without having password login."
-                        );
-                    userModel.updateOne(
-                        { _id: session.userId },
-                        { $unset: { "services.github": "" } },
-                        next
-                    );
-                },
-            ],
-            async (err) => {
-                if (err && err !== true) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "UNLINK_GITHUB",
-                        `Unlinking GitHub failed for userId '${session.userId}'. '${err}'`
-                    );
-                    cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "UNLINK_GITHUB",
-                        `Unlinking GitHub successful for userId '${session.userId}'.`
-                    );
-                    cache.runJob("PUB", {
-                        channel: "user.unlinkGithub",
-                        value: session.userId,
-                    });
-                    cb({
-                        status: "success",
-                        message: "Successfully unlinked GitHub.",
-                    });
-                }
-            }
-        );
-    }),
-
-    /**
-     * Requests a password reset for an email
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} email - the email of the user that requests a password reset
-     * @param {Function} cb - gets called with the result
-     */
-    requestPasswordReset: async (session, email, cb) => {
-        let code = await utils.runJob("GENERATE_RANDOM_STRING", { length: 8 });
-        console.log(111, code);
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        const resetPasswordRequestSchema = await mail.runJob("GET_SCHEMA", {
-            schemaName: "resetPasswordRequest",
-        });
-        async.waterfall(
-            [
-                (next) => {
-                    if (!email || typeof email !== "string")
-                        return next("Invalid email.");
-                    email = email.toLowerCase();
-                    userModel.findOne({ "email.address": email }, next);
-                },
-
-                (user, next) => {
-                    if (!user) return next("User not found.");
-                    if (
-                        !user.services.password ||
-                        !user.services.password.password
-                    )
-                        return next(
-                            "User does not have a password set, and probably uses GitHub to log in."
-                        );
-                    next(null, user);
-                },
-
-                (user, next) => {
-                    let expires = new Date();
-                    expires.setDate(expires.getDate() + 1);
-                    userModel.findOneAndUpdate(
-                        { "email.address": email },
-                        {
-                            $set: {
-                                "services.password.reset": {
-                                    code: code,
-                                    expires,
-                                },
-                            },
-                        },
-                        { runValidators: true },
-                        next
-                    );
-                },
-
-                (user, next) => {
-                    resetPasswordRequestSchema(
-                        user.email.address,
-                        user.username,
-                        code,
-                        next
-                    );
-                },
-            ],
-            async (err) => {
-                if (err && err !== true) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "REQUEST_PASSWORD_RESET",
-                        `Email '${email}' failed to request password reset. '${err}'`
-                    );
-                    cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "REQUEST_PASSWORD_RESET",
-                        `Email '${email}' successfully requested a password reset.`
-                    );
-                    cb({
-                        status: "success",
-                        message: "Successfully requested password reset.",
-                    });
-                }
-            }
-        );
-    },
-
-    /**
-     * Verifies a reset code
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} code - the password reset code
-     * @param {Function} cb - gets called with the result
-     */
-    verifyPasswordResetCode: async (session, code, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        async.waterfall(
-            [
-                (next) => {
-                    if (!code || typeof code !== "string")
-                        return next("Invalid code.");
-                    userModel.findOne(
-                        { "services.password.reset.code": code },
-                        next
-                    );
-                },
-
-                (user, next) => {
-                    if (!user) return next("Invalid code.");
-                    if (!user.services.password.reset.expires > new Date())
-                        return next("That code has expired.");
-                    next(null);
-                },
-            ],
-            async (err) => {
-                if (err && err !== true) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "VERIFY_PASSWORD_RESET_CODE",
-                        `Code '${code}' failed to verify. '${err}'`
-                    );
-                    cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "VERIFY_PASSWORD_RESET_CODE",
-                        `Code '${code}' successfully verified.`
-                    );
-                    cb({
-                        status: "success",
-                        message: "Successfully verified password reset code.",
-                    });
-                }
-            }
-        );
-    },
-
-    /**
-     * Changes a user's password with a reset code
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} code - the password reset code
-     * @param {String} newPassword - the new password reset code
-     * @param {Function} cb - gets called with the result
-     */
-    changePasswordWithResetCode: async (session, code, newPassword, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        async.waterfall(
-            [
-                (next) => {
-                    if (!code || typeof code !== "string")
-                        return next("Invalid code.");
-                    userModel.findOne(
-                        { "services.password.reset.code": code },
-                        next
-                    );
-                },
-
-                (user, next) => {
-                    if (!user) return next("Invalid code.");
-                    if (!user.services.password.reset.expires > new Date())
-                        return next("That code has expired.");
-                    next();
-                },
-
-                (next) => {
-                    if (!db.passwordValid(newPassword))
-                        return next(
-                            "Invalid password. Check if it meets all the requirements."
-                        );
-                    return next();
-                },
-
-                (next) => {
-                    bcrypt.genSalt(10, next);
-                },
-
-                // hash the password
-                (salt, next) => {
-                    bcrypt.hash(sha256(newPassword), salt, next);
-                },
-
-                (hashedPassword, next) => {
-                    userModel.updateOne(
-                        { "services.password.reset.code": code },
-                        {
-                            $set: {
-                                "services.password.password": hashedPassword,
-                            },
-                            $unset: { "services.password.reset": "" },
-                        },
-                        { runValidators: true },
-                        next
-                    );
-                },
-            ],
-            async (err) => {
-                if (err && err !== true) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "CHANGE_PASSWORD_WITH_RESET_CODE",
-                        `Code '${code}' failed to change password. '${err}'`
-                    );
-                    cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "CHANGE_PASSWORD_WITH_RESET_CODE",
-                        `Code '${code}' successfully changed password.`
-                    );
-                    cb({
-                        status: "success",
-                        message: "Successfully changed password.",
-                    });
-                }
-            }
-        );
-    },
-
-    /**
-     * Bans a user by userId
-     *
-     * @param {Object} session - the session object automatically added by socket.io
-     * @param {String} value - the user id that is going to be banned
-     * @param {String} reason - the reason for the ban
-     * @param {String} expiresAt - the time the ban expires
-     * @param {Function} cb - gets called with the result
-     */
-    banUserById: hooks.adminRequired(
-        (session, userId, reason, expiresAt, cb) => {
-            async.waterfall(
-                [
-                    (next) => {
-                        if (!userId)
-                            return next("You must provide a userId to ban.");
-                        else if (!reason)
-                            return next(
-                                "You must provide a reason for the ban."
-                            );
-                        else return next();
-                    },
-
-                    (next) => {
-                        if (!expiresAt || typeof expiresAt !== "string")
-                            return next("Invalid expire date.");
-                        let date = new Date();
-                        switch (expiresAt) {
-                            case "1h":
-                                expiresAt = date.setHours(date.getHours() + 1);
-                                break;
-                            case "12h":
-                                expiresAt = date.setHours(date.getHours() + 12);
-                                break;
-                            case "1d":
-                                expiresAt = date.setDate(date.getDate() + 1);
-                                break;
-                            case "1w":
-                                expiresAt = date.setDate(date.getDate() + 7);
-                                break;
-                            case "1m":
-                                expiresAt = date.setMonth(date.getMonth() + 1);
-                                break;
-                            case "3m":
-                                expiresAt = date.setMonth(date.getMonth() + 3);
-                                break;
-                            case "6m":
-                                expiresAt = date.setMonth(date.getMonth() + 6);
-                                break;
-                            case "1y":
-                                expiresAt = date.setFullYear(
-                                    date.getFullYear() + 1
-                                );
-                                break;
-                            case "never":
-                                expiresAt = new Date(3093527980800000);
-                                break;
-                            default:
-                                return next("Invalid expire date.");
-                        }
-
-                        next();
-                    },
-
-                    (next) => {
-                        punishments
-                            .runJob("ADD_PUNISHMENT", {
-                                type: "banUserId",
-                                value: userId,
-                                reason,
-                                expiresAt,
-                                punishedBy,
-                            })
-                            .then((punishment) => {
-                                next(null, punishment);
-                            })
-                            .catch(next);
-                    },
-
-                    (punishment, next) => {
-                        cache.runJob("PUB", {
-                            channel: "user.ban",
-                            value: { userId, punishment },
-                        });
-                        next();
-                    },
-                ],
-                async (err) => {
-                    if (err && err !== true) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        console.log(
-                            "ERROR",
-                            "BAN_USER_BY_ID",
-                            `User ${session.userId} failed to ban user ${userId} with the reason ${reason}. '${err}'`
-                        );
-                        cb({ status: "failure", message: err });
-                    } else {
-                        console.log(
-                            "SUCCESS",
-                            "BAN_USER_BY_ID",
-                            `User ${session.userId} has successfully banned user ${userId} with the reason ${reason}.`
-                        );
-                        cb({
-                            status: "success",
-                            message: "Successfully banned user.",
-                        });
-                    }
-                }
-            );
-        }
-    ),
-
-    getFavoriteStations: hooks.loginRequired(async (session, cb) => {
-        const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
-        async.waterfall(
-            [
-                (next) => {
-                    userModel.findOne({ _id: session.userId }, next);
-                },
-
-                (user, next) => {
-                    if (!user) return next("User not found.");
-                    next(null, user);
-                },
-            ],
-            async (err, user) => {
-                if (err && err !== true) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "GET_FAVORITE_STATIONS",
-                        `User ${session.userId} failed to get favorite stations. '${err}'`
-                    );
-                    cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "GET_FAVORITE_STATIONS",
-                        `User ${session.userId} got favorite stations.`
-                    );
-                    cb({
-                        status: "success",
-                        favoriteStations: user.favoriteStations,
-                    });
-                }
-            }
-        );
-    }),
+export default {
+	/**
+	 * Lists all Users
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {Function} cb - gets called with the result
+	 */
+	index: isAdminRequired(async (session, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+
+		async.waterfall(
+			[
+				next => {
+					userModel.find({}).exec(next);
+				}
+			],
+			async (err, users) => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "USER_INDEX", `Indexing users failed. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "USER_INDEX", `Indexing users successful.`);
+				const filteredUsers = [];
+				users.forEach(user => {
+					filteredUsers.push({
+						_id: user._id,
+						username: user.username,
+						role: user.role,
+						liked: user.liked,
+						disliked: user.disliked,
+						songsRequested: user.statistics.songsRequested,
+						email: {
+							address: user.email.address,
+							verified: user.email.verified
+						},
+						hasPassword: !!user.services.password,
+						services: { github: user.services.github }
+					});
+				});
+				return cb({ status: "success", data: filteredUsers });
+			}
+		);
+	}),
+
+	/**
+	 * Logs user in
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} identifier - the email of the user
+	 * @param {string} password - the plaintext of the user
+	 * @param {Function} cb - gets called with the result
+	 */
+	login: async (session, identifier, password, cb) => {
+		identifier = identifier.toLowerCase();
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		const sessionSchema = await cache.runJob("GET_SCHEMA", {
+			schemaName: "session"
+		});
+
+		async.waterfall(
+			[
+				// check if a user with the requested identifier exists
+				next => {
+					userModel.findOne(
+						{
+							$or: [{ "email.address": identifier }]
+						},
+						next
+					);
+				},
+
+				// if the user doesn't exist, respond with a failure
+				// otherwise compare the requested password and the actual users password
+				(user, next) => {
+					if (!user) return next("User not found");
+					if (!user.services.password || !user.services.password.password)
+						return next("The account you are trying to access uses GitHub to log in.");
+
+					return bcrypt.compare(sha256(password), user.services.password.password, (err, match) => {
+						if (err) return next(err);
+						if (!match) return next("Incorrect password");
+						return next(null, user);
+					});
+				},
+
+				(user, next) => {
+					utils.runJob("GUID", {}).then(sessionId => {
+						next(null, user, sessionId);
+					});
+				},
+
+				(user, sessionId, next) => {
+					cache
+						.runJob("HSET", {
+							table: "sessions",
+							key: sessionId,
+							value: sessionSchema(sessionId, user._id)
+						})
+						.then(() => {
+							next(null, sessionId);
+						})
+						.catch(next);
+				}
+			],
+			async (err, sessionId) => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"USER_PASSWORD_LOGIN",
+						`Login failed with password for user "${identifier}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+
+				console.log(
+					"SUCCESS",
+					"USER_PASSWORD_LOGIN",
+					`Login successful with password for user "${identifier}"`
+				);
+
+				return cb({
+					status: "success",
+					message: "Login successful",
+					user: {},
+					SID: sessionId
+				});
+			}
+		);
+	},
+
+	/**
+	 * Registers a new user
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} username - the username for the new user
+	 * @param {string} email - the email for the new user
+	 * @param {string} password - the plaintext password for the new user
+	 * @param {object} recaptcha - the recaptcha data
+	 * @param {Function} cb - gets called with the result
+	 */
+	async register(session, username, email, password, recaptcha, cb) {
+		email = email.toLowerCase();
+		const verificationToken = await utils.runJob("GENERATE_RANDOM_STRING", {
+			length: 64
+		});
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		const verifyEmailSchema = await mail.runJob("GET_SCHEMA", {
+			schemaName: "verifyEmail"
+		});
+
+		async.waterfall(
+			[
+				next => {
+					if (config.get("registrationDisabled") === true)
+						return next("Registration is not allowed at this time.");
+					return next();
+				},
+
+				next => {
+					if (!db.passwordValid(password))
+						return next("Invalid password. Check if it meets all the requirements.");
+					return next();
+				},
+
+				// verify the request with google recaptcha
+				next => {
+					if (config.get("apis.recaptcha.enabled") === true)
+						request(
+							{
+								url: "https://www.google.com/recaptcha/api/siteverify",
+								method: "POST",
+								form: {
+									secret: config.get("apis").recaptcha.secret,
+									response: recaptcha
+								}
+							},
+							next
+						);
+					else next(null, null, null);
+				},
+
+				// check if the response from Google recaptcha is successful
+				// if it is, we check if a user with the requested username already exists
+				(response, body, next) => {
+					if (config.get("apis.recaptcha.enabled") === true) {
+						const json = JSON.parse(body);
+						if (json.success !== true) return next("Response from recaptcha was not successful.");
+					}
+
+					return userModel.findOne({ username: new RegExp(`^${username}$`, "i") }, next);
+				},
+
+				// if the user already exists, respond with that
+				// otherwise check if a user with the requested email already exists
+				(user, next) => {
+					if (user) return next("A user with that username already exists.");
+					return userModel.findOne({ "email.address": email }, next);
+				},
+
+				// if the user already exists, respond with that
+				// otherwise, generate a salt to use with hashing the new users password
+				(user, next) => {
+					if (user) return next("A user with that email already exists.");
+					return bcrypt.genSalt(10, next);
+				},
+
+				// hash the password
+				(salt, next) => {
+					bcrypt.hash(sha256(password), salt, next);
+				},
+
+				(hash, next) => {
+					utils.runJob("GENERATE_RANDOM_STRING", { length: 12 }).then(_id => {
+						next(null, hash, _id);
+					});
+				},
+
+				// create the user object
+				(hash, _id, next) => {
+					next(null, {
+						_id,
+						username,
+						email: {
+							address: email,
+							verificationToken
+						},
+						services: {
+							password: {
+								password: hash
+							}
+						}
+					});
+				},
+
+				// generate the url for gravatar avatar
+				(user, next) => {
+					utils
+						.runJob("CREATE_GRAVATAR", {
+							email: user.email.address
+						})
+						.then(url => {
+							user.avatar = url;
+							next(null, user);
+						});
+				},
+
+				// save the new user to the database
+				(user, next) => {
+					userModel.create(user, next);
+				},
+
+				// respond with the new user
+				(newUser, next) => {
+					verifyEmailSchema(email, username, verificationToken, () => {
+						next(null, newUser);
+					});
+				}
+			],
+			async (err, user) => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"USER_PASSWORD_REGISTER",
+						`Register failed with password for user "${username}"."${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+
+				return module.exports.login(session, email, password, result => {
+					const obj = {
+						status: "success",
+						message: "Successfully registered."
+					};
+					if (result.status === "success") {
+						obj.SID = result.SID;
+					}
+					activities.runJob("ADD_ACTIVITY", {
+						userId: user._id,
+						activityType: "created_account"
+					});
+					console.log(
+						"SUCCESS",
+						"USER_PASSWORD_REGISTER",
+						`Register successful with password for user "${username}".`
+					);
+					return cb(obj);
+				});
+			}
+		);
+	},
+
+	/**
+	 * Logs out a user
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {Function} cb - gets called with the result
+	 */
+	logout: (session, cb) => {
+		async.waterfall(
+			[
+				next => {
+					cache
+						.runJob("HGET", {
+							table: "sessions",
+							key: session.sessionId
+						})
+						.then(session => {
+							next(null, session);
+						})
+						.catch(next);
+				},
+
+				(session, next) => {
+					if (!session) return next("Session not found");
+					return next(null, session);
+				},
+
+				(session, next) => {
+					cache
+						.runJob("HDEL", {
+							table: "sessions",
+							key: session.sessionId
+						})
+						.then(() => {
+							next();
+						})
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "USER_LOGOUT", `Logout failed. "${err}" `);
+					cb({ status: "failure", message: err });
+				} else {
+					console.log("SUCCESS", "USER_LOGOUT", `Logout successful.`);
+					cb({
+						status: "success",
+						message: "Successfully logged out."
+					});
+				}
+			}
+		);
+	},
+
+	/**
+	 * Removes all sessions for a user
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} userId - the id of the user we are trying to delete the sessions of
+	 * @param {Function} cb - gets called with the result
+	 */
+	removeSessions: isLoginRequired(async (session, userId, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		async.waterfall(
+			[
+				next => {
+					userModel.findOne({ _id: session.userId }, (err, user) => {
+						if (err) return next(err);
+						if (user.role !== "admin" && session.userId !== userId)
+							return next("Only admins and the owner of the account can remove their sessions.");
+						return next();
+					});
+				},
+
+				next => {
+					cache
+						.runJob("HGETALL", { table: "sessions" })
+						.then(sessions => {
+							next(null, sessions);
+						})
+						.catch(next);
+				},
+
+				(sessions, next) => {
+					if (!sessions) return next("There are no sessions for this user to remove.");
+
+					const keys = Object.keys(sessions);
+
+					return next(null, keys, sessions);
+				},
+
+				(keys, sessions, next) => {
+					cache.runJob("PUB", {
+						channel: "user.removeSessions",
+						value: userId
+					});
+					async.each(
+						keys,
+						(sessionId, callback) => {
+							const session = sessions[sessionId];
+							if (session.userId === userId) {
+								cache
+									.runJob("HDEL", {
+										channel: "sessions",
+										key: sessionId
+									})
+									.then(() => {
+										callback(null);
+									})
+									.catch(next);
+							}
+						},
+						err => {
+							next(err);
+						}
+					);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"REMOVE_SESSIONS_FOR_USER",
+						`Couldn't remove all sessions for user "${userId}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				console.log("SUCCESS", "REMOVE_SESSIONS_FOR_USER", `Removed all sessions for user "${userId}".`);
+				return cb({
+					status: "success",
+					message: "Successfully removed all sessions."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Gets user object from username (only a few properties)
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} username - the username of the user we are trying to find
+	 * @param {Function} cb - gets called with the result
+	 */
+	findByUsername: async (session, username, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		async.waterfall(
+			[
+				next => {
+					userModel.findOne({ username: new RegExp(`^${username}$`, "i") }, next);
+				},
+
+				(account, next) => {
+					if (!account) return next("User not found.");
+					return next(null, account);
+				}
+			],
+			async (err, account) => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+
+					console.log("ERROR", "FIND_BY_USERNAME", `User not found for username "${username}". "${err}"`);
+
+					return cb({ status: "failure", message: err });
+				}
+
+				console.log("SUCCESS", "FIND_BY_USERNAME", `User found for username "${username}".`);
+
+				return cb({
+					status: "success",
+					data: {
+						_id: account._id,
+						name: account.name,
+						username: account.username,
+						location: account.location,
+						bio: account.bio,
+						role: account.role,
+						avatar: account.avatar,
+						createdAt: account.createdAt
+					}
+				});
+			}
+		);
+	},
+
+	/**
+	 * Gets a username from an userId
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} userId - the userId of the person we are trying to get the username from
+	 * @param {Function} cb - gets called with the result
+	 */
+	getUsernameFromId: async (session, userId, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		userModel
+			.findById(userId)
+			.then(user => {
+				if (user) {
+					console.log("SUCCESS", "GET_USERNAME_FROM_ID", `Found username for userId "${userId}".`);
+
+					return cb({
+						status: "success",
+						data: user.username
+					});
+				}
+
+				console.log(
+					"ERROR",
+					"GET_USERNAME_FROM_ID",
+					`Getting the username from userId "${userId}" failed. User not found.`
+				);
+
+				return cb({
+					status: "failure",
+					message: "Couldn't find the user."
+				});
+			})
+			.catch(async err => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"GET_USERNAME_FROM_ID",
+						`Getting the username from userId "${userId}" failed. "${err}"`
+					);
+					cb({ status: "failure", message: err });
+				}
+			});
+	},
+
+	// TODO Fix security issues
+	/**
+	 * Gets user info from session
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {Function} cb - gets called with the result
+	 */
+	findBySession: async (session, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		async.waterfall(
+			[
+				next => {
+					cache
+						.runJob("HGET", {
+							table: "sessions",
+							key: session.sessionId
+						})
+						.then(session => {
+							next(null, session);
+						})
+						.catch(next);
+				},
+
+				(session, next) => {
+					if (!session) return next("Session not found.");
+					return next(null, session);
+				},
+
+				(session, next) => {
+					userModel.findOne({ _id: session.userId }, next);
+				},
+
+				(user, next) => {
+					if (!user) return next("User not found.");
+					return next(null, user);
+				}
+			],
+			async (err, user) => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "FIND_BY_SESSION", `User not found. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				const data = {
+					email: {
+						address: user.email.address
+					},
+					avatar: user.avatar,
+					username: user.username,
+					name: user.name,
+					location: user.location,
+					bio: user.bio
+				};
+				if (user.services.password && user.services.password.password) data.password = true;
+				if (user.services.github && user.services.github.id) data.github = true;
+				console.log("SUCCESS", "FIND_BY_SESSION", `User found. "${user.username}".`);
+				return cb({
+					status: "success",
+					data
+				});
+			}
+		);
+	},
+
+	/**
+	 * Updates a user's username
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} updatingUserId - the updating user's id
+	 * @param {string} newUsername - the new username
+	 * @param {Function} cb - gets called with the result
+	 */
+	updateUsername: isLoginRequired(async (session, updatingUserId, newUsername, cb) => {
+		const userModel = await db.runJob("GET_MODEL", {
+			modelName: "user"
+		});
+		async.waterfall(
+			[
+				next => {
+					if (updatingUserId === session.userId) return next(null, true);
+					return userModel.findOne({ _id: session.userId }, next);
+				},
+
+				(user, next) => {
+					if (user !== true && (!user || user.role !== "admin")) return next("Invalid permissions.");
+					return userModel.findOne({ _id: updatingUserId }, next);
+				},
+
+				(user, next) => {
+					if (!user) return next("User not found.");
+					if (user.username === newUsername)
+						return next("New username can't be the same as the old username.");
+					return next(null);
+				},
+
+				next => {
+					userModel.findOne({ username: new RegExp(`^${newUsername}$`, "i") }, next);
+				},
+
+				(user, next) => {
+					if (!user) return next();
+					if (user._id === updatingUserId) return next();
+					return next("That username is already in use.");
+				},
+
+				next => {
+					userModel.updateOne(
+						{ _id: updatingUserId },
+						{ $set: { username: newUsername } },
+						{ runValidators: true },
+						next
+					);
+				}
+			],
+			async err => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+
+					console.log(
+						"ERROR",
+						"UPDATE_USERNAME",
+						`Couldn't update username for user "${updatingUserId}" to username "${newUsername}". "${err}"`
+					);
+
+					return cb({ status: "failure", message: err });
+				}
+
+				cache.runJob("PUB", {
+					channel: "user.updateUsername",
+					value: {
+						username: newUsername,
+						_id: updatingUserId
+					}
+				});
+
+				console.log(
+					"SUCCESS",
+					"UPDATE_USERNAME",
+					`Updated username for user "${updatingUserId}" to username "${newUsername}".`
+				);
+
+				return cb({
+					status: "success",
+					message: "Username updated successfully"
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Updates a user's email
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} updatingUserId - the updating user's id
+	 * @param {string} newEmail - the new email
+	 * @param {Function} cb - gets called with the result
+	 */
+	updateEmail: isLoginRequired(async (session, updatingUserId, newEmail, cb) => {
+		newEmail = newEmail.toLowerCase();
+		const verificationToken = await utils.runJob("GENERATE_RANDOM_STRING", { length: 64 });
+		const userModel = await db.runJob("GET_MODEL", {
+			modelName: "user"
+		});
+		const verifyEmailSchema = await mail.runJob("GET_SCHEMA", {
+			schemaName: "verifyEmail"
+		});
+		async.waterfall(
+			[
+				next => {
+					if (updatingUserId === session.userId) return next(null, true);
+					return userModel.findOne({ _id: session.userId }, next);
+				},
+
+				(user, next) => {
+					if (user !== true && (!user || user.role !== "admin")) return next("Invalid permissions.");
+					return userModel.findOne({ _id: updatingUserId }, next);
+				},
+
+				(user, next) => {
+					if (!user) return next("User not found.");
+					if (user.email.address === newEmail)
+						return next("New email can't be the same as your the old email.");
+					return next();
+				},
+
+				next => {
+					userModel.findOne({ "email.address": newEmail }, next);
+				},
+
+				(user, next) => {
+					if (!user) return next();
+					if (user._id === updatingUserId) return next();
+					return next("That email is already in use.");
+				},
+
+				// regenerate the url for gravatar avatar
+				next => {
+					utils.runJob("CREATE_GRAVATAR", { email: newEmail }).then(url => {
+						next(null, url);
+					});
+				},
+
+				(avatar, next) => {
+					userModel.updateOne(
+						{ _id: updatingUserId },
+						{
+							$set: {
+								avatar,
+								"email.address": newEmail,
+								"email.verified": false,
+								"email.verificationToken": verificationToken
+							}
+						},
+						{ runValidators: true },
+						next
+					);
+				},
+
+				(res, next) => {
+					userModel.findOne({ _id: updatingUserId }, next);
+				},
+
+				(user, next) => {
+					verifyEmailSchema(newEmail, user.username, verificationToken, () => {
+						next();
+					});
+				}
+			],
+			async err => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+
+					console.log(
+						"ERROR",
+						"UPDATE_EMAIL",
+						`Couldn't update email for user "${updatingUserId}" to email "${newEmail}". '${err}'`
+					);
+
+					return cb({ status: "failure", message: err });
+				}
+
+				console.log(
+					"SUCCESS",
+					"UPDATE_EMAIL",
+					`Updated email for user "${updatingUserId}" to email "${newEmail}".`
+				);
+
+				return cb({
+					status: "success",
+					message: "Email updated successfully."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Updates a user's name
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} updatingUserId - the updating user's id
+	 * @param {string} newBio - the new name
+	 * @param {Function} cb - gets called with the result
+	 */
+	updateName: isLoginRequired(async (session, updatingUserId, newName, cb) => {
+		const userModel = await db.runJob("GET_MODEL", {
+			modelName: "user"
+		});
+
+		async.waterfall(
+			[
+				next => {
+					if (updatingUserId === session.userId) return next(null, true);
+					return userModel.findOne({ _id: session.userId }, next);
+				},
+
+				(user, next) => {
+					if (user !== true && (!user || user.role !== "admin")) return next("Invalid permissions.");
+					return userModel.findOne({ _id: updatingUserId }, next);
+				},
+
+				(user, next) => {
+					if (!user) return next("User not found.");
+					return userModel.updateOne(
+						{ _id: updatingUserId },
+						{ $set: { name: newName } },
+						{ runValidators: true },
+						next
+					);
+				}
+			],
+			async err => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"UPDATE_NAME",
+						`Couldn't update name for user "${updatingUserId}" to name "${newName}". "${err}"`
+					);
+					cb({ status: "failure", message: err });
+				} else {
+					console.log(
+						"SUCCESS",
+						"UPDATE_NAME",
+						`Updated name for user "${updatingUserId}" to name "${newName}".`
+					);
+					cb({
+						status: "success",
+						message: "Name updated successfully"
+					});
+				}
+			}
+		);
+	}),
+
+	/**
+	 * Updates a user's location
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} updatingUserId - the updating user's id
+	 * @param {string} newLocation - the new location
+	 * @param {Function} cb - gets called with the result
+	 */
+	updateLocation: isLoginRequired(async (session, updatingUserId, newLocation, cb) => {
+		const userModel = await db.runJob("GET_MODEL", {
+			modelName: "user"
+		});
+
+		async.waterfall(
+			[
+				next => {
+					if (updatingUserId === session.userId) return next(null, true);
+					return userModel.findOne({ _id: session.userId }, next);
+				},
+
+				(user, next) => {
+					if (user !== true && (!user || user.role !== "admin")) return next("Invalid permissions.");
+					return userModel.findOne({ _id: updatingUserId }, next);
+				},
+
+				(user, next) => {
+					if (!user) return next("User not found.");
+					return userModel.updateOne(
+						{ _id: updatingUserId },
+						{ $set: { location: newLocation } },
+						{ runValidators: true },
+						next
+					);
+				}
+			],
+			async err => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+
+					console.log(
+						"ERROR",
+						"UPDATE_LOCATION",
+						`Couldn't update location for user "${updatingUserId}" to location "${newLocation}". "${err}"`
+					);
+
+					return cb({ status: "failure", message: err });
+				}
+
+				console.log(
+					"SUCCESS",
+					"UPDATE_LOCATION",
+					`Updated location for user "${updatingUserId}" to location "${newLocation}".`
+				);
+
+				return cb({
+					status: "success",
+					message: "Location updated successfully"
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Updates a user's bio
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} updatingUserId - the updating user's id
+	 * @param {string} newBio - the new bio
+	 * @param {Function} cb - gets called with the result
+	 */
+	updateBio: isLoginRequired(async (session, updatingUserId, newBio, cb) => {
+		const userModel = await db.runJob("GET_MODEL", {
+			modelName: "user"
+		});
+
+		async.waterfall(
+			[
+				next => {
+					if (updatingUserId === session.userId) return next(null, true);
+					return userModel.findOne({ _id: session.userId }, next);
+				},
+
+				(user, next) => {
+					if (user !== true && (!user || user.role !== "admin")) return next("Invalid permissions.");
+					return userModel.findOne({ _id: updatingUserId }, next);
+				},
+
+				(user, next) => {
+					if (!user) return next("User not found.");
+					return userModel.updateOne(
+						{ _id: updatingUserId },
+						{ $set: { bio: newBio } },
+						{ runValidators: true },
+						next
+					);
+				}
+			],
+			async err => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"UPDATE_BIO",
+						`Couldn't update bio for user "${updatingUserId}" to bio "${newBio}". "${err}"`
+					);
+					cb({ status: "failure", message: err });
+				} else {
+					console.log(
+						"SUCCESS",
+						"UPDATE_BIO",
+						`Updated bio for user "${updatingUserId}" to bio "${newBio}".`
+					);
+					cb({
+						status: "success",
+						message: "Bio updated successfully"
+					});
+				}
+			}
+		);
+	}),
+
+	/**
+	 * Updates the type of a user's avatar
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} updatingUserId - the updating user's id
+	 * @param {string} newType - the new type
+	 * @param {Function} cb - gets called with the result
+	 */
+	updateAvatarType: isLoginRequired(async (session, updatingUserId, newType, cb) => {
+		const userModel = await db.runJob("GET_MODEL", {
+			modelName: "user"
+		});
+		async.waterfall(
+			[
+				next => {
+					if (updatingUserId === session.userId) return next(null, true);
+					return userModel.findOne({ _id: session.userId }, next);
+				},
+
+				(user, next) => {
+					if (user !== true && (!user || user.role !== "admin")) return next("Invalid permissions.");
+					return userModel.findOne({ _id: updatingUserId }, next);
+				},
+
+				(user, next) => {
+					if (!user) return next("User not found.");
+					return userModel.updateOne(
+						{ _id: updatingUserId },
+						{ $set: { "avatar.type": newType } },
+						{ runValidators: true },
+						next
+					);
+				}
+			],
+			async err => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"UPDATE_AVATAR_TYPE",
+						`Couldn't update avatar type for user "${updatingUserId}" to type "${newType}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+
+				console.log(
+					"SUCCESS",
+					"UPDATE_AVATAR_TYPE",
+					`Updated avatar type for user "${updatingUserId}" to type "${newType}".`
+				);
+				return cb({
+					status: "success",
+					message: "Avatar type updated successfully"
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Updates a user's role
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} updatingUserId - the updating user's id
+	 * @param {string} newRole - the new role
+	 * @param {Function} cb - gets called with the result
+	 */
+	updateRole: isAdminRequired(async (session, updatingUserId, newRole, cb) => {
+		newRole = newRole.toLowerCase();
+		const userModel = await db.runJob("GET_MODEL", {
+			modelName: "user"
+		});
+		async.waterfall(
+			[
+				next => {
+					userModel.findOne({ _id: updatingUserId }, next);
+				},
+
+				(user, next) => {
+					if (!user) return next("User not found.");
+					if (user.role === newRole) return next("New role can't be the same as the old role.");
+					return next();
+				},
+				next => {
+					userModel.updateOne(
+						{ _id: updatingUserId },
+						{ $set: { role: newRole } },
+						{ runValidators: true },
+						next
+					);
+				}
+			],
+			async err => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+
+					console.log(
+						"ERROR",
+						"UPDATE_ROLE",
+						`User "${session.userId}" couldn't update role for user "${updatingUserId}" to role "${newRole}". "${err}"`
+					);
+
+					return cb({ status: "failure", message: err });
+				}
+
+				console.log(
+					"SUCCESS",
+					"UPDATE_ROLE",
+					`User "${session.userId}" updated the role of user "${updatingUserId}" to role "${newRole}".`
+				);
+
+				return cb({
+					status: "success",
+					message: "Role successfully updated."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Updates a user's password
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} previousPassword - the previous password
+	 * @param {string} newPassword - the new password
+	 * @param {Function} cb - gets called with the result
+	 */
+	updatePassword: isLoginRequired(async (session, previousPassword, newPassword, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+
+		async.waterfall(
+			[
+				next => {
+					userModel.findOne({ _id: session.userId }, next);
+				},
+
+				(user, next) => {
+					if (!user.services.password) return next("This account does not have a password set.");
+					return next(null, user.services.password.password);
+				},
+
+				(storedPassword, next) => {
+					bcrypt.compare(sha256(previousPassword), storedPassword).then(res => {
+						if (res) return next();
+						return next("Please enter the correct previous password.");
+					});
+				},
+
+				next => {
+					if (!db.passwordValid(newPassword))
+						return next("Invalid new password. Check if it meets all the requirements.");
+					return next();
+				},
+
+				next => {
+					bcrypt.genSalt(10, next);
+				},
+
+				// hash the password
+				(salt, next) => {
+					bcrypt.hash(sha256(newPassword), salt, next);
+				},
+
+				(hashedPassword, next) => {
+					userModel.updateOne(
+						{ _id: session.userId },
+						{
+							$set: {
+								"services.password.password": hashedPassword
+							}
+						},
+						next
+					);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"UPDATE_PASSWORD",
+						`Failed updating user password of user '${session.userId}'. '${err}'.`
+					);
+					return cb({ status: "failure", message: err });
+				}
+
+				console.log("SUCCESS", "UPDATE_PASSWORD", `User '${session.userId}' updated their password.`);
+				return cb({
+					status: "success",
+					message: "Password successfully updated."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Requests a password for a session
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} email - the email of the user that requests a password reset
+	 * @param {Function} cb - gets called with the result
+	 */
+	requestPassword: isLoginRequired(async (session, cb) => {
+		const code = await utils.runJob("GENERATE_RANDOM_STRING", { length: 8 });
+		const passwordRequestSchema = await mail.runJob("GET_SCHEMA", {
+			schemaName: "passwordRequest"
+		});
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		async.waterfall(
+			[
+				next => {
+					userModel.findOne({ _id: session.userId }, next);
+				},
+
+				(user, next) => {
+					if (!user) return next("User not found.");
+					if (user.services.password && user.services.password.password)
+						return next("You already have a password set.");
+					return next(null, user);
+				},
+
+				(user, next) => {
+					const expires = new Date();
+					expires.setDate(expires.getDate() + 1);
+					userModel.findOneAndUpdate(
+						{ "email.address": user.email.address },
+						{
+							$set: {
+								"services.password": {
+									set: { code, expires }
+								}
+							}
+						},
+						{ runValidators: true },
+						next
+					);
+				},
+
+				(user, next) => {
+					passwordRequestSchema(user.email.address, user.username, code, next);
+				}
+			],
+			async err => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+
+					console.log(
+						"ERROR",
+						"REQUEST_PASSWORD",
+						`UserId '${session.userId}' failed to request password. '${err}'`
+					);
+
+					return cb({ status: "failure", message: err });
+				}
+
+				console.log(
+					"SUCCESS",
+					"REQUEST_PASSWORD",
+					`UserId '${session.userId}' successfully requested a password.`
+				);
+
+				return cb({
+					status: "success",
+					message: "Successfully requested password."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Verifies a password code
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} code - the password code
+	 * @param {Function} cb - gets called with the result
+	 */
+	verifyPasswordCode: isLoginRequired(async (session, code, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		async.waterfall(
+			[
+				next => {
+					if (!code || typeof code !== "string") return next("Invalid code.");
+					return userModel.findOne(
+						{
+							"services.password.set.code": code,
+							_id: session.userId
+						},
+						next
+					);
+				},
+
+				(user, next) => {
+					if (!user) return next("Invalid code.");
+					if (user.services.password.set.expires < new Date()) return next("That code has expired.");
+					return next(null);
+				}
+			],
+			async err => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "VERIFY_PASSWORD_CODE", `Code '${code}' failed to verify. '${err}'`);
+					cb({ status: "failure", message: err });
+				} else {
+					console.log("SUCCESS", "VERIFY_PASSWORD_CODE", `Code '${code}' successfully verified.`);
+					cb({
+						status: "success",
+						message: "Successfully verified password code."
+					});
+				}
+			}
+		);
+	}),
+
+	/**
+	 * Adds a password to a user with a code
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} code - the password code
+	 * @param {string} newPassword - the new password code
+	 * @param {Function} cb - gets called with the result
+	 */
+	changePasswordWithCode: isLoginRequired(async (session, code, newPassword, cb) => {
+		const userModel = await db.runJob("GET_MODEL", {
+			modelName: "user"
+		});
+		async.waterfall(
+			[
+				next => {
+					if (!code || typeof code !== "string") return next("Invalid code.");
+					return userModel.findOne({ "services.password.set.code": code }, next);
+				},
+
+				(user, next) => {
+					if (!user) return next("Invalid code.");
+					if (!user.services.password.set.expires > new Date()) return next("That code has expired.");
+					return next();
+				},
+
+				next => {
+					if (!db.passwordValid(newPassword))
+						return next("Invalid password. Check if it meets all the requirements.");
+					return next();
+				},
+
+				next => {
+					bcrypt.genSalt(10, next);
+				},
+
+				// hash the password
+				(salt, next) => {
+					bcrypt.hash(sha256(newPassword), salt, next);
+				},
+
+				(hashedPassword, next) => {
+					userModel.updateOne(
+						{ "services.password.set.code": code },
+						{
+							$set: {
+								"services.password.password": hashedPassword
+							},
+							$unset: { "services.password.set": "" }
+						},
+						{ runValidators: true },
+						next
+					);
+				}
+			],
+			async err => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "ADD_PASSWORD_WITH_CODE", `Code '${code}' failed to add password. '${err}'`);
+					cb({ status: "failure", message: err });
+				} else {
+					console.log("SUCCESS", "ADD_PASSWORD_WITH_CODE", `Code '${code}' successfully added password.`);
+					cache.runJob("PUB", {
+						channel: "user.linkPassword",
+						value: session.userId
+					});
+					cb({
+						status: "success",
+						message: "Successfully added password."
+					});
+				}
+			}
+		);
+	}),
+
+	/**
+	 * Unlinks password from user
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {Function} cb - gets called with the result
+	 */
+	unlinkPassword: isLoginRequired(async (session, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		async.waterfall(
+			[
+				next => {
+					userModel.findOne({ _id: session.userId }, next);
+				},
+
+				(user, next) => {
+					if (!user) return next("Not logged in.");
+					if (!user.services.github || !user.services.github.id)
+						return next("You can't remove password login without having GitHub login.");
+					return userModel.updateOne({ _id: session.userId }, { $unset: { "services.password": "" } }, next);
+				}
+			],
+			async err => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"UNLINK_PASSWORD",
+						`Unlinking password failed for userId '${session.userId}'. '${err}'`
+					);
+					cb({ status: "failure", message: err });
+				} else {
+					console.log(
+						"SUCCESS",
+						"UNLINK_PASSWORD",
+						`Unlinking password successful for userId '${session.userId}'.`
+					);
+					cache.runJob("PUB", {
+						channel: "user.unlinkPassword",
+						value: session.userId
+					});
+					cb({
+						status: "success",
+						message: "Successfully unlinked password."
+					});
+				}
+			}
+		);
+	}),
+
+	/**
+	 * Unlinks GitHub from user
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {Function} cb - gets called with the result
+	 */
+	unlinkGitHub: isLoginRequired(async (session, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		async.waterfall(
+			[
+				next => {
+					userModel.findOne({ _id: session.userId }, next);
+				},
+
+				(user, next) => {
+					if (!user) return next("Not logged in.");
+					if (!user.services.password || !user.services.password.password)
+						return next("You can't remove GitHub login without having password login.");
+					return userModel.updateOne({ _id: session.userId }, { $unset: { "services.github": "" } }, next);
+				}
+			],
+			async err => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"UNLINK_GITHUB",
+						`Unlinking GitHub failed for userId '${session.userId}'. '${err}'`
+					);
+					cb({ status: "failure", message: err });
+				} else {
+					console.log(
+						"SUCCESS",
+						"UNLINK_GITHUB",
+						`Unlinking GitHub successful for userId '${session.userId}'.`
+					);
+					cache.runJob("PUB", {
+						channel: "user.unlinkGithub",
+						value: session.userId
+					});
+					cb({
+						status: "success",
+						message: "Successfully unlinked GitHub."
+					});
+				}
+			}
+		);
+	}),
+
+	/**
+	 * Requests a password reset for an email
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} email - the email of the user that requests a password reset
+	 * @param {Function} cb - gets called with the result
+	 */
+	requestPasswordReset: async (session, email, cb) => {
+		const code = await utils.runJob("GENERATE_RANDOM_STRING", { length: 8 });
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+
+		const resetPasswordRequestSchema = await mail.runJob("GET_SCHEMA", {
+			schemaName: "resetPasswordRequest"
+		});
+
+		async.waterfall(
+			[
+				next => {
+					if (!email || typeof email !== "string") return next("Invalid email.");
+					email = email.toLowerCase();
+					return userModel.findOne({ "email.address": email }, next);
+				},
+
+				(user, next) => {
+					if (!user) return next("User not found.");
+					if (!user.services.password || !user.services.password.password)
+						return next("User does not have a password set, and probably uses GitHub to log in.");
+					return next(null, user);
+				},
+
+				(user, next) => {
+					const expires = new Date();
+					expires.setDate(expires.getDate() + 1);
+					userModel.findOneAndUpdate(
+						{ "email.address": email },
+						{
+							$set: {
+								"services.password.reset": {
+									code,
+									expires
+								}
+							}
+						},
+						{ runValidators: true },
+						next
+					);
+				},
+
+				(user, next) => {
+					resetPasswordRequestSchema(user.email.address, user.username, code, next);
+				}
+			],
+			async err => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"REQUEST_PASSWORD_RESET",
+						`Email '${email}' failed to request password reset. '${err}'`
+					);
+					cb({ status: "failure", message: err });
+				} else {
+					console.log(
+						"SUCCESS",
+						"REQUEST_PASSWORD_RESET",
+						`Email '${email}' successfully requested a password reset.`
+					);
+					cb({
+						status: "success",
+						message: "Successfully requested password reset."
+					});
+				}
+			}
+		);
+	},
+
+	/**
+	 * Verifies a reset code
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} code - the password reset code
+	 * @param {Function} cb - gets called with the result
+	 */
+	verifyPasswordResetCode: async (session, code, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		async.waterfall(
+			[
+				next => {
+					if (!code || typeof code !== "string") return next("Invalid code.");
+					return userModel.findOne({ "services.password.reset.code": code }, next);
+				},
+
+				(user, next) => {
+					if (!user) return next("Invalid code.");
+					if (!user.services.password.reset.expires > new Date()) return next("That code has expired.");
+					return next(null);
+				}
+			],
+			async err => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "VERIFY_PASSWORD_RESET_CODE", `Code '${code}' failed to verify. '${err}'`);
+					cb({ status: "failure", message: err });
+				} else {
+					console.log("SUCCESS", "VERIFY_PASSWORD_RESET_CODE", `Code '${code}' successfully verified.`);
+					cb({
+						status: "success",
+						message: "Successfully verified password reset code."
+					});
+				}
+			}
+		);
+	},
+
+	/**
+	 * Changes a user's password with a reset code
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} code - the password reset code
+	 * @param {string} newPassword - the new password reset code
+	 * @param {Function} cb - gets called with the result
+	 */
+	changePasswordWithResetCode: async (session, code, newPassword, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		async.waterfall(
+			[
+				next => {
+					if (!code || typeof code !== "string") return next("Invalid code.");
+					return userModel.findOne({ "services.password.reset.code": code }, next);
+				},
+
+				(user, next) => {
+					if (!user) return next("Invalid code.");
+					if (!user.services.password.reset.expires > new Date()) return next("That code has expired.");
+					return next();
+				},
+
+				next => {
+					if (!db.passwordValid(newPassword))
+						return next("Invalid password. Check if it meets all the requirements.");
+					return next();
+				},
+
+				next => {
+					bcrypt.genSalt(10, next);
+				},
+
+				// hash the password
+				(salt, next) => {
+					bcrypt.hash(sha256(newPassword), salt, next);
+				},
+
+				(hashedPassword, next) => {
+					userModel.updateOne(
+						{ "services.password.reset.code": code },
+						{
+							$set: {
+								"services.password.password": hashedPassword
+							},
+							$unset: { "services.password.reset": "" }
+						},
+						{ runValidators: true },
+						next
+					);
+				}
+			],
+			async err => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"CHANGE_PASSWORD_WITH_RESET_CODE",
+						`Code '${code}' failed to change password. '${err}'`
+					);
+					cb({ status: "failure", message: err });
+				} else {
+					console.log(
+						"SUCCESS",
+						"CHANGE_PASSWORD_WITH_RESET_CODE",
+						`Code '${code}' successfully changed password.`
+					);
+					cb({
+						status: "success",
+						message: "Successfully changed password."
+					});
+				}
+			}
+		);
+	},
+
+	/**
+	 * Bans a user by userId
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} value - the user id that is going to be banned
+	 * @param {string} reason - the reason for the ban
+	 * @param {string} expiresAt - the time the ban expires
+	 * @param {Function} cb - gets called with the result
+	 */
+	banUserById: isAdminRequired((session, userId, reason, expiresAt, cb) => {
+		async.waterfall(
+			[
+				next => {
+					if (!userId) return next("You must provide a userId to ban.");
+					if (!reason) return next("You must provide a reason for the ban.");
+					return next();
+				},
+
+				next => {
+					if (!expiresAt || typeof expiresAt !== "string") return next("Invalid expire date.");
+					const date = new Date();
+					switch (expiresAt) {
+						case "1h":
+							expiresAt = date.setHours(date.getHours() + 1);
+							break;
+						case "12h":
+							expiresAt = date.setHours(date.getHours() + 12);
+							break;
+						case "1d":
+							expiresAt = date.setDate(date.getDate() + 1);
+							break;
+						case "1w":
+							expiresAt = date.setDate(date.getDate() + 7);
+							break;
+						case "1m":
+							expiresAt = date.setMonth(date.getMonth() + 1);
+							break;
+						case "3m":
+							expiresAt = date.setMonth(date.getMonth() + 3);
+							break;
+						case "6m":
+							expiresAt = date.setMonth(date.getMonth() + 6);
+							break;
+						case "1y":
+							expiresAt = date.setFullYear(date.getFullYear() + 1);
+							break;
+						case "never":
+							expiresAt = new Date(3093527980800000);
+							break;
+						default:
+							return next("Invalid expire date.");
+					}
+
+					return next();
+				},
+
+				next => {
+					punishments
+						.runJob("ADD_PUNISHMENT", {
+							type: "banUserId",
+							value: userId,
+							reason,
+							expiresAt,
+							punishedBy: "" // needs changed
+						})
+						.then(punishment => next(null, punishment))
+						.catch(next);
+				},
+
+				(punishment, next) => {
+					cache.runJob("PUB", {
+						channel: "user.ban",
+						value: { userId, punishment }
+					});
+					next();
+				}
+			],
+			async err => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"BAN_USER_BY_ID",
+						`User ${session.userId} failed to ban user ${userId} with the reason ${reason}. '${err}'`
+					);
+					cb({ status: "failure", message: err });
+				} else {
+					console.log(
+						"SUCCESS",
+						"BAN_USER_BY_ID",
+						`User ${session.userId} has successfully banned user ${userId} with the reason ${reason}.`
+					);
+					cb({
+						status: "success",
+						message: "Successfully banned user."
+					});
+				}
+			}
+		);
+	}),
+
+	getFavoriteStations: isLoginRequired(async (session, cb) => {
+		const userModel = await db.runJob("GET_MODEL", { modelName: "user" });
+		async.waterfall(
+			[
+				next => {
+					userModel.findOne({ _id: session.userId }, next);
+				},
+
+				(user, next) => {
+					if (!user) return next("User not found.");
+					return next(null, user);
+				}
+			],
+			async (err, user) => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log(
+						"ERROR",
+						"GET_FAVORITE_STATIONS",
+						`User ${session.userId} failed to get favorite stations. '${err}'`
+					);
+					cb({ status: "failure", message: err });
+				} else {
+					console.log("SUCCESS", "GET_FAVORITE_STATIONS", `User ${session.userId} got favorite stations.`);
+					cb({
+						status: "success",
+						favoriteStations: user.favoriteStations
+					});
+				}
+			}
+		);
+	})
 };

+ 79 - 91
backend/logic/actions/utils.js

@@ -1,98 +1,86 @@
-"use strict";
+import async from "async";
 
-const async = require("async");
+import { isAdminRequired, isLoginRequired, isOwnerRequired } from "./hooks";
 
-const hooks = require("./hooks");
+import utils from "../utils";
 
-const utils = require("../utils");
+export default {
+	getModules: isAdminRequired((session, cb) => {
+		async.waterfall(
+			[
+				next => {
+					next(null, utils.moduleManager.modules);
+				},
 
-module.exports = {
-    getModules: hooks.adminRequired((session, cb) => {
-        async.waterfall(
-            [
-                (next) => {
-                    next(null, utils.moduleManager.modules);
-                },
+				(modules, next) => {
+					// console.log(modules, next);
+					next(
+						null,
+						Object.keys(modules).map(moduleName => {
+							const module = modules[moduleName];
+							return {
+								name: module.name,
+								status: module.status,
+								stage: module.stage,
+								jobsInQueue: module.jobQueue.length(),
+								jobsInProgress: module.jobQueue.running(),
+								concurrency: module.jobQueue.concurrency
+							};
+						})
+					);
+				}
+			],
+			async (err, modules) => {
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "GET_MODULES", `User ${session.userId} failed to get modules. '${err}'`);
+					cb({ status: "failure", message: err });
+				} else {
+					console.log(
+						"SUCCESS",
+						"GET_MODULES",
+						`User ${session.userId} has successfully got the modules info.`
+					);
+					cb({
+						status: "success",
+						message: "Successfully got modules.",
+						modules
+					});
+				}
+			}
+		);
+	}),
 
-                (modules, next) => {
-                    // console.log(modules, next);
-                    next(
-                        null,
-                        Object.keys(modules).map((moduleName) => {
-                            const module = modules[moduleName];
-                            return {
-                                name: module.name,
-                                status: module.status,
-                                stage: module.stage,
-                                jobsInQueue: module.jobQueue.length(),
-                                jobsInProgress: module.jobQueue.running(),
-                                concurrency: module.jobQueue.concurrency,
-                            };
-                        })
-                    );
-                },
-            ],
-            async (err, modules) => {
-                if (err && err !== true) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "GET_MODULES",
-                        `User ${session.userId} failed to get modules. '${err}'`
-                    );
-                    cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "GET_MODULES",
-                        `User ${session.userId} has successfully got the modules info.`
-                    );
-                    cb({
-                        status: "success",
-                        message: "Successfully got modules.",
-                        modules,
-                    });
-                }
-            }
-        );
-    }),
+	getModule: isAdminRequired((session, moduleName, cb) => {
+		async.waterfall(
+			[
+				next => {
+					next(null, utils.moduleManager.modules[moduleName]);
+				}
+			],
+			async (err, module) => {
+				const jobsInQueue = module.jobQueue._tasks.heap.map(task => task.data);
 
-    getModule: hooks.adminRequired((session, moduleName, cb) => {
-        async.waterfall(
-            [
-                (next) => {
-                    next(null, utils.moduleManager.modules[moduleName]);
-                },
-            ],
-            async (err, module) => {
-                const jobsInQueue = module.jobQueue._tasks.heap.map((task) => {
-                    return task.data;
-                });
-
-                // console.log(module.runningJobs);
-                if (err && err !== true) {
-                    err = await utils.runJob("GET_ERROR", { error: err });
-                    console.log(
-                        "ERROR",
-                        "GET_MODULE",
-                        `User ${session.userId} failed to get module. '${err}'`
-                    );
-                    cb({ status: "failure", message: err });
-                } else {
-                    console.log(
-                        "SUCCESS",
-                        "GET_MODULE",
-                        `User ${session.userId} has successfully got the module info.`
-                    );
-                    cb({
-                        status: "success",
-                        message: "Successfully got module info.",
-                        runningJobs: module.runningJobs,
-                        jobStatistics: module.jobStatistics,
-                        jobsInQueue,
-                    });
-                }
-            }
-        );
-    }),
+				// console.log(module.runningJobs);
+				if (err && err !== true) {
+					err = await utils.runJob("GET_ERROR", { error: err });
+					console.log("ERROR", "GET_MODULE", `User ${session.userId} failed to get module. '${err}'`);
+					cb({ status: "failure", message: err });
+				} else {
+					console.log(
+						"SUCCESS",
+						"GET_MODULE",
+						`User ${session.userId} has successfully got the module info.`
+					);
+					cb({
+						status: "success",
+						message: "Successfully got module info.",
+						runningJobs: module.runningJobs,
+						jobStatistics: module.jobStatistics,
+						jobsInQueue
+					});
+				}
+			}
+		);
+	})
 };

+ 64 - 70
backend/logic/activities.js

@@ -1,80 +1,74 @@
-const CoreClass = require("../core.js");
+import async from "async";
 
-const async = require("async");
-const mongoose = require("mongoose");
+import CoreClass from "../core";
 
 class ActivitiesModule extends CoreClass {
-    constructor() {
-        super("activities");
-    }
+	constructor() {
+		super("activities");
+	}
 
-    initialize() {
-        return new Promise((resolve, reject) => {
-            this.db = this.moduleManager.modules["db"];
-            this.io = this.moduleManager.modules["io"];
-            this.utils = this.moduleManager.modules["utils"];
+	initialize() {
+		return new Promise(resolve => {
+			this.db = this.moduleManager.modules.db;
+			this.io = this.moduleManager.modules.io;
+			this.utils = this.moduleManager.modules.utils;
 
-            resolve();
-        });
-    }
+			resolve();
+		});
+	}
 
-    // TODO: Migrate
-    ADD_ACTIVITY(payload) {
-        //userId, activityType, payload
-        return new Promise((resolve, reject) => {
-            async.waterfall(
-                [
-                    (next) => {
-                        this.db
-                            .runJob("GET_MODEL", { modelName: "activity" })
-                            .then((res) => {
-                                next(null, res);
-                            })
-                            .catch(next);
-                    },
-                    (activityModel, next) => {
-                        const activity = new activityModel({
-                            userId: payload.userId,
-                            activityType: payload.activityType,
-                            payload: payload.payload,
-                        });
+	// TODO: Migrate
+	ADD_ACTIVITY(payload) {
+		// userId, activityType, payload
+		return new Promise((resolve, reject) => {
+			async.waterfall(
+				[
+					next => {
+						this.db
+							.runJob("GET_MODEL", { modelName: "activity" })
+							.then(res => next(null, res))
+							.catch(next);
+					},
+					(ActivityModel, next) => {
+						const activity = new ActivityModel({
+							userId: payload.userId,
+							activityType: payload.activityType,
+							payload: payload.payload
+						});
 
-                        activity.save((err, activity) => {
-                            if (err) return next(err);
-                            next(null, activity);
-                        });
-                    },
+						activity.save((err, activity) => {
+							if (err) return next(err);
+							return next(null, activity);
+						});
+					},
 
-                    (activity, next) => {
-                        this.utils
-                            .runJob("SOCKETS_FROM_USER", {
-                                userId: activity.userId,
-                            })
-                            .then((response) => {
-                                response.sockets.forEach((socket) => {
-                                    socket.emit(
-                                        "event:activity.create",
-                                        activity
-                                    );
-                                });
-                                next();
-                            })
-                            .catch(next);
-                    },
-                ],
-                async (err, activity) => {
-                    if (err) {
-                        err = await this.utils.runJob("GET_ERROR", {
-                            error: err,
-                        });
-                        reject(new Error(err));
-                    } else {
-                        resolve({ activity });
-                    }
-                }
-            );
-        });
-    }
+					(activity, next) => {
+						this.utils
+							.runJob("SOCKETS_FROM_USER", {
+								userId: activity.userId
+							})
+							.then(response => {
+								response.sockets.forEach(socket => {
+									socket.emit("event:activity.create", activity);
+								});
+								next();
+							})
+							.catch(next);
+					}
+				],
+				async (err, activity) => {
+					if (err) {
+						err = await this.utils.runJob("GET_ERROR", {
+							error: err
+						});
+						reject(new Error(err));
+					} else {
+						resolve({ activity });
+					}
+				}
+			);
+		});
+	}
 }
 
-module.exports = new ActivitiesModule();
+export default new ActivitiesModule();

+ 238 - 252
backend/logic/api.js

@@ -1,257 +1,243 @@
-const CoreClass = require("../core.js");
+import config from "config";
 
-const async = require("async");
-const config = require("config");
-const crypto = require("crypto");
+import async from "async";
+
+import CoreClass from "../core";
 
 class APIModule extends CoreClass {
-    constructor() {
-        super("api");
-    }
-
-    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"];
-
-            const SIDname = config.get("cookie.SIDname");
-
-            const actions = require("./actions");
-
-            const isLoggedIn = (req, res, next) => {
-                let SID;
-                async.waterfall(
-                    [
-                        next => {
-                            this.utils
-                                .runJob("PARSE_COOKIES", {
-                                    cookieString: req.headers.cookie,
-                                })
-                                .then(res => {
-                                    SID = res[SIDname];
-                                    next(null);
-                                }).catch(next);
-                        },
-
-                        next => {
-                            if (!SID) return next("No SID.");
-                            next();
-                        },
-
-                        (next) => {
-                            this.cache
-                                .runJob("HGET", { table: "sessions", key: SID })
-                                .then((session) => {
-                                    next(null, session);
-                                });
-                        },
-
-                        (session, next) => {
-                            if (!session) return next("No session found.");
-
-                            session.refreshDate = Date.now();
-
-                            req.session = session;
-                            this.cache
-                                .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", {})
-                                .then((punishments) => {
-                                    const isLoggedIn = !!(
-                                        req.session &&
-                                        req.session.refreshDate
-                                    );
-                                    const userId = isLoggedIn
-                                        ? req.session.userId
-                                        : null;
-
-                                    let banishment = { banned: false, ban: 0 };
-
-                                    punishments.forEach((punishment) => {
-                                        if (
-                                            punishment.expiresAt >
-                                            banishment.ban
-                                        )
-                                            banishment.ban = punishment;
-                                        if (
-                                            punishment.type === "banUserId" &&
-                                            isLoggedIn &&
-                                            punishment.value === userId
-                                        )
-                                            banishment.banned = true;
-                                        if (
-                                            punishment.type === "banUserIp" &&
-                                            punishment.value === req.ip
-                                        )
-                                            banishment.banned = true;
-                                    });
-
-                                    req.banishment = banishment;
-
-                                    next();
-                                })
-                                .catch(() => {
-                                    next();
-                                });
-                        },
-                    ],
-                    (err) => {
-                        if (err)
-                            return res.json({status: "error", message: "You are not logged in"});                        
-                        next();
-                    }
-                );
-            }
-
-            this.app.runJob("GET_APP", {})
-                .then((response) => {
-                    response.app.get("/", (req, res) => {
-                        res.json({
-                            status: "success",
-                            message: "Coming Soon",
-                        });
-                    });
-
-                    response.app.get("/export/privatePlaylist/:playlistId", isLoggedIn, (req, res) => {
-                        const playlistId = req.params.playlistId;
-                        this.playlists.runJob("GET_PLAYLIST", { playlistId })
-                            .then(playlist => {
-                                if (playlist.createdBy === req.session.userId)
-                                    res.json({status: "success", playlist });
-                                else
-                                    res.json({status: "error", message: "You're not the owner."});
-                            })
-                            .catch(err => {
-                                res.json({status: "error", message: err.message});
-                            });
-                    });
-
-                    // 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);
-                    //     });
-                    // });
-
-                    // 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);
-                    //             });
-                    //         });
-                    //     });
-                    // });
-
-                    resolve();
-                })
-                .catch((err) => {
-                    reject(err);
-                });
-        });
-    }
+	constructor() {
+		super("api");
+	}
+
+	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;
+
+			const SIDname = config.get("cookie.SIDname");
+
+			const isLoggedIn = (req, res, next) => {
+				let SID;
+				async.waterfall(
+					[
+						next => {
+							this.utils
+								.runJob("PARSE_COOKIES", {
+									cookieString: req.headers.cookie
+								})
+								.then(res => {
+									SID = res[SIDname];
+									next(null);
+								})
+								.catch(next);
+						},
+
+						next => {
+							if (!SID) return next("No SID.");
+							return next();
+						},
+
+						next => {
+							this.cache
+								.runJob("HGET", { table: "sessions", key: SID })
+								.then(session => next(null, session));
+						},
+
+						(session, next) => {
+							if (!session) return next("No session found.");
+
+							session.refreshDate = Date.now();
+
+							req.session = session;
+
+							return this.cache
+								.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", {})
+								.then(punishments => {
+									const isLoggedIn = !!(req.session && req.session.refreshDate);
+									const userId = isLoggedIn ? req.session.userId : null;
+
+									const banishment = { banned: false, ban: 0 };
+
+									punishments.forEach(punishment => {
+										if (punishment.expiresAt > banishment.ban) banishment.ban = punishment;
+										if (
+											punishment.type === "banUserId" &&
+											isLoggedIn &&
+											punishment.value === userId
+										)
+											banishment.banned = true;
+										if (punishment.type === "banUserIp" && punishment.value === req.ip)
+											banishment.banned = true;
+									});
+
+									req.banishment = banishment;
+
+									next();
+								})
+								.catch(() => {
+									next();
+								});
+						}
+					],
+					err => {
+						if (err) return res.json({ status: "error", message: "You are not logged in" });
+						return next();
+					}
+				);
+			};
+
+			this.app
+				.runJob("GET_APP", {})
+				.then(response => {
+					response.app.get("/", (req, res) => {
+						res.json({
+							status: "success",
+							message: "Coming Soon"
+						});
+					});
+
+					response.app.get("/export/privatePlaylist/:playlistId", isLoggedIn, (req, res) => {
+						const { playlistId } = req.params;
+						this.playlists
+							.runJob("GET_PLAYLIST", { playlistId })
+							.then(playlist => {
+								if (playlist.createdBy === req.session.userId)
+									res.json({ status: "success", playlist });
+								else res.json({ status: "error", message: "You're not the owner." });
+							})
+							.catch(err => {
+								res.json({ status: "error", message: err.message });
+							});
+					});
+
+					// 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);
+					//     });
+					// });
+
+					// 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);
+					//             });
+					//         });
+					//     });
+					// });
+
+					resolve();
+				})
+				.catch(err => {
+					reject(err);
+				});
+		});
+	}
 }
 
-module.exports = new APIModule();
+export default new APIModule();

+ 453 - 535
backend/logic/app.js

@@ -1,540 +1,458 @@
-const CoreClass = require("../core.js");
+import config from "config";
 
-const express = require("express");
-const bodyParser = require("body-parser");
-const cookieParser = require("cookie-parser");
-const cors = require("cors");
-const config = require("config");
-const async = require("async");
-const request = require("request");
-const OAuth2 = require("oauth").OAuth2;
+import async from "async";
+import request from "request";
+
+import cors from "cors";
+import cookieParser from "cookie-parser";
+import bodyParser from "body-parser";
+import express from "express";
+import { OAuth2 } from "oauth";
+import CoreClass from "../core";
 
 class AppModule extends CoreClass {
-    constructor() {
-        super("app");
-    }
-
-    initialize() {
-        return new Promise(async (resolve, reject) => {
-            const mail = this.moduleManager.modules["mail"],
-                cache = this.moduleManager.modules["cache"],
-                db = this.moduleManager.modules["db"],
-                activities = this.moduleManager.modules["activities"];
-
-            this.utils = this.moduleManager.modules["utils"];
-
-            let app = (this.app = express());
-            const SIDname = config.get("cookie.SIDname");
-            this.server = app.listen(config.get("serverPort"));
-
-            app.use(cookieParser());
-
-            app.use(bodyParser.json());
-            app.use(bodyParser.urlencoded({ extended: true }));
-
-            const userModel = await db.runJob("GET_MODEL", {
-                modelName: "user",
-            });
-
-            let corsOptions = Object.assign({}, config.get("cors"));
-
-            app.use(cors(corsOptions));
-            app.options("*", cors(corsOptions));
-
-            let oauth2 = new OAuth2(
-                config.get("apis.github.client"),
-                config.get("apis.github.secret"),
-                "https://github.com/",
-                "login/oauth/authorize",
-                "login/oauth/access_token",
-                null
-            );
-
-            let redirect_uri =
-                config.get("serverDomain") + "/auth/github/authorize/callback";
-
-            app.get("/auth/github/authorize", async (req, res) => {
-                if (this.getStatus() !== "READY") {
-                    this.log(
-                        "INFO",
-                        "APP_REJECTED_GITHUB_AUTHORIZE",
-                        `A user tried to use github authorize, but the APP module is currently not ready.`
-                    );
-                    return redirectOnErr(
-                        res,
-                        "Something went wrong on our end. Please try again later."
-                    );
-                }
-
-                let params = [
-                    `client_id=${config.get("apis.github.client")}`,
-                    `redirect_uri=${config.get(
-                        "serverDomain"
-                    )}/auth/github/authorize/callback`,
-                    `scope=user:email`,
-                ].join("&");
-                res.redirect(
-                    `https://github.com/login/oauth/authorize?${params}`
-                );
-            });
-
-            app.get("/auth/github/link", async (req, res) => {
-                if (this.getStatus() !== "READY") {
-                    this.log(
-                        "INFO",
-                        "APP_REJECTED_GITHUB_AUTHORIZE",
-                        `A user tried to use github authorize, but the APP module is currently not ready.`
-                    );
-                    return redirectOnErr(
-                        res,
-                        "Something went wrong on our end. Please try again later."
-                    );
-                }
-                let params = [
-                    `client_id=${config.get("apis.github.client")}`,
-                    `redirect_uri=${config.get(
-                        "serverDomain"
-                    )}/auth/github/authorize/callback`,
-                    `scope=user:email`,
-                    `state=${req.cookies[SIDname]}`,
-                ].join("&");
-                res.redirect(
-                    `https://github.com/login/oauth/authorize?${params}`
-                );
-            });
-
-            function redirectOnErr(res, err) {
-                return res.redirect(
-                    `${config.get("domain")}/?err=${encodeURIComponent(err)}`
-                );
-            }
-
-            app.get("/auth/github/authorize/callback", async (req, res) => {
-                if (this.getStatus() !== "READY") {
-                    this.log(
-                        "INFO",
-                        "APP_REJECTED_GITHUB_AUTHORIZE",
-                        `A user tried to use github authorize, but the APP module is currently not ready.`
-                    );
-                    return redirectOnErr(
-                        res,
-                        "Something went wrong on our end. Please try again later."
-                    );
-                }
-
-                let code = req.query.code;
-                let access_token;
-                let body;
-                let address;
-
-                const state = req.query.state;
-
-                const verificationToken = await this.utils.runJob(
-                    "GENERATE_RANDOM_STRING",
-                    { length: 64 }
-                );
-
-                async.waterfall(
-                    [
-                        (next) => {
-                            if (req.query.error)
-                                return next(req.query.error_description);
-                            next();
-                        },
-
-                        (next) => {
-                            oauth2.getOAuthAccessToken(
-                                code,
-                                { redirect_uri },
-                                next
-                            );
-                        },
-
-                        (_access_token, refresh_token, results, next) => {
-                            if (results.error)
-                                return next(results.error_description);
-                            access_token = _access_token;
-                            request.get(
-                                {
-                                    url: `https://api.github.com/user`,
-                                    headers: {
-                                        "User-Agent": "request",
-                                        Authorization: `token ${access_token}`,
-                                    },
-                                },
-                                next
-                            );
-                        },
-
-                        (httpResponse, _body, next) => {
-                            body = _body = JSON.parse(_body);
-                            if (httpResponse.statusCode !== 200)
-                                return next(body.message);
-                            if (state) {
-                                return async.waterfall(
-                                    [
-                                        (next) => {
-                                            cache
-                                                .runJob("HGET", {
-                                                    table: "sessions",
-                                                    key: state,
-                                                })
-                                                .then((session) =>
-                                                    next(null, session)
-                                                )
-                                                .catch(next);
-                                        },
-
-                                        (session, next) => {
-                                            if (!session)
-                                                return next("Invalid session.");
-                                            userModel.findOne(
-                                                { _id: session.userId },
-                                                next
-                                            );
-                                        },
-
-                                        (user, next) => {
-                                            if (!user)
-                                                return next("User not found.");
-                                            if (
-                                                user.services.github &&
-                                                user.services.github.id
-                                            )
-                                                return next(
-                                                    "Account already has GitHub linked."
-                                                );
-                                            userModel.updateOne(
-                                                { _id: user._id },
-                                                {
-                                                    $set: {
-                                                        "services.github": {
-                                                            id: body.id,
-                                                            access_token,
-                                                        },
-                                                    },
-                                                },
-                                                { runValidators: true },
-                                                (err) => {
-                                                    if (err) return next(err);
-                                                    next(null, user, body);
-                                                }
-                                            );
-                                        },
-
-                                        (user) => {
-                                            cache.runJob("PUB", {
-                                                channel: "user.linkGithub",
-                                                value: user._id,
-                                            });
-                                            res.redirect(
-                                                `${config.get(
-                                                    "domain"
-                                                )}/settings#security`
-                                            );
-                                        },
-                                    ],
-                                    next
-                                );
-                            }
-
-                            if (!body.id)
-                                return next("Something went wrong, no id.");
-
-                            userModel.findOne(
-                                { "services.github.id": body.id },
-                                (err, user) => {
-                                    next(err, user, body);
-                                }
-                            );
-                        },
-
-                        (user, body, next) => {
-                            if (user) {
-                                user.services.github.access_token = access_token;
-                                return user.save(() => {
-                                    next(true, user._id);
-                                });
-                            }
-
-                            userModel.findOne(
-                                {
-                                    username: new RegExp(
-                                        `^${body.login}$`,
-                                        "i"
-                                    ),
-                                },
-                                (err, user) => {
-                                    next(err, user);
-                                }
-                            );
-                        },
-
-                        (user, next) => {
-                            if (user)
-                                return next(
-                                    `An account with that username already exists.`
-                                );
-
-                            request.get(
-                                {
-                                    url: `https://api.github.com/user/emails`,
-                                    headers: {
-                                        "User-Agent": "request",
-                                        Authorization: `token ${access_token}`,
-                                    },
-                                },
-                                next
-                            );
-                        },
-
-                        (httpResponse, body2, next) => {
-                            body2 = JSON.parse(body2);
-                            if (!Array.isArray(body2))
-                                return next(body2.message);
-
-                            body2.forEach((email) => {
-                                if (email.primary)
-                                    address = email.email.toLowerCase();
-                            });
-
-                            userModel.findOne(
-                                { "email.address": address },
-                                next
-                            );
-                        },
-
-                        (user, next) => {
-                            this.utils
-                                .runJob("GENERATE_RANDOM_STRING", {
-                                    length: 12,
-                                })
-                                .then((_id) => {
-                                    next(null, user, _id);
-                                });
-                        },
-
-                        (user, _id, next) => {
-                            if (user) {
-                                if (Object.keys(JSON.parse(user.services.github)).length === 0)
-                                    return next(`An account with that email address exists, but is not linked to GitHub.`)    
-                                else
-                                    return next(`An account with that email address already exists.`);
-                            }
-
-                            next(null, {
-                                _id, //TODO Check if exists
-                                username: body.login,
-                                name: body.name,
-                                location: body.location,
-                                bio: body.bio,
-                                email: {
-                                    address,
-                                    verificationToken,
-                                },
-                                services: {
-                                    github: { id: body.id, access_token },
-                                },
-                            });
-                        },
-
-                        // 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);
-                                });
-                        },
-
-                        // save the new user to the database
-                        (user, next) => {
-                            userModel.create(user, next);
-                        },
-
-                        // add the activity of account creation
-                        (user, next) => {
-                            activities.runJob("ADD_ACTIVITY", {
-                                userId: user._id,
-                                activityType: "created_account",
-                            });
-                            next(null, user);
-                        },
-
-                        (user, next) => {
-                            mail.runJob("GET_SCHEMA", {
-                                schemaName: "verifyEmail",
-                            }).then((verifyEmailSchema) => {
-                                verifyEmailSchema(
-                                    address,
-                                    body.login,
-                                    user.email.verificationToken
-                                );
-                                next(null, user._id);
-                            });
-                        },
-                    ],
-                    async (err, userId) => {
-                        if (err && err !== true) {
-                            err = await this.utils.runJob("GET_ERROR", {
-                                error: err,
-                            });
-
-                            this.log(
-                                "ERROR",
-                                "AUTH_GITHUB_AUTHORIZE_CALLBACK",
-                                `Failed to authorize with GitHub. "${err}"`
-                            );
-
-                            return redirectOnErr(res, err);
-                        }
-
-                        const sessionId = await this.utils.runJob("GUID", {});
-                        const sessionSchema = await cache.runJob("GET_SCHEMA", {
-                            schemaName: "session",
-                        });
-                        cache
-                            .runJob("HSET", {
-                                table: "sessions",
-                                key: sessionId,
-                                value: sessionSchema(sessionId, userId),
-                            })
-                            .then(() => {
-                                let date = new Date();
-                                date.setTime(
-                                    new Date().getTime() +
-                                        2 * 365 * 24 * 60 * 60 * 1000
-                                );
-
-                                res.cookie(SIDname, sessionId, {
-                                    expires: date,
-                                    secure: config.get("cookie.secure"),
-                                    path: "/",
-                                    domain: config.get("cookie.domain"),
-                                });
-
-                                this.log(
-                                    "INFO",
-                                    "AUTH_GITHUB_AUTHORIZE_CALLBACK",
-                                    `User "${userId}" successfully authorized with GitHub.`
-                                );
-
-                                res.redirect(`${config.get("domain")}/`);
-                            })
-                            .catch((err) => {
-                                return redirectOnErr(res, err.message);
-                            });
-                    }
-                );
-            });
-
-            app.get("/auth/verify_email", async (req, res) => {
-                if (this.getStatus() !== "READY") {
-                    this.log(
-                        "INFO",
-                        "APP_REJECTED_GITHUB_AUTHORIZE",
-                        `A user tried to use github authorize, but the APP module is currently not ready.`
-                    );
-                    return redirectOnErr(
-                        res,
-                        "Something went wrong on our end. Please try again later."
-                    );
-                }
-
-                let code = req.query.code;
-
-                async.waterfall(
-                    [
-                        (next) => {
-                            if (!code) return next("Invalid code.");
-                            next();
-                        },
-
-                        (next) => {
-                            userModel.findOne(
-                                { "email.verificationToken": code },
-                                next
-                            );
-                        },
-
-                        (user, next) => {
-                            if (!user) return next("User not found.");
-                            if (user.email.verified)
-                                return next("This email is already verified.");
-
-                            userModel.updateOne(
-                                { "email.verificationToken": code },
-                                {
-                                    $set: { "email.verified": true },
-                                    $unset: { "email.verificationToken": "" },
-                                },
-                                { runValidators: true },
-                                next
-                            );
-                        },
-                    ],
-                    (err) => {
-                        if (err) {
-                            let error = "An error occurred.";
-
-                            if (typeof err === "string") error = err;
-                            else if (err.message) error = err.message;
-
-                            this.log(
-                                "ERROR",
-                                "VERIFY_EMAIL",
-                                `Verifying email failed. "${error}"`
-                            );
-
-                            return res.json({
-                                status: "failure",
-                                message: error,
-                            });
-                        }
-
-                        this.log(
-                            "INFO",
-                            "VERIFY_EMAIL",
-                            `Successfully verified email.`
-                        );
-
-                        res.redirect(
-                            `${config.get(
-                                "domain"
-                            )}?msg=Thank you for verifying your email`
-                        );
-                    }
-                );
-            });
-
-            resolve();
-        });
-    }
-
-    SERVER(payload) {
-        return new Promise((resolve, reject) => {
-            resolve(this.server);
-        });
-    }
-
-    GET_APP(payload) {
-        return new Promise((resolve, reject) => {
-            resolve({ app: this.app });
-        });
-    }
-
-    EXAMPLE_JOB(payload) {
-        return new Promise((resolve, reject) => {
-            if (true) {
-                resolve({});
-            } else {
-                reject(new Error("Nothing changed."));
-            }
-        });
-    }
+	constructor() {
+		super("app");
+	}
+
+	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;
+
+			const app = (this.app = express());
+			const SIDname = config.get("cookie.SIDname");
+			this.server = app.listen(config.get("serverPort"));
+
+			app.use(cookieParser());
+
+			app.use(bodyParser.json());
+			app.use(bodyParser.urlencoded({ extended: true }));
+
+			let userModel;
+			db.runJob("GET_MODEL", { modelName: "user" })
+				.then(model => {
+					userModel = model;
+				})
+				.catch(console.error);
+
+			const corsOptions = { ...config.get("cors") };
+
+			app.use(cors(corsOptions));
+			app.options("*", cors(corsOptions));
+
+			const oauth2 = new OAuth2(
+				config.get("apis.github.client"),
+				config.get("apis.github.secret"),
+				"https://github.com/",
+				"login/oauth/authorize",
+				"login/oauth/access_token",
+				null
+			);
+
+			const redirectUri = `${config.get("serverDomain")}/auth/github/authorize/callback`;
+
+			/**
+			 * @param {object} res - response object from Express
+			 * @param {string} err - custom error message
+			 */
+			function redirectOnErr(res, err) {
+				res.redirect(`${config.get("domain")}/?err=${encodeURIComponent(err)}`);
+			}
+
+			app.get("/auth/github/authorize", async (req, res) => {
+				if (this.getStatus() !== "READY") {
+					this.log(
+						"INFO",
+						"APP_REJECTED_GITHUB_AUTHORIZE",
+						`A user tried to use github authorize, but the APP module is currently not ready.`
+					);
+					return redirectOnErr(res, "Something went wrong on our end. Please try again later.");
+				}
+
+				const params = [
+					`client_id=${config.get("apis.github.client")}`,
+					`redirect_uri=${config.get("serverDomain")}/auth/github/authorize/callback`,
+					`scope=user:email`
+				].join("&");
+				return res.redirect(`https://github.com/login/oauth/authorize?${params}`);
+			});
+
+			app.get("/auth/github/link", async (req, res) => {
+				if (this.getStatus() !== "READY") {
+					this.log(
+						"INFO",
+						"APP_REJECTED_GITHUB_AUTHORIZE",
+						`A user tried to use github authorize, but the APP module is currently not ready.`
+					);
+					return redirectOnErr(res, "Something went wrong on our end. Please try again later.");
+				}
+
+				const params = [
+					`client_id=${config.get("apis.github.client")}`,
+					`redirect_uri=${config.get("serverDomain")}/auth/github/authorize/callback`,
+					`scope=user:email`,
+					`state=${req.cookies[SIDname]}`
+				].join("&");
+				return res.redirect(`https://github.com/login/oauth/authorize?${params}`);
+			});
+
+			app.get("/auth/github/authorize/callback", async (req, res) => {
+				if (this.getStatus() !== "READY") {
+					this.log(
+						"INFO",
+						"APP_REJECTED_GITHUB_AUTHORIZE",
+						`A user tried to use github authorize, but the APP module is currently not ready.`
+					);
+					return redirectOnErr(res, "Something went wrong on our end. Please try again later.");
+				}
+
+				const { code } = req.query;
+				let accessToken;
+				let body;
+				let address;
+
+				const { state } = req.query;
+
+				const verificationToken = await this.utils.runJob("GENERATE_RANDOM_STRING", { length: 64 });
+
+				return async.waterfall(
+					[
+						next => {
+							if (req.query.error) return next(req.query.error_description);
+							return next();
+						},
+
+						next => {
+							oauth2.getOAuthAccessToken(code, { redirect_uri: redirectUri }, next);
+						},
+
+						(_accessToken, refreshToken, results, next) => {
+							if (results.error) return next(results.error_description);
+
+							accessToken = _accessToken;
+
+							return request.get(
+								{
+									url: `https://api.github.com/user`,
+									headers: {
+										"User-Agent": "request",
+										Authorization: `token ${accessToken}`
+									}
+								},
+								next
+							);
+						},
+
+						(httpResponse, _body, next) => {
+							body = _body = JSON.parse(_body);
+
+							if (httpResponse.statusCode !== 200) return next(body.message);
+
+							if (state) {
+								return async.waterfall(
+									[
+										next => {
+											cache
+												.runJob("HGET", {
+													table: "sessions",
+													key: state
+												})
+												.then(session => next(null, session))
+												.catch(next);
+										},
+
+										(session, next) => {
+											if (!session) return next("Invalid session.");
+											return userModel.findOne({ _id: session.userId }, next);
+										},
+
+										(user, next) => {
+											if (!user) return next("User not found.");
+											if (user.services.github && user.services.github.id)
+												return next("Account already has GitHub linked.");
+
+											return userModel.updateOne(
+												{ _id: user._id },
+												{
+													$set: {
+														"services.github": {
+															id: body.id,
+															accessToken
+														}
+													}
+												},
+												{ runValidators: true },
+												err => {
+													if (err) return next(err);
+													return next(null, user, body);
+												}
+											);
+										},
+
+										user => {
+											cache.runJob("PUB", {
+												channel: "user.linkGithub",
+												value: user._id
+											});
+											res.redirect(`${config.get("domain")}/settings#security`);
+										}
+									],
+									next
+								);
+							}
+
+							if (!body.id) return next("Something went wrong, no id.");
+
+							return userModel.findOne({ "services.github.id": body.id }, (err, user) => {
+								next(err, user, body);
+							});
+						},
+
+						(user, body, next) => {
+							if (user) {
+								user.services.github.access_token = accessToken;
+								return user.save(() => next(true, user._id));
+							}
+
+							return userModel.findOne({ username: new RegExp(`^${body.login}$`, "i") }, (err, user) =>
+								next(err, user)
+							);
+						},
+
+						(user, next) => {
+							if (user) return next(`An account with that username already exists.`);
+
+							return request.get(
+								{
+									url: `https://api.github.com/user/emails`,
+									headers: {
+										"User-Agent": "request",
+										Authorization: `token ${accessToken}`
+									}
+								},
+								next
+							);
+						},
+
+						(httpResponse, body2, next) => {
+							body2 = JSON.parse(body2);
+
+							if (!Array.isArray(body2)) return next(body2.message);
+
+							body2.forEach(email => {
+								if (email.primary) address = email.email.toLowerCase();
+							});
+
+							return userModel.findOne({ "email.address": address }, next);
+						},
+
+						(user, next) => {
+							this.utils
+								.runJob("GENERATE_RANDOM_STRING", {
+									length: 12
+								})
+								.then(_id => {
+									next(null, user, _id);
+								});
+						},
+
+						(user, _id, next) => {
+							if (user) {
+								if (Object.keys(JSON.parse(user.services.github)).length === 0)
+									return next(
+										`An account with that email address exists, but is not linked to GitHub.`
+									);
+								return next(`An account with that email address already exists.`);
+							}
+
+							return next(null, {
+								_id, // TODO Check if exists
+								username: body.login,
+								name: body.name,
+								location: body.location,
+								bio: body.bio,
+								email: {
+									address,
+									verificationToken
+								},
+								services: {
+									github: { id: body.id, accessToken }
+								}
+							});
+						},
+
+						// 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);
+								});
+						},
+
+						// save the new user to the database
+						(user, next) => {
+							userModel.create(user, next);
+						},
+
+						// add the activity of account creation
+						(user, next) => {
+							activities.runJob("ADD_ACTIVITY", {
+								userId: user._id,
+								activityType: "created_account"
+							});
+							next(null, user);
+						},
+
+						(user, next) => {
+							mail.runJob("GET_SCHEMA", {
+								schemaName: "verifyEmail"
+							}).then(verifyEmailSchema => {
+								verifyEmailSchema(address, body.login, user.email.verificationToken);
+								next(null, user._id);
+							});
+						}
+					],
+					async (err, userId) => {
+						if (err && err !== true) {
+							err = await this.utils.runJob("GET_ERROR", {
+								error: err
+							});
+
+							this.log(
+								"ERROR",
+								"AUTH_GITHUB_AUTHORIZE_CALLBACK",
+								`Failed to authorize with GitHub. "${err}"`
+							);
+
+							return redirectOnErr(res, err);
+						}
+
+						const sessionId = await this.utils.runJob("GUID", {});
+						const sessionSchema = await cache.runJob("GET_SCHEMA", {
+							schemaName: "session"
+						});
+
+						return cache
+							.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);
+
+								res.cookie(SIDname, sessionId, {
+									expires: date,
+									secure: config.get("cookie.secure"),
+									path: "/",
+									domain: config.get("cookie.domain")
+								});
+
+								this.log(
+									"INFO",
+									"AUTH_GITHUB_AUTHORIZE_CALLBACK",
+									`User "${userId}" successfully authorized with GitHub.`
+								);
+
+								res.redirect(`${config.get("domain")}/`);
+							})
+							.catch(err => redirectOnErr(res, err.message));
+					}
+				);
+			});
+
+			app.get("/auth/verify_email", async (req, res) => {
+				if (this.getStatus() !== "READY") {
+					this.log(
+						"INFO",
+						"APP_REJECTED_GITHUB_AUTHORIZE",
+						`A user tried to use github authorize, but the APP module is currently not ready.`
+					);
+					return redirectOnErr(res, "Something went wrong on our end. Please try again later.");
+				}
+
+				const { code } = req.query;
+
+				return async.waterfall(
+					[
+						next => {
+							if (!code) return next("Invalid code.");
+							return next();
+						},
+
+						next => {
+							userModel.findOne({ "email.verificationToken": code }, next);
+						},
+
+						(user, next) => {
+							if (!user) return next("User not found.");
+							if (user.email.verified) return next("This email is already verified.");
+
+							return userModel.updateOne(
+								{ "email.verificationToken": code },
+								{
+									$set: { "email.verified": true },
+									$unset: { "email.verificationToken": "" }
+								},
+								{ runValidators: true },
+								next
+							);
+						}
+					],
+					err => {
+						if (err) {
+							let error = "An error occurred.";
+
+							if (typeof err === "string") error = err;
+							else if (err.message) error = err.message;
+
+							this.log("ERROR", "VERIFY_EMAIL", `Verifying email failed. "${error}"`);
+
+							return res.json({
+								status: "failure",
+								message: error
+							});
+						}
+
+						this.log("INFO", "VERIFY_EMAIL", `Successfully verified email.`);
+
+						return res.redirect(`${config.get("domain")}?msg=Thank you for verifying your email`);
+					}
+				);
+			});
+
+			return resolve();
+		});
+	}
+
+	SERVER() {
+		return new Promise(resolve => {
+			resolve(this.server);
+		});
+	}
+
+	GET_APP() {
+		return new Promise(resolve => {
+			resolve({ app: this.app });
+		});
+	}
+
+	// EXAMPLE_JOB() {
+	// 	return new Promise((resolve, reject) => {
+	// 		if (true) resolve({});
+	// 		else reject(new Error("Nothing changed."));
+	// 	});
+	// }
 }
 
-module.exports = new AppModule();
+export default new AppModule();

+ 255 - 257
backend/logic/cache/index.js

@@ -1,267 +1,265 @@
-const CoreClass = require("../../core.js");
+import config from "config";
+import redis from "redis";
+import mongoose from "mongoose";
 
-const redis = require("redis");
-const config = require("config");
-const mongoose = require("mongoose");
+import CoreClass from "../../core";
 
 // Lightweight / convenience wrapper around redis module for our needs
 
-const pubs = {},
-    subs = {};
+const pubs = {};
+const subs = {};
 
 class CacheModule extends CoreClass {
-    constructor() {
-        super("cache");
-    }
-
-    initialize() {
-        return new Promise((resolve, reject) => {
-            this.schemas = {
-                session: require("./schemas/session"),
-                station: require("./schemas/station"),
-                playlist: require("./schemas/playlist"),
-                officialPlaylist: require("./schemas/officialPlaylist"),
-                song: require("./schemas/song"),
-                punishment: require("./schemas/punishment"),
-            };
-
-            this.url = config.get("redis").url;
-            this.password = config.get("redis").password;
-
-            this.log("INFO", "Connecting...");
-
-            this.client = redis.createClient({
-                url: this.url,
-                password: this.password,
-                retry_strategy: (options) => {
-                    if (this.getStatus() === "LOCKDOWN") return;
-                    if (this.getStatus() !== "RECONNECTING")
-                        this.setStatus("RECONNECTING");
-
-                    this.log("INFO", `Attempting to reconnect.`);
-
-                    if (options.attempt >= 10) {
-                        this.log("ERROR", `Stopped trying to reconnect.`);
-
-                        this.setStatus("FAILED");
-
-                        // this.failed = true;
-                        // this._lockdown();
-
-                        return undefined;
-                    }
-
-                    return 3000;
-                },
-            });
-
-            this.client.on("error", (err) => {
-                if (this.getStatus() === "INITIALIZING") reject(err);
-                if (this.getStatus() === "LOCKDOWN") return;
-
-                this.log("ERROR", `Error ${err.message}.`);
-            });
-
-            this.client.on("connect", () => {
-                this.log("INFO", "Connected succesfully.");
-
-                if (this.getStatus() === "INITIALIZING") resolve();
-                else if (
-                    this.getStatus() === "FAILED" ||
-                    this.getStatus() === "RECONNECTING"
-                )
-                    this.setStatus("READY");
-            });
-        });
-    }
-
-    /**
-     * Gracefully closes all the Redis client connections
-     */
-    QUIT(payload) {
-        return new Promise((resolve, reject) => {
-            if (this.client.connected) {
-                this.client.quit();
-                Object.keys(pubs).forEach((channel) => pubs[channel].quit());
-                Object.keys(subs).forEach((channel) =>
-                    subs[channel].client.quit()
-                );
-            }
-            resolve();
-        });
-    }
-
-    /**
-     * Sets a single value in a table
-     *
-     * @param {String} table - name of the table we want to set a key of (table === redis hash)
-     * @param {String} key -  name of the key to set
-     * @param {*} value - the value we want to set
-     * @param {Function} cb - gets called when the value has been set in Redis
-     * @param {Boolean} [stringifyJson=true] - stringify 'value' if it's an Object or Array
-     */
-    HSET(payload) {
-        //table, key, value, cb, stringifyJson = true
-        return new Promise((resolve, reject) => {
-            let key = payload.key;
-            let value = payload.value;
-
-            if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
-            // 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) => {
-                if (err) return reject(new Error(err));
-                else resolve(JSON.parse(value));
-            });
-        });
-    }
-
-    /**
-     * Gets a single value from a table
-     *
-     * @param {String} table - name of the table to get the value from (table === redis hash)
-     * @param {String} key - name of the key to fetch
-     * @param {Function} cb - gets called when the value is returned from Redis
-     * @param {Boolean} [parseJson=true] - attempt to parse returned data as JSON
-     */
-    HGET(payload) {
-        //table, key, cb, parseJson = true
-        return new Promise((resolve, reject) => {
-            // if (!key || !table)
-            // return typeof cb === "function" ? cb(null, null) : null;
-            let key = payload.key;
-
-            if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
-
-            this.client.hget(payload.table, key, (err, value) => {
-                if (err) return reject(new Error(err));
-                try {
-                    value = JSON.parse(value);
-                } catch (e) {}
-                resolve(value);
-            });
-        });
-    }
-
-    /**
-     * Deletes a single value from a table
-     *
-     * @param {String} table - name of the table to delete the value from (table === redis hash)
-     * @param {String} key - name of the key to delete
-     * @param {Function} cb - gets called when the value has been deleted from Redis or when it returned an error
-     */
-    HDEL(payload) {
-        //table, key, cb
-        return new Promise((resolve, reject) => {
-            // if (!payload.key || !table || typeof key !== "string")
-            // return cb(null, null);
-
-            let key = payload.key;
-
-            if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
-
-            this.client.hdel(payload.table, key, (err) => {
-                if (err) return reject(new Error(err));
-                else return resolve();
-            });
-        });
-    }
-
-    /**
-     * Returns all the keys for a table
-     *
-     * @param {String} table - name of the table to get the values from (table === redis hash)
-     * @param {Function} cb - gets called when the values are returned from Redis
-     * @param {Boolean} [parseJson=true] - attempts to parse all values as JSON by default
-     */
-    HGETALL(payload) {
-        //table, cb, parseJson = true
-        return new Promise((resolve, reject) => {
-            this.client.hgetall(payload.table, (err, obj) => {
-                if (err) return reject(new Error(err));
-                if (obj)
-                    Object.keys(obj).forEach((key) => {
-                        try {
-                            obj[key] = JSON.parse(obj[key]);
-                        } catch (e) {}
-                    });
-                else if (!obj) obj = [];
-                resolve(obj);
-            });
-        });
-    }
-
-    /**
-     * Publish a message to a channel, caches the redis client connection
-     *
-     * @param {String} channel - the name of the channel we want to publish a message to
-     * @param {*} value - the value we want to send
-     * @param {Boolean} [stringifyJson=true] - stringify 'value' if it's an Object or Array
-     */
-    PUB(payload) {
-        //channel, value, stringifyJson = true
-        return new Promise((resolve, reject) => {
-            /*if (pubs[channel] === undefined) {
+	constructor() {
+		super("cache");
+	}
+
+	async initialize() {
+		const importSchema = schemaName =>
+			new Promise(resolve => {
+				import(`./schemas/${schemaName}`).then(schema => resolve(schema.default));
+			});
+
+		this.schemas = {
+			session: await importSchema("session"),
+			station: await importSchema("station"),
+			playlist: await importSchema("playlist"),
+			officialPlaylist: await importSchema("officialPlaylist"),
+			song: await importSchema("song"),
+			punishment: await importSchema("punishment")
+		};
+
+		return new Promise((resolve, reject) => {
+			this.url = config.get("redis").url;
+			this.password = config.get("redis").password;
+
+			this.log("INFO", "Connecting...");
+
+			this.client = redis.createClient({
+				url: this.url,
+				password: this.password,
+				retry_strategy: options => {
+					if (this.getStatus() === "LOCKDOWN") return;
+					if (this.getStatus() !== "RECONNECTING") this.setStatus("RECONNECTING");
+
+					this.log("INFO", `Attempting to reconnect.`);
+
+					if (options.attempt >= 10) {
+						this.log("ERROR", `Stopped trying to reconnect.`);
+
+						this.setStatus("FAILED");
+
+						// this.failed = true;
+						// this._lockdown();
+					}
+				}
+			});
+
+			this.client.on("error", err => {
+				if (this.getStatus() === "INITIALIZING") reject(err);
+				if (this.getStatus() === "LOCKDOWN") return;
+
+				this.log("ERROR", `Error ${err.message}.`);
+			});
+
+			this.client.on("connect", () => {
+				this.log("INFO", "Connected succesfully.");
+
+				if (this.getStatus() === "INITIALIZING") resolve();
+				else if (this.getStatus() === "FAILED" || this.getStatus() === "RECONNECTING") this.setStatus("READY");
+			});
+		});
+	}
+
+	QUIT() {
+		return new Promise(resolve => {
+			if (this.client.connected) {
+				this.client.quit();
+				Object.keys(pubs).forEach(channel => pubs[channel].quit());
+				Object.keys(subs).forEach(channel => subs[channel].client.quit());
+			}
+			resolve();
+		});
+	}
+
+	/**
+	 * Sets a single value in a table
+	 *
+	 * @param {object} payload - object containing payload
+	 * @param {string} payload.table - name of the table we want to set a key of (table === redis hash)
+	 * @param {string} payload.key -  name of the key to set
+	 * @param {*} payload.value - the value we want to set
+	 * @param {boolean} [payload.stringifyJson=true] - stringify 'value' if it's an Object or Array
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	HSET(payload) {
+		// table, key, value, cb, stringifyJson = true
+		return new Promise((resolve, reject) => {
+			let { key } = payload;
+			let { value } = payload;
+
+			if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
+			// 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 => {
+				if (err) return reject(new Error(err));
+				return resolve(JSON.parse(value));
+			});
+		});
+	}
+
+	/**
+	 * Gets a single value from a table
+	 *
+	 * @param {object} payload - object containing payload
+	 * @param {string} payload.table - name of the table to get the value from (table === redis hash)
+	 * @param {string} payload.key - name of the key to fetch
+	 * @param {boolean} [payload.parseJson=true] - attempt to parse returned data as JSON
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	HGET(payload) {
+		// table, key, parseJson = true
+		return new Promise((resolve, reject) => {
+			let { key } = payload;
+
+			if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
+
+			this.client.hget(payload.table, key, (err, value) => {
+				if (err) return reject(new Error(err));
+				try {
+					value = JSON.parse(value);
+				} catch (e) {
+					return reject(err);
+				}
+
+				return resolve(value);
+			});
+		});
+	}
+
+	/**
+	 * Deletes a single value from a table
+	 *
+	 * @param {object} payload - object containing payload
+	 * @param {string} payload.table - name of the table to delete the value from (table === redis hash)
+	 * @param {string} payload.key - name of the key to delete
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	HDEL(payload) {
+		// table, key, cb
+		return new Promise((resolve, reject) => {
+			// if (!payload.key || !table || typeof key !== "string")
+			// return cb(null, null);
+
+			let { key } = payload;
+
+			if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
+
+			this.client.hdel(payload.table, key, err => {
+				if (err) return reject(new Error(err));
+				return resolve();
+			});
+		});
+	}
+
+	/**
+	 * Returns all the keys for a table
+	 *
+	 * @param {object} payload - object containing payload
+	 * @param {string} payload.table - name of the table to get the values from (table === redis hash)
+	 * @param {boolean} [payload.parseJson=true] - attempts to parse all values as JSON by default
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	HGETALL(payload) {
+		// table, cb, parseJson = true
+		return new Promise((resolve, reject) => {
+			this.client.hgetall(payload.table, (err, obj) => {
+				if (err) return reject(new Error(err));
+				if (obj)
+					Object.keys(obj).forEach(key => {
+						obj[key] = JSON.parse(obj[key]);
+					});
+				else if (!obj) obj = [];
+
+				return resolve(obj);
+			});
+		});
+	}
+
+	/**
+	 * Publish a message to a channel, caches the redis client connection
+	 *
+	 * @param {object} payload - object containing payload
+	 * @param {string} payload.channel - the name of the channel we want to publish a message to
+	 * @param {*} payload.value - the value we want to send
+	 * @param {boolean} [payload.stringifyJson=true] - stringify 'value' if it's an Object or Array
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	PUB(payload) {
+		// channel, value, stringifyJson = true
+		return new Promise((resolve, reject) => {
+			/* if (pubs[channel] === undefined) {
             pubs[channel] = redis.createClient({ url: this.url });
             pubs[channel].on('error', (err) => console.error);
-            }*/
-
-            let value = payload.value;
-
-            if (["object", "array"].includes(typeof value))
-                value = JSON.stringify(value);
-
-            //pubs[channel].publish(channel, value);
-            this.client.publish(payload.channel, value, (err, res) => {
-                if (err) reject(err);
-                else resolve();
-            });
-        });
-    }
-
-    /**
-     * Subscribe to a channel, caches the redis client connection
-     *
-     * @param {String} channel - name of the channel to subscribe to
-     * @param {Function} cb - gets called when a message is received
-     * @param {Boolean} [parseJson=true] - parse the message as JSON
-     */
-    SUB(payload) {
-        //channel, cb, parseJson = true
-        return new Promise((resolve, reject) => {
-            if (subs[payload.channel] === undefined) {
-                subs[payload.channel] = {
-                    client: redis.createClient({
-                        url: this.url,
-                        password: this.password,
-                    }),
-                    cbs: [],
-                };
-                subs[payload.channel].client.on(
-                    "message",
-                    (channel, message) => {
-                        try {
-                            message = JSON.parse(message);
-                        } catch (e) {}
-                        subs[channel].cbs.forEach((cb) => cb(message));
-                    }
-                );
-                subs[payload.channel].client.subscribe(payload.channel);
-            }
-
-            subs[payload.channel].cbs.push(payload.cb);
-
-            resolve();
-        });
-    }
-
-    GET_SCHEMA(payload) {
-        return new Promise((resolve, reject) => {
-            resolve(this.schemas[payload.schemaName]);
-        });
-    }
+            } */
+
+			let { value } = payload;
+
+			if (["object", "array"].includes(typeof value)) value = JSON.stringify(value);
+
+			// pubs[channel].publish(channel, value);
+			this.client.publish(payload.channel, value, err => {
+				if (err) reject(err);
+				else resolve();
+			});
+		});
+	}
+
+	/**
+	 * Subscribe to a channel, caches the redis client connection
+	 *
+	 * @param {object} payload - object containing payload
+	 * @param {string} payload.channel - name of the channel to subscribe to
+	 * @param {boolean} [payload.parseJson=true] - parse the message as JSON
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	SUB(payload) {
+		// channel, cb, parseJson = true
+		return new Promise(resolve => {
+			if (subs[payload.channel] === undefined) {
+				subs[payload.channel] = {
+					client: redis.createClient({
+						url: this.url,
+						password: this.password
+					}),
+					cbs: []
+				};
+
+				subs[payload.channel].client.on("message", (channel, message) => {
+					try {
+						message = JSON.parse(message);
+					} catch (err) {
+						console.error(err);
+					}
+
+					return subs[channel].cbs.forEach(cb => cb(message));
+				});
+
+				subs[payload.channel].client.subscribe(payload.channel);
+			}
+
+			subs[payload.channel].cbs.push(payload.cb);
+
+			resolve();
+		});
+	}
+
+	GET_SCHEMA(payload) {
+		return new Promise(resolve => {
+			resolve(this.schemas[payload.schemaName]);
+		});
+	}
 }
 
-module.exports = new CacheModule();
+export default new CacheModule();

+ 4 - 8
backend/logic/cache/schemas/officialPlaylist.js

@@ -1,8 +1,4 @@
-'use strict';
-
-module.exports = (stationId, songs) => {
-	return {
-		stationId,
-		songs
-	}
-};
+export default (stationId, songs) => ({
+	stationId,
+	songs
+});

+ 3 - 7
backend/logic/cache/schemas/playlist.js

@@ -1,13 +1,9 @@
-'use strict';
-
 /**
  * Schema for a playlist stored / cached in redis,
  * gets created when a playlist is in use
  * and therefore is put into the redis cache
  *
- * @param playlist
- * @returns {Object}
+ * @param {object} playlist - object containing the playlist
+ * @returns {object} - returns same object
  */
-module.exports = (playlist) => {
-	return playlist;
-};
+export default playlist => playlist;

+ 7 - 5
backend/logic/cache/schemas/punishment.js

@@ -1,5 +1,7 @@
-'use strict';
-
-module.exports = (punishment, punishmentId) => {
-	return { type: punishment.type, value: punishment.value, reason: punishment.reason, expiresAt: new Date(punishment.expiresAt).getTime(), punishmentId };
-};
+export default (punishment, punishmentId) => ({
+	type: punishment.type,
+	value: punishment.value,
+	reason: punishment.reason,
+	expiresAt: new Date(punishment.expiresAt).getTime(),
+	punishmentId
+});

+ 6 - 10
backend/logic/cache/schemas/session.js

@@ -1,10 +1,6 @@
-'use strict';
-
-module.exports = (sessionId, userId) => {
-	return {
-		sessionId: sessionId,
-		userId: userId,
-		refreshDate: Date.now(),
-		created: Date.now()
-	};
-};
+export default (sessionId, userId) => ({
+	sessionId,
+	userId,
+	refreshDate: Date.now(),
+	created: Date.now()
+});

+ 1 - 5
backend/logic/cache/schemas/song.js

@@ -1,5 +1 @@
-'use strict';
-
-module.exports = (song) => {
-	return song;
-};
+export default song => song;

+ 3 - 7
backend/logic/cache/schemas/station.js

@@ -1,13 +1,9 @@
-'use strict';
-
 /**
  * Schema for a station stored / cached in redis,
  * gets created when a station is in use
  * and therefore is put into the redis cache
  *
- * @param station
- * @returns {Object}
+ * @param {object} station -  object containing the station
+ * @returns {object} - returns same object
  */
-module.exports = (station) => {
-	return station;
-};
+export default station => station;

+ 227 - 324
backend/logic/db/index.js

@@ -1,198 +1,148 @@
-const CoreClass = require("../../core.js");
+import config from "config";
+import mongoose from "mongoose";
+import bluebird from "bluebird";
 
-const mongoose = require("mongoose");
-const config = require("config");
+import CoreClass from "../../core";
 
 const regex = {
-    azAZ09_: /^[A-Za-z0-9_]+$/,
-    az09_: /^[a-z0-9_]+$/,
-    emailSimple: /^[\x00-\x7F]+@[a-z0-9]+\.[a-z0-9]+(\.[a-z0-9]+)?$/,
-    ascii: /^[\x00-\x7F]+$/,
-    custom: (regex) => new RegExp(`^[${regex}]+$`),
+	azAZ09_: /^[A-Za-z0-9_]+$/,
+	az09_: /^[a-z0-9_]+$/,
+	emailSimple: /^[\x00-\x7F]+@[a-z0-9]+\.[a-z0-9]+(\.[a-z0-9]+)?$/,
+	ascii: /^[\x00-\x7F]+$/,
+	custom: regex => new RegExp(`^[${regex}]+$`)
 };
 
-const isLength = (string, min, max) => {
-    return !(
-        typeof string !== "string" ||
-        string.length < min ||
-        string.length > max
-    );
-};
-
-const bluebird = require("bluebird");
+const isLength = (string, min, max) => !(typeof string !== "string" || string.length < min || string.length > max);
 
 mongoose.Promise = bluebird;
 
 class DBModule extends CoreClass {
-    constructor() {
-        super("db");
-    }
-
-    initialize() {
-        return new Promise((resolve, reject) => {
-            this.schemas = {};
-            this.models = {};
-
-            const mongoUrl = config.get("mongo").url;
-
-            mongoose
-                .connect(mongoUrl, {
-                    useNewUrlParser: true,
-                    useUnifiedTopology: true,
-                    useCreateIndex: true
-                })
-                .then(() => {
-                    this.schemas = {
-                        song: new mongoose.Schema(require(`./schemas/song`)),
-                        queueSong: new mongoose.Schema(
-                            require(`./schemas/queueSong`)
-                        ),
-                        station: new mongoose.Schema(
-                            require(`./schemas/station`)
-                        ),
-                        user: new mongoose.Schema(require(`./schemas/user`)),
-                        activity: new mongoose.Schema(
-                            require(`./schemas/activity`)
-                        ),
-                        playlist: new mongoose.Schema(
-                            require(`./schemas/playlist`)
-                        ),
-                        news: new mongoose.Schema(require(`./schemas/news`)),
-                        report: new mongoose.Schema(
-                            require(`./schemas/report`)
-                        ),
-                        punishment: new mongoose.Schema(
-                            require(`./schemas/punishment`)
-                        ),
-                    };
-
-                    this.models = {
-                        song: mongoose.model("song", this.schemas.song),
-                        queueSong: mongoose.model(
-                            "queueSong",
-                            this.schemas.queueSong
-                        ),
-                        station: mongoose.model(
-                            "station",
-                            this.schemas.station
-                        ),
-                        user: mongoose.model("user", this.schemas.user),
-                        activity: mongoose.model(
-                            "activity",
-                            this.schemas.activity
-                        ),
-                        playlist: mongoose.model(
-                            "playlist",
-                            this.schemas.playlist
-                        ),
-                        news: mongoose.model("news", this.schemas.news),
-                        report: mongoose.model("report", this.schemas.report),
-                        punishment: mongoose.model(
-                            "punishment",
-                            this.schemas.punishment
-                        ),
-                    };
-
-                    mongoose.connection.on("error", (err) => {
-                        this.log("ERROR", err);
-                    });
-
-                    mongoose.connection.on("disconnected", () => {
-                        this.log(
-                            "ERROR",
-                            "Disconnected, going to try to reconnect..."
-                        );
-                        this.setStatus("RECONNECTING");
-                    });
-
-                    mongoose.connection.on("reconnected", () => {
-                        this.log("INFO", "Reconnected.");
-                        this.setStatus("READY");
-                    });
-
-                    mongoose.connection.on("reconnectFailed", () => {
-                        this.log(
-                            "INFO",
-                            "Reconnect failed, stopping reconnecting."
-                        );
-                        // this.failed = true;
-                        // this._lockdown();
-                        this.setStatus("FAILED");
-                    });
-
-                    // User
-                    this.schemas.user.path("username").validate((username) => {
-                        return (
-                            isLength(username, 2, 32) &&
-                            regex.custom("a-zA-Z0-9_-").test(username)
-                        );
-                    }, "Invalid username.");
-
-                    this.schemas.user
-                        .path("email.address")
-                        .validate((email) => {
-                            if (!isLength(email, 3, 254)) return false;
-                            if (email.indexOf("@") !== email.lastIndexOf("@"))
-                                return false;
-                            return (
-                                regex.emailSimple.test(email) &&
-                                regex.ascii.test(email)
-                            );
-                        }, "Invalid email.");
-
-                    // Station
-                    this.schemas.station.path("name").validate((id) => {
-                        return isLength(id, 2, 16) && regex.az09_.test(id);
-                    }, "Invalid station name.");
-
-                    this.schemas.station
-                        .path("displayName")
-                        .validate((displayName) => {
-                            return (
-                                isLength(displayName, 2, 32) &&
-                                regex.ascii.test(displayName)
-                            );
-                        }, "Invalid display name.");
-
-                    this.schemas.station
-                        .path("description")
-                        .validate((description) => {
-                            if (!isLength(description, 2, 200)) return false;
-                            let characters = description.split("");
-                            return (
-                                characters.filter((character) => {
-                                    return character.charCodeAt(0) === 21328;
-                                }).length === 0
-                            );
-                        }, "Invalid display name.");
-
-                    this.schemas.station.path("owner").validate({
-                        validator: (owner) => {
-                            return new Promise((resolve, reject) => {
-                                this.models.station.countDocuments(
-                                    { owner: owner },
-                                    (err, c) => {
-                                        if (err)
-                                            reject(
-                                                new Error(
-                                                    "A mongo error happened."
-                                                )
-                                            );
-                                        else if (c >= 3)
-                                            reject(
-                                                new Error(
-                                                    "User already has 3 stations."
-                                                )
-                                            );
-                                        else resolve();
-                                    }
-                                );
-                            });
-                        },
-                        message: "User already has 3 stations.",
-                    });
-
-                    /*
+	constructor() {
+		super("db");
+	}
+
+	initialize() {
+		return new Promise((resolve, reject) => {
+			this.schemas = {};
+			this.models = {};
+
+			const mongoUrl = config.get("mongo").url;
+
+			mongoose
+				.connect(mongoUrl, {
+					useNewUrlParser: true,
+					useUnifiedTopology: true,
+					useCreateIndex: true
+				})
+				.then(async () => {
+					this.schemas = {
+						song: {},
+						queueSong: {},
+						station: {},
+						user: {},
+						activity: {},
+						playlist: {},
+						news: {},
+						report: {},
+						punishment: {}
+					};
+
+					const importSchema = schemaName =>
+						new Promise(resolve => {
+							import(`./schemas/${schemaName}`).then(schema => {
+								this.schemas[schemaName] = new mongoose.Schema(schema.default);
+								return resolve();
+							});
+						});
+
+					await importSchema("song");
+					await importSchema("queueSong");
+					await importSchema("station");
+					await importSchema("user");
+					await importSchema("activity");
+					await importSchema("playlist");
+					await importSchema("news");
+					await importSchema("report");
+					await importSchema("punishment");
+
+					this.models = {
+						song: mongoose.model("song", this.schemas.song),
+						queueSong: mongoose.model("queueSong", this.schemas.queueSong),
+						station: mongoose.model("station", this.schemas.station),
+						user: mongoose.model("user", this.schemas.user),
+						activity: mongoose.model("activity", this.schemas.activity),
+						playlist: mongoose.model("playlist", this.schemas.playlist),
+						news: mongoose.model("news", this.schemas.news),
+						report: mongoose.model("report", this.schemas.report),
+						punishment: mongoose.model("punishment", this.schemas.punishment)
+					};
+
+					mongoose.connection.on("error", err => {
+						this.log("ERROR", err);
+					});
+
+					mongoose.connection.on("disconnected", () => {
+						this.log("ERROR", "Disconnected, going to try to reconnect...");
+						this.setStatus("RECONNECTING");
+					});
+
+					mongoose.connection.on("reconnected", () => {
+						this.log("INFO", "Reconnected.");
+						this.setStatus("READY");
+					});
+
+					mongoose.connection.on("reconnectFailed", () => {
+						this.log("INFO", "Reconnect failed, stopping reconnecting.");
+						// this.failed = true;
+						// this._lockdown();
+						this.setStatus("FAILED");
+					});
+
+					// User
+					this.schemas.user
+						.path("username")
+						.validate(
+							username => isLength(username, 2, 32) && regex.custom("a-zA-Z0-9_-").test(username),
+							"Invalid username."
+						);
+
+					this.schemas.user.path("email.address").validate(email => {
+						if (!isLength(email, 3, 254)) return false;
+						if (email.indexOf("@") !== email.lastIndexOf("@")) return false;
+						return regex.emailSimple.test(email) && regex.ascii.test(email);
+					}, "Invalid email.");
+
+					// Station
+					this.schemas.station
+						.path("name")
+						.validate(id => isLength(id, 2, 16) && regex.az09_.test(id), "Invalid station name.");
+
+					this.schemas.station
+						.path("displayName")
+						.validate(
+							displayName => isLength(displayName, 2, 32) && regex.ascii.test(displayName),
+							"Invalid display name."
+						);
+
+					this.schemas.station.path("description").validate(description => {
+						if (!isLength(description, 2, 200)) return false;
+						const characters = description.split("");
+						return characters.filter(character => character.charCodeAt(0) === 21328).length === 0;
+					}, "Invalid display name.");
+
+					this.schemas.station.path("owner").validate({
+						validator: owner =>
+							new Promise((resolve, reject) => {
+								this.models.station.countDocuments({ owner }, (err, c) => {
+									if (err) reject(new Error("A mongo error happened."));
+									else if (c >= 3) reject(new Error("User already has 3 stations."));
+									else resolve();
+								});
+							}),
+						message: "User already has 3 stations."
+					});
+
+					/*
 					this.schemas.station.path('queue').validate((queue, callback) => { //Callback no longer works, see station max count
 						let totalDuration = 0;
 						queue.forEach((song) => {
@@ -229,143 +179,96 @@ class DBModule extends CoreClass {
 					}, 'The max amount of songs per user is 3, and only 2 in a row is allowed.');
 					*/
 
-                    // Song
-                    let songTitle = (title) => {
-                        return isLength(title, 1, 100);
-                    };
-                    this.schemas.song
-                        .path("title")
-                        .validate(songTitle, "Invalid title.");
-                    this.schemas.queueSong
-                        .path("title")
-                        .validate(songTitle, "Invalid title.");
-
-                    this.schemas.song.path("artists").validate((artists) => {
-                        return !(artists.length < 1 || artists.length > 10);
-                    }, "Invalid artists.");
-                    this.schemas.queueSong
-                        .path("artists")
-                        .validate((artists) => {
-                            return !(artists.length < 0 || artists.length > 10);
-                        }, "Invalid artists.");
-
-                    let songArtists = (artists) => {
-                        return (
-                            artists.filter((artist) => {
-                                return (
-                                    isLength(artist, 1, 64) && artist !== "NONE"
-                                );
-                            }).length === artists.length
-                        );
-                    };
-                    this.schemas.song
-                        .path("artists")
-                        .validate(songArtists, "Invalid artists.");
-                    this.schemas.queueSong
-                        .path("artists")
-                        .validate(songArtists, "Invalid artists.");
-
-                    let songGenres = (genres) => {
-                        if (genres.length < 1 || genres.length > 16)
-                            return false;
-                        return (
-                            genres.filter((genre) => {
-                                return (
-                                    isLength(genre, 1, 32) &&
-                                    regex.ascii.test(genre)
-                                );
-                            }).length === genres.length
-                        );
-                    };
-                    this.schemas.song
-                        .path("genres")
-                        .validate(songGenres, "Invalid genres.");
-                    this.schemas.queueSong
-                        .path("genres")
-                        .validate(songGenres, "Invalid genres.");
-
-                    let songThumbnail = (thumbnail) => {
-                        if (!isLength(thumbnail, 1, 256)) return false;
-                        if (config.get("cookie.secure") === true)
-                            return thumbnail.startsWith("https://");
-                        else
-                            return (
-                                thumbnail.startsWith("http://") ||
-                                thumbnail.startsWith("https://")
-                            );
-                    };
-                    this.schemas.song
-                        .path("thumbnail")
-                        .validate(songThumbnail, "Invalid thumbnail.");
-                    this.schemas.queueSong
-                        .path("thumbnail")
-                        .validate(songThumbnail, "Invalid thumbnail.");
-
-                    // Playlist
-                    this.schemas.playlist
-                        .path("displayName")
-                        .validate((displayName) => {
-                            return (
-                                isLength(displayName, 1, 32) &&
-                                regex.ascii.test(displayName)
-                            );
-                        }, "Invalid display name.");
-
-                    this.schemas.playlist
-                        .path("createdBy")
-                        .validate((createdBy) => {
-                            this.models.playlist.countDocuments(
-                                { createdBy: createdBy },
-                                (err, c) => {
-                                    return !(err || c >= 10);
-                                }
-                            );
-                        }, "Max 10 playlists per user.");
-
-                    this.schemas.playlist.path("songs").validate((songs) => {
-                        return songs.length <= 5000;
-                    }, "Max 5000 songs per playlist.");
-
-                    this.schemas.playlist.path("songs").validate((songs) => {
-                        if (songs.length === 0) return true;
-                        return songs[0].duration <= 10800;
-                    }, "Max 3 hours per song.");
-
-                    // Report
-                    this.schemas.report
-                        .path("description")
-                        .validate((description) => {
-                            return (
-                                !description ||
-                                (isLength(description, 0, 400) &&
-                                    regex.ascii.test(description))
-                            );
-                        }, "Invalid description.");
-
-                    resolve();
-                })
-                .catch((err) => {
-                    this.log("ERROR", err);
-                    reject(err);
-                });
-        });
-    }
-
-    GET_MODEL(payload) {
-        return new Promise((resolve, reject) => {
-            resolve(this.models[payload.modelName]);
-        });
-    }
-
-    GET_SCHEMA(payload) {
-        return new Promise((resolve, reject) => {
-            resolve(this.schemas[payload.schemaName]);
-        });
-    }
-
-    passwordValid(password) {
-        return isLength(password, 6, 200);
-    }
+					// Song
+					const songTitle = title => isLength(title, 1, 100);
+					this.schemas.song.path("title").validate(songTitle, "Invalid title.");
+					this.schemas.queueSong.path("title").validate(songTitle, "Invalid title.");
+
+					this.schemas.song
+						.path("artists")
+						.validate(artists => !(artists.length < 1 || artists.length > 10), "Invalid artists.");
+					this.schemas.queueSong
+						.path("artists")
+						.validate(artists => !(artists.length < 0 || artists.length > 10), "Invalid artists.");
+
+					const songArtists = artists =>
+						artists.filter(artist => isLength(artist, 1, 64) && artist !== "NONE").length ===
+						artists.length;
+					this.schemas.song.path("artists").validate(songArtists, "Invalid artists.");
+					this.schemas.queueSong.path("artists").validate(songArtists, "Invalid artists.");
+
+					const songGenres = genres => {
+						if (genres.length < 1 || genres.length > 16) return false;
+						return (
+							genres.filter(genre => isLength(genre, 1, 32) && regex.ascii.test(genre)).length ===
+							genres.length
+						);
+					};
+					this.schemas.song.path("genres").validate(songGenres, "Invalid genres.");
+					this.schemas.queueSong.path("genres").validate(songGenres, "Invalid genres.");
+
+					const songThumbnail = thumbnail => {
+						if (!isLength(thumbnail, 1, 256)) return false;
+						if (config.get("cookie.secure") === true) return thumbnail.startsWith("https://");
+						return thumbnail.startsWith("http://") || thumbnail.startsWith("https://");
+					};
+					this.schemas.song.path("thumbnail").validate(songThumbnail, "Invalid thumbnail.");
+					this.schemas.queueSong.path("thumbnail").validate(songThumbnail, "Invalid thumbnail.");
+
+					// Playlist
+					this.schemas.playlist
+						.path("displayName")
+						.validate(
+							displayName => isLength(displayName, 1, 32) && regex.ascii.test(displayName),
+							"Invalid display name."
+						);
+
+					this.schemas.playlist.path("createdBy").validate(createdBy => {
+						this.models.playlist.countDocuments({ createdBy }, (err, c) => !(err || c >= 10));
+					}, "Max 10 playlists per user.");
+
+					this.schemas.playlist
+						.path("songs")
+						.validate(songs => songs.length <= 5000, "Max 5000 songs per playlist.");
+
+					this.schemas.playlist.path("songs").validate(songs => {
+						if (songs.length === 0) return true;
+						return songs[0].duration <= 10800;
+					}, "Max 3 hours per song.");
+
+					// Report
+					this.schemas.report
+						.path("description")
+						.validate(
+							description =>
+								!description || (isLength(description, 0, 400) && regex.ascii.test(description)),
+							"Invalid description."
+						);
+
+					resolve();
+				})
+				.catch(err => {
+					this.log("ERROR", err);
+					reject(err);
+				});
+		});
+	}
+
+	GET_MODEL(payload) {
+		return new Promise(resolve => {
+			resolve(this.models[payload.modelName]);
+		});
+	}
+
+	GET_SCHEMA(payload) {
+		return new Promise(resolve => {
+			resolve(this.schemas[payload.schemaName]);
+		});
+	}
+
+	passwordValid(password) {
+		return isLength(password, 6, 200);
+	}
 }
 
-module.exports = new DBModule();
+export default new DBModule();

+ 16 - 12
backend/logic/db/schemas/activity.js

@@ -1,16 +1,20 @@
-module.exports = {
+export default {
 	createdAt: { type: Date, default: Date.now, required: true },
 	hidden: { type: Boolean, default: false, required: true },
 	userId: { type: String, required: true },
-	activityType: { type: String, enum: [
-		"created_account",
-		"created_station",
-		"deleted_station",
-		"created_playlist",
-		"deleted_playlist",
-		"liked_song",
-		"added_song_to_playlist",
-		"added_songs_to_playlist"
-	], required: true },
+	activityType: {
+		type: String,
+		enum: [
+			"created_account",
+			"created_station",
+			"deleted_station",
+			"created_playlist",
+			"deleted_playlist",
+			"liked_song",
+			"added_song_to_playlist",
+			"added_songs_to_playlist"
+		],
+		required: true
+	},
 	payload: { type: Array, required: true }
-}
+};

+ 1 - 1
backend/logic/db/schemas/news.js

@@ -1,4 +1,4 @@
-module.exports = {
+export default {
 	title: { type: String, required: true },
 	description: { type: String, required: true },
 	bugs: [{ type: String }],

+ 1 - 1
backend/logic/db/schemas/playlist.js

@@ -1,4 +1,4 @@
-module.exports = {
+export default {
 	displayName: { type: String, min: 2, max: 32, required: true },
 	songs: { type: Array },
 	createdBy: { type: String, required: true },

+ 2 - 2
backend/logic/db/schemas/punishment.js

@@ -1,7 +1,7 @@
-module.exports = {
+export default {
 	type: { type: String, enum: ["banUserId", "banUserIp"], required: true },
 	value: { type: String, required: true },
-	reason: { type: String, required: true, default: 'Unknown' },
+	reason: { type: String, required: true, default: "Unknown" },
 	active: { type: Boolean, required: true, default: true },
 	expiresAt: { type: Date, required: true },
 	punishedAt: { type: Date, default: Date.now, required: true },

+ 1 - 1
backend/logic/db/schemas/queueSong.js

@@ -1,4 +1,4 @@
-module.exports = {
+export default {
 	songId: { type: String, min: 11, max: 11, required: true, index: true },
 	title: { type: String, required: true },
 	artists: [{ type: String }],

+ 8 - 6
backend/logic/db/schemas/report.js

@@ -1,14 +1,16 @@
-module.exports = {
+export default {
 	resolved: { type: Boolean, default: false, required: true },
 	song: {
 		_id: { type: String, required: true },
-		songId: { type: String, required: true },
+		songId: { type: String, required: true }
 	},
 	description: { type: String },
-	issues: [{
-		name: String,
-		reasons: Array
-	}],
+	issues: [
+		{
+			name: String,
+			reasons: Array
+		}
+	],
 	createdBy: { type: String, required: true },
 	createdAt: { type: Date, default: Date.now, required: true }
 };

+ 2 - 2
backend/logic/db/schemas/song.js

@@ -1,4 +1,4 @@
-module.exports = {
+export default {
 	songId: { type: String, min: 11, max: 11, required: true, index: true },
 	title: { type: String, required: true },
 	artists: [{ type: String }],
@@ -14,4 +14,4 @@ module.exports = {
 	acceptedBy: { type: String, required: true },
 	acceptedAt: { type: Date, default: Date.now, required: true },
 	discogs: { type: Object }
-};
+};

+ 16 - 14
backend/logic/db/schemas/station.js

@@ -1,6 +1,6 @@
-const mongoose = require('mongoose');
+import mongoose from "mongoose";
 
-module.exports = {
+export default {
 	name: { type: String, lowercase: true, maxlength: 16, minlength: 2, index: true, unique: true, required: true },
 	type: { type: String, enum: ["official", "community"], required: true },
 	displayName: { type: String, minlength: 2, maxlength: 32, required: true, unique: true },
@@ -15,7 +15,7 @@ module.exports = {
 		thumbnail: { type: String },
 		likes: { type: Number, default: -1 },
 		dislikes: { type: Number, default: -1 },
-		skipVotes: [{ type: String }],
+		skipVotes: [{ type: String }]
 	},
 	currentSongIndex: { type: Number, default: 0, required: true },
 	timePaused: { type: Number, default: 0, required: true },
@@ -26,17 +26,19 @@ module.exports = {
 	blacklistedGenres: [{ type: String }],
 	privacy: { type: String, enum: ["public", "unlisted", "private"], default: "private" },
 	locked: { type: Boolean, default: false },
-	queue: [{
-		songId: { type: String, required: true },
-		title: { type: String },
-		artists: [{ type: String }],
-		duration: { type: Number },
-		skipDuration: { type: Number },
-		thumbnail: { type: String },
-		likes: { type: Number, default: -1 },
-		dislikes: { type: Number, default: -1 },
-		requestedBy: { type: String, required: true }
-	}],
+	queue: [
+		{
+			songId: { type: String, required: true },
+			title: { type: String },
+			artists: [{ type: String }],
+			duration: { type: Number },
+			skipDuration: { type: Number },
+			thumbnail: { type: String },
+			likes: { type: Number, default: -1 },
+			dislikes: { type: Number, default: -1 },
+			requestedBy: { type: String, required: true }
+		}
+	],
 	owner: { type: String },
 	privatePlaylist: { type: mongoose.Schema.Types.ObjectId },
 	partyMode: { type: Boolean }

+ 2 - 2
backend/logic/db/schemas/user.js

@@ -1,6 +1,6 @@
-module.exports = {
+export default {
 	username: { type: String, required: true },
-	role: { type: String, default: 'default', required: true },
+	role: { type: String, default: "default", required: true },
 	email: {
 		verified: { type: Boolean, default: false, required: true },
 		verificationToken: String,

+ 96 - 108
backend/logic/discord.js

@@ -1,113 +1,101 @@
-const CoreClass = require("../core.js");
+import config from "config";
+import Discord from "discord.js";
 
-const Discord = require("discord.js");
-const config = require("config");
+import CoreClass from "../core";
+
+// const test = new CoreClass();
+// console.log(test.setModuleManager());
 
 class DiscordModule extends CoreClass {
-    constructor() {
-        super("discord");
-    }
-
-    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);
-        });
-    }
-
-    SEND_ADMIN_ALERT_MESSAGE(payload) {
-        return new Promise((resolve, reject) => {
-            const channel = this.client.channels.find(
-                (channel) => channel.id === this.adminAlertChannelId
-            );
-            if (channel !== null) {
-                let 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."));
-            // }
-        });
-    }
+	constructor() {
+		super("discord");
+
+		console.log(this.setModuleManager());
+	}
+
+	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);
+		});
+	}
+
+	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."));
+			// }
+		});
+	}
 }
 
-module.exports = new DiscordModule();
+export default new DiscordModule();

+ 313 - 377
backend/logic/io.js

@@ -1,382 +1,318 @@
-const CoreClass = require("../core.js");
+/**
+ * @file
+ */
 
-const socketio = require("socket.io");
-const async = require("async");
-const config = require("config");
+import config from "config";
+import async from "async";
+import socketio from "socket.io";
+
+import actions from "./actions";
+import CoreClass from "../core";
 
 class IOModule extends CoreClass {
-    constructor() {
-        super("io");
-    }
-
-    initialize() {
-        return new Promise(async (resolve, reject) => {
-            this.setStage(1);
-
-            const app = this.moduleManager.modules["app"],
-                cache = this.moduleManager.modules["cache"],
-                utils = this.moduleManager.modules["utils"],
-                db = this.moduleManager.modules["db"],
-                punishments = this.moduleManager.modules["punishments"];
-
-            const actions = require("./actions");
-
-            this.setStage(2);
-
-            const SIDname = config.get("cookie.SIDname");
-
-            // TODO: Check every 30s/60s, for all sockets, if they are still allowed to be in the rooms they are in, and on socket at all (permission changing/banning)
-            this._io = socketio(await app.runJob("SERVER", {}));
-
-            this.setStage(3);
-
-            this._io.use(async (socket, next) => {
-                if (this.getStatus() !== "READY") {
-                    this.log(
-                        "INFO",
-                        "IO_REJECTED_CONNECTION",
-                        `A user tried to connect, but the IO module is currently not ready. IP: ${socket.ip}.${sessionInfo}`
-                    );
-                    return socket.disconnect(true);
-                }
-
-                let SID;
-
-                socket.ip =
-                    socket.request.headers["x-forwarded-for"] || "0.0.0.0";
-
-                async.waterfall(
-                    [
-                        (next) => {
-                            utils
-                                .runJob("PARSE_COOKIES", {
-                                    cookieString: socket.request.headers.cookie,
-                                })
-                                .then((res) => {
-                                    SID = res[SIDname];
-                                    next(null);
-                                });
-                        },
-
-                        (next) => {
-                            if (!SID) return next("No SID.");
-                            next();
-                        },
-
-                        (next) => {
-                            cache
-                                .runJob("HGET", { table: "sessions", key: SID })
-                                .then((session) => {
-                                    next(null, session);
-                                });
-                        },
-
-                        (session, next) => {
-                            if (!session) return next("No session found.");
-
-                            session.refreshDate = Date.now();
-
-                            socket.session = session;
-                            cache
-                                .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", {})
-                                .then((punishments) => {
-                                    const isLoggedIn = !!(
-                                        socket.session &&
-                                        socket.session.refreshDate
-                                    );
-                                    const userId = isLoggedIn
-                                        ? socket.session.userId
-                                        : null;
-
-                                    let banishment = { banned: false, ban: 0 };
-
-                                    punishments.forEach((punishment) => {
-                                        if (
-                                            punishment.expiresAt >
-                                            banishment.ban
-                                        )
-                                            banishment.ban = punishment;
-                                        if (
-                                            punishment.type === "banUserId" &&
-                                            isLoggedIn &&
-                                            punishment.value === userId
-                                        )
-                                            banishment.banned = true;
-                                        if (
-                                            punishment.type === "banUserIp" &&
-                                            punishment.value === socket.ip
-                                        )
-                                            banishment.banned = true;
-                                    });
-
-                                    socket.banishment = banishment;
-
-                                    next();
-                                })
-                                .catch(() => {
-                                    next();
-                                });
-                        },
-                    ],
-                    () => {
-                        if (!socket.session)
-                            socket.session = { socketId: socket.id };
-                        else socket.session.socketId = socket.id;
-
-                        next();
-                    }
-                );
-            });
-
-            this.setStage(4);
-
-            this._io.on("connection", async (socket) => {
-                if (this.getStatus() !== "READY") {
-                    this.log(
-                        "INFO",
-                        "IO_REJECTED_CONNECTION",
-                        `A user tried to connect, but the IO module is currently not ready. IP: ${socket.ip}.${sessionInfo}`
-                    );
-                    return socket.disconnect(true);
-                }
-
-                let sessionInfo = "";
-
-                if (socket.session.sessionId)
-                    sessionInfo = ` UserID: ${socket.session.userId}.`;
-
-                // if session is banned
-                if (socket.banishment && socket.banishment.banned) {
-                    this.log(
-                        "INFO",
-                        "IO_BANNED_CONNECTION",
-                        `A user tried to connect, but is currently banned. IP: ${socket.ip}.${sessionInfo}`
-                    );
-                    socket.emit("keep.event:banned", socket.banishment.ban);
-                    socket.disconnect(true);
-                } else {
-                    this.log(
-                        "INFO",
-                        "IO_CONNECTION",
-                        `User connected. IP: ${socket.ip}.${sessionInfo}`
-                    );
-
-                    // catch when the socket has been disconnected
-                    socket.on("disconnect", () => {
-                        if (socket.session.sessionId)
-                            sessionInfo = ` UserID: ${socket.session.userId}.`;
-                        this.log(
-                            "INFO",
-                            "IO_DISCONNECTION",
-                            `User disconnected. IP: ${socket.ip}.${sessionInfo}`
-                        );
-                    });
-
-                    socket.use((data, next) => {
-                        if (data.length === 0)
-                            return next(
-                                new Error("Not enough arguments specified.")
-                            );
-                        else if (typeof data[0] !== "string")
-                            return next(
-                                new Error("First argument must be a string.")
-                            );
-                        else {
-                            const namespaceAction = data[0];
-                            if (
-                                !namespaceAction ||
-                                namespaceAction.indexOf(".") === -1 ||
-                                namespaceAction.indexOf(".") !==
-                                    namespaceAction.lastIndexOf(".")
-                            )
-                                return next(
-                                    new Error("Invalid first argument")
-                                );
-                            const namespace = data[0].split(".")[0];
-                            const action = data[0].split(".")[1];
-                            if (!namespace)
-                                return next(new Error("Invalid namespace."));
-                            else if (!action)
-                                return next(new Error("Invalid action."));
-                            else if (!actions[namespace])
-                                return next(new Error("Namespace not found."));
-                            else if (!actions[namespace][action])
-                                return next(new Error("Action not found."));
-                            else return next();
-                        }
-                    });
-
-                    // catch errors on the socket (internal to socket.io)
-                    socket.on("error", console.error);
-
-                    // have the socket listen for each action
-                    Object.keys(actions).forEach((namespace) => {
-                        Object.keys(actions[namespace]).forEach((action) => {
-                            // the full name of the action
-                            let name = `${namespace}.${action}`;
-
-                            // listen for this action to be called
-                            socket.on(name, async (...args) => {
-                                let cb = args[args.length - 1];
-                                if (typeof cb !== "function")
-                                    cb = () => {
-                                        this.this.log(
-                                            "INFO",
-                                            "IO_MODULE",
-                                            `There was no callback provided for ${name}.`
-                                        );
-                                    };
-                                else args.pop();
-
-                                if (this.getStatus() !== "READY") {
-                                    this.log(
-                                        "INFO",
-                                        "IO_REJECTED_ACTION",
-                                        `A user tried to execute an action, but the IO module is currently not ready. Action: ${namespace}.${action}.`
-                                    );
-                                    return;
-                                } else {
-                                    this.log(
-                                        "INFO",
-                                        "IO_ACTION",
-                                        `A user executed an action. Action: ${namespace}.${action}.`
-                                    );
-                                }
-
-                                // load the session from the cache
-                                cache
-                                    .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
-                                            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"
-                                                            )
-                                                                return cb(
-                                                                    result
-                                                                );
-                                                        },
-                                                    ])
-                                            );
-                                        } catch (err) {
-                                            this.log(
-                                                "ERROR",
-                                                "IO_ACTION_ERROR",
-                                                `Some type of exception occurred in the action ${namespace}.${action}. Error message: ${err.message}`
-                                            );
-                                            if (typeof cb === "function")
-                                                return cb({
-                                                    status: "error",
-                                                    message:
-                                                        "An error occurred while executing the specified action.",
-                                                });
-                                        }
-                                    })
-                                    .catch((err) => {
-                                        if (typeof cb === "function")
-                                            return cb({
-                                                status: "error",
-                                                message:
-                                                    "An error occurred while obtaining your session",
-                                            });
-                                    });
-                            });
-                        });
-                    });
-
-                    if (socket.session.sessionId) {
-                        cache
-                            .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;
-                                                }
-                                                socket.emit(
-                                                    "ready",
-                                                    true,
-                                                    role,
-                                                    username,
-                                                    userId
-                                                );
-                                            }
-                                        );
-                                    });
-                                } else socket.emit("ready", false);
-                            })
-                            .catch((err) => {
-                                socket.emit("ready", false);
-                            });
-                    } else socket.emit("ready", false);
-                }
-            });
-
-            this.setStage(5);
-
-            resolve();
-        });
-    }
-
-    IO() {
-        return new Promise((resolve, reject) => {
-            resolve(this._io);
-        });
-    }
+	constructor() {
+		super("io");
+	}
+
+	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;
+
+		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");
+		this._io = socketio(server);
+
+		return new Promise(resolve => {
+			this.setStage(3);
+
+			this._io.use(async (socket, cb) => {
+				if (this.getStatus() !== "READY") {
+					this.log(
+						"INFO",
+						"IO_REJECTED_CONNECTION",
+						`A user tried to connect, but the IO module is currently not ready. IP: ${socket.ip}`
+					);
+					return socket.disconnect(true);
+				}
+
+				let SID;
+
+				socket.ip = socket.request.headers["x-forwarded-for"] || "0.0.0.0";
+
+				return async.waterfall(
+					[
+						next => {
+							utils
+								.runJob("PARSE_COOKIES", {
+									cookieString: socket.request.headers.cookie
+								})
+								.then(res => {
+									SID = res[SIDname];
+									next(null);
+								});
+						},
+
+						next => {
+							if (!SID) return next("No SID.");
+							return next();
+						},
+
+						next => {
+							cache.runJob("HGET", { table: "sessions", key: SID }).then(session => {
+								next(null, session);
+							});
+						},
+
+						(session, next) => {
+							if (!session) return next("No session found.");
+
+							session.refreshDate = Date.now();
+
+							socket.session = session;
+
+							return cache
+								.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", {})
+								.then(punishments => {
+									const isLoggedIn = !!(socket.session && socket.session.refreshDate);
+									const userId = isLoggedIn ? socket.session.userId : null;
+
+									const banishment = {
+										banned: false,
+										ban: 0
+									};
+
+									punishments.forEach(punishment => {
+										if (punishment.expiresAt > banishment.ban) banishment.ban = punishment;
+										if (
+											punishment.type === "banUserId" &&
+											isLoggedIn &&
+											punishment.value === userId
+										)
+											banishment.banned = true;
+										if (punishment.type === "banUserIp" && punishment.value === socket.ip)
+											banishment.banned = true;
+									});
+
+									socket.banishment = banishment;
+
+									next();
+								})
+								.catch(() => {
+									next();
+								});
+						}
+					],
+					() => {
+						if (!socket.session) socket.session = { socketId: socket.id };
+						else socket.session.socketId = socket.id;
+
+						cb();
+					}
+				);
+			});
+
+			this.setStage(4);
+
+			this._io.on("connection", async socket => {
+				let sessionInfo = "";
+
+				if (this.getStatus() !== "READY") {
+					this.log(
+						"INFO",
+						"IO_REJECTED_CONNECTION",
+						`A user tried to connect, but the IO module is currently not ready. IP: ${socket.ip}.${sessionInfo}`
+					);
+					return socket.disconnect(true);
+				}
+
+				if (socket.session.sessionId) sessionInfo = ` UserID: ${socket.session.userId}.`;
+
+				// if session is banned
+				if (socket.banishment && socket.banishment.banned) {
+					this.log(
+						"INFO",
+						"IO_BANNED_CONNECTION",
+						`A user tried to connect, but is currently banned. IP: ${socket.ip}.${sessionInfo}`
+					);
+
+					socket.emit("keep.event:banned", socket.banishment.ban);
+
+					return socket.disconnect(true);
+				}
+
+				this.log("INFO", "IO_CONNECTION", `User connected. IP: ${socket.ip}.${sessionInfo}`);
+
+				// catch when the socket has been disconnected
+				socket.on("disconnect", () => {
+					if (socket.session.sessionId) sessionInfo = ` UserID: ${socket.session.userId}.`;
+					this.log("INFO", "IO_DISCONNECTION", `User disconnected. IP: ${socket.ip}.${sessionInfo}`);
+				});
+
+				socket.use((data, next) => {
+					if (data.length === 0) return next(new Error("Not enough arguments specified."));
+					if (typeof data[0] !== "string") return next(new Error("First argument must be a string."));
+
+					const namespaceAction = data[0];
+					if (
+						!namespaceAction ||
+						namespaceAction.indexOf(".") === -1 ||
+						namespaceAction.indexOf(".") !== namespaceAction.lastIndexOf(".")
+					)
+						return next(new Error("Invalid first argument"));
+					const namespace = data[0].split(".")[0];
+					const action = data[0].split(".")[1];
+
+					if (!namespace) return next(new Error("Invalid namespace."));
+					if (!action) return next(new Error("Invalid action."));
+					if (!actions[namespace]) return next(new Error("Namespace not found."));
+					if (!actions[namespace][action]) return next(new Error("Action not found."));
+
+					return next();
+				});
+
+				// catch errors on the socket (internal to socket.io)
+				socket.on("error", console.error);
+
+				if (socket.session.sessionId) {
+					cache
+						.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);
+									});
+								});
+							} else socket.emit("ready", false);
+						})
+						.catch(() => socket.emit("ready", false));
+				} else socket.emit("ready", false);
+
+				// have the socket listen for each action
+				return Object.keys(actions).forEach(namespace => {
+					Object.keys(actions[namespace]).forEach(action => {
+						// the full name of the action
+						const name = `${namespace}.${action}`;
+
+						// listen for this action to be called
+						socket.on(name, async (...args) => {
+							let cb = args[args.length - 1];
+
+							if (typeof cb !== "function")
+								cb = () => {
+									this.this.log("INFO", "IO_MODULE", `There was no callback provided for ${name}.`);
+								};
+							else args.pop();
+
+							if (this.getStatus() !== "READY") {
+								this.log(
+									"INFO",
+									"IO_REJECTED_ACTION",
+									`A user tried to execute an action, but the IO module is currently not ready. Action: ${namespace}.${action}.`
+								);
+								return;
+							}
+							this.log("INFO", "IO_ACTION", `A user executed an action. Action: ${namespace}.${action}.`);
+
+							// load the session from the cache
+							cache
+								.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) {
+										if (typeof cb === "function")
+											cb({
+												status: "error",
+												message: "An error occurred while executing the specified action."
+											});
+
+										return this.log(
+											"ERROR",
+											"IO_ACTION_ERROR",
+											`Some type of exception occurred in the action ${namespace}.${action}. Error message: ${err.message}`
+										);
+									}
+								})
+								.catch(() => {
+									if (typeof cb === "function")
+										cb({
+											status: "error",
+											message: "An error occurred while obtaining your session"
+										});
+								});
+						});
+					});
+				});
+			});
+
+			this.setStage(5);
+
+			return resolve();
+		});
+	}
+
+	IO() {
+		return new Promise(resolve => {
+			resolve(this._io);
+		});
+	}
 }
 
-module.exports = new IOModule();
+export default new IOModule();

+ 39 - 45
backend/logic/mail/index.js

@@ -1,50 +1,44 @@
-const CoreClass = require("../../core.js");
+/* eslint-disable global-require */
+import mailgun from "mailgun-js";
 
-const config = require("config");
-
-let mailgun = null;
+import CoreClass from "../../core";
 
 class MailModule extends CoreClass {
-    constructor() {
-        super("mail");
-    }
-
-    initialize() {
-        return new Promise((resolve, reject) => {
-            this.schemas = {
-                verifyEmail: require("./schemas/verifyEmail"),
-                resetPasswordRequest: require("./schemas/resetPasswordRequest"),
-                passwordRequest: require("./schemas/passwordRequest"),
-            };
-
-            this.enabled = config.get("apis.mailgun.enabled");
-
-            if (this.enabled)
-                mailgun = require("mailgun-js")({
-                    apiKey: config.get("apis.mailgun.key"),
-                    domain: config.get("apis.mailgun.domain"),
-                });
-
-            resolve();
-        });
-    }
-
-    SEND_MAIL(payload) {
-        //data, cb
-        return new Promise((resolve, reject) => {
-            if (this.enabled)
-                mailgun.messages().send(payload.data, () => {
-                    resolve();
-                });
-            else resolve();
-        });
-    }
-
-    GET_SCHEMA(payload) {
-        return new Promise((resolve, reject) => {
-            resolve(this.schemas[payload.schemaName]);
-        });
-    }
+	constructor() {
+		super("mail");
+	}
+
+	async initialize() {
+		const importSchema = schemaName =>
+			new Promise(resolve => {
+				import(`./schemas/${schemaName}`).then(schema => resolve(schema.default));
+			});
+
+		this.schemas = {
+			verifyEmail: await importSchema("verifyEmail"),
+			resetPasswordRequest: await importSchema("resetPasswordRequest"),
+			passwordRequest: await importSchema("passwordRequest")
+		};
+
+		return new Promise(resolve => resolve());
+	}
+
+	SEND_MAIL(payload) {
+		// data, cb
+		return new Promise(resolve => {
+			if (this.enabled)
+				mailgun.messages().send(payload.data, () => {
+					resolve();
+				});
+			else resolve();
+		});
+	}
+
+	GET_SCHEMA(payload) {
+		return new Promise(resolve => {
+			resolve(this.schemas[payload.schemaName]);
+		});
+	}
 }
 
-module.exports = new MailModule();
+export default new MailModule();

+ 18 - 20
backend/logic/mail/schemas/passwordRequest.js

@@ -1,23 +1,21 @@
-const config = require("config");
-
 // const moduleManager = require('../../../index');
 
-const mail = require("../index");
+import mail from "../index";
 
 /**
  * Sends a request password email
  *
- * @param {String} to - the email address of the recipient
- * @param {String} username - the username of the recipient
- * @param {String} code - the password code of the recipient
+ * @param {string} to - the email address of the recipient
+ * @param {string} username - the username of the recipient
+ * @param {string} code - the password code of the recipient
  * @param {Function} cb - gets called when an error occurred or when the operation was successful
  */
-module.exports = function(to, username, code, cb) {
-    let data = {
-        from: "Musare <noreply@musare.com>",
-        to: to,
-        subject: "Password request",
-        html: `
+export default (to, username, code, cb) => {
+	const data = {
+		from: "Musare <noreply@musare.com>",
+		to,
+		subject: "Password request",
+		html: `
 				Hello there ${username},
 				<br>
 				<br>
@@ -26,13 +24,13 @@ module.exports = function(to, username, code, cb) {
 				<br>
 				The code is <b>${code}</b>. You can enter this code on the page you requested the password on. This code will expire in 24 hours.
 			`
-    };
+	};
 
-    mail.runJob("SEND_MAIL", { data })
-        .then(() => {
-            cb();
-        })
-        .catch(err => {
-            cb(err);
-        });
+	mail.runJob("SEND_MAIL", { data })
+		.then(() => {
+			cb();
+		})
+		.catch(err => {
+			cb(err);
+		});
 };

+ 18 - 20
backend/logic/mail/schemas/resetPasswordRequest.js

@@ -1,23 +1,21 @@
-const config = require("config");
-
 // const moduleManager = require('../../../index');
 
-const mail = require("../index");
+import mail from "../index";
 
 /**
  * Sends a request password reset email
  *
- * @param {String} to - the email address of the recipient
- * @param {String} username - the username of the recipient
- * @param {String} code - the password reset code of the recipient
+ * @param {string} to - the email address of the recipient
+ * @param {string} username - the username of the recipient
+ * @param {string} code - the password reset code of the recipient
  * @param {Function} cb - gets called when an error occurred or when the operation was successful
  */
-module.exports = function(to, username, code, cb) {
-    let data = {
-        from: "Musare <noreply@musare.com>",
-        to: to,
-        subject: "Password reset request",
-        html: `
+export default (to, username, code, cb) => {
+	const data = {
+		from: "Musare <noreply@musare.com>",
+		to,
+		subject: "Password reset request",
+		html: `
 				Hello there ${username},
 				<br>
 				<br>
@@ -26,13 +24,13 @@ module.exports = function(to, username, code, cb) {
 				<br>
 				The reset code is <b>${code}</b>. You can enter this code on the page you requested the password reset. This code will expire in 24 hours.
 			`
-    };
+	};
 
-    mail.runJob("SEND_MAIL", { data })
-        .then(() => {
-            cb();
-        })
-        .catch(err => {
-            cb(err);
-        });
+	mail.runJob("SEND_MAIL", { data })
+		.then(() => {
+			cb();
+		})
+		.catch(err => {
+			cb(err);
+		});
 };

+ 22 - 24
backend/logic/mail/schemas/verifyEmail.js

@@ -1,39 +1,37 @@
-const config = require("config");
+import config from "config";
 
 // const moduleManager = require('../../../index');
 
-const mail = require("../index");
+import mail from "../index";
 
 /**
  * Sends a verify email email
  *
- * @param {String} to - the email address of the recipient
- * @param {String} username - the username of the recipient
- * @param {String} code - the email reset code of the recipient
+ * @param {string} to - the email address of the recipient
+ * @param {string} username - the username of the recipient
+ * @param {string} code - the email reset code of the recipient
  * @param {Function} cb - gets called when an error occurred or when the operation was successful
  */
-module.exports = function(to, username, code, cb) {
-    let data = {
-        from: "Musare <noreply@musare.com>",
-        to: to,
-        subject: "Please verify your email",
-        html: `
+export default (to, username, code, cb) => {
+	const data = {
+		from: "Musare <noreply@musare.com>",
+		to,
+		subject: "Please verify your email",
+		html: `
 				Hello there ${username},
 				<br>
 				<br>
-				To verify your email, please visit <a href="${config.get(
-                    "serverDomain"
-                )}/auth/verify_email?code=${code}">${config.get(
-            "serverDomain"
-        )}/auth/verify_email?code=${code}</a>.
+				To verify your email, please visit <a href="${config.get("serverDomain")}/auth/verify_email?code=${code}">${config.get(
+			"serverDomain"
+		)}/auth/verify_email?code=${code}</a>.
 			`
-    };
+	};
 
-    mail.runJob("SEND_MAIL", { data })
-        .then(() => {
-            cb();
-        })
-        .catch(err => {
-            cb(err);
-        });
+	mail.runJob("SEND_MAIL", { data })
+		.then(() => {
+			cb();
+		})
+		.catch(err => {
+			cb(err);
+		});
 };

+ 244 - 265
backend/logic/notifications.js

@@ -1,271 +1,250 @@
-const CoreClass = require("../core.js");
+import config from "config";
 
-const crypto = require("crypto");
-const redis = require("redis");
-const config = require("config");
-const utils = require("./utils");
+import crypto from "crypto";
+import redis from "redis";
 
-const subscriptions = [];
+import CoreClass from "../core";
+import utils from "./utils";
 
 class NotificationsModule extends CoreClass {
-    constructor() {
-        super("notifications");
-    }
-
-    initialize() {
-        return new Promise((resolve, reject) => {
-            const url = (this.url = config.get("redis").url);
-            const password = (this.password = config.get("redis").password);
-
-            this.pub = redis.createClient({
-                url,
-                password,
-                retry_strategy: (options) => {
-                    if (this.getStatus() === "LOCKDOWN") return;
-                    if (this.getStatus() !== "RECONNECTING")
-                        this.setStatus("RECONNECTING");
-
-                    this.log("INFO", `Attempting to reconnect.`);
-
-                    if (options.attempt >= 10) {
-                        this.log("ERROR", `Stopped trying to reconnect.`);
-
-                        this.setStatus("FAILED");
-
-                        // this.failed = true;
-                        // this._lockdown();
-
-                        return undefined;
-                    }
-
-                    return 3000;
-                },
-            });
-            this.sub = redis.createClient({
-                url,
-                password,
-                retry_strategy: (options) => {
-                    if (this.getStatus() === "LOCKDOWN") return;
-                    if (this.getStatus() !== "RECONNECTING")
-                        this.setStatus("RECONNECTING");
-
-                    this.log("INFO", `Attempting to reconnect.`);
-
-                    if (options.attempt >= 10) {
-                        this.log("ERROR", `Stopped trying to reconnect.`);
-
-                        this.setStatus("FAILED");
-
-                        // this.failed = true;
-                        // this._lockdown();
-
-                        return undefined;
-                    }
-
-                    return 3000;
-                },
-            });
-
-            this.sub.on("error", (err) => {
-                if (this.getStatus() === "INITIALIZING") reject(err);
-                if (this.getStatus() === "LOCKDOWN") return;
-
-                this.log("ERROR", `Error ${err.message}.`);
-            });
-
-            this.pub.on("error", (err) => {
-                if (this.getStatus() === "INITIALIZING") reject(err);
-                if (this.getStatus() === "LOCKDOWN") return;
-
-                this.log("ERROR", `Error ${err.message}.`);
-            });
-
-            this.sub.on("connect", () => {
-                this.log("INFO", "Sub connected succesfully.");
-
-                if (this.getStatus() === "INITIALIZING") resolve();
-                else if (
-                    this.getStatus() === "LOCKDOWN" ||
-                    this.getStatus() === "RECONNECTING"
-                )
-                    this.setStatus("READY");
-            });
-
-            this.pub.on("connect", () => {
-                this.log("INFO", "Pub connected succesfully.");
-
-                this.pub.config("GET", "notify-keyspace-events", async (err, response) => {
-                    if (err) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        this.log("ERROR", "NOTIFICATIONS_INITIALIZE", `Getting notify-keyspace-events gave an error. ${err}`);
-                        this.log(
-                            "STATION_ISSUE",
-                            `Getting notify-keyspace-events gave an error. ${err}. ${response}`
-                        );
-                        return;
-                    }
-                    if (response[1] === "xE") {
-                        this.log("INFO", "NOTIFICATIONS_INITIALIZE", `notify-keyspace-events is set correctly`);
-                        this.log("STATION_ISSUE", `notify-keyspace-events is set correctly`);
-                    } else {
-                        this.log("ERROR", "NOTIFICATIONS_INITIALIZE", `notify-keyspace-events is NOT correctly! It is set to: ${response[1]}`);
-                        this.log("STATION_ISSUE", `notify-keyspace-events is NOT correctly! It is set to: ${response[1]}`);
-                    }
-                });
-
-                if (this.getStatus() === "INITIALIZING") resolve();
-                else if (
-                    this.getStatus() === "LOCKDOWN" ||
-                    this.getStatus() === "RECONNECTING"
-                )
-                    this.setStatus("INITIALIZED");
-            });
-
-            this.sub.on("pmessage", (pattern, channel, expiredKey) => {
-                this.log(
-                    "STATION_ISSUE",
-                    `PMESSAGE1 - Pattern: ${pattern}; Channel: ${channel}; ExpiredKey: ${expiredKey}`
-                );
-                subscriptions.forEach((sub) => {
-                    this.log(
-                        "STATION_ISSUE",
-                        `PMESSAGE2 - Sub name: ${sub.name}; Calls cb: ${!(
-                            sub.name !== expiredKey
-                        )}`
-                    );
-                    if (sub.name !== expiredKey) return;
-                    sub.cb();
-                });
-            });
-
-            this.sub.psubscribe(`__keyevent@${this.pub.options.db}__:expired`);
-        });
-    }
-
-    /**
-     * Schedules a notification to be dispatched in a specific amount of milliseconds,
-     * notifications are unique by name, and the first one is always kept, as in
-     * attempting to schedule a notification that already exists won't do anything
-     *
-     * @param {String} name - the name of the notification we want to schedule
-     * @param {Integer} time - how long in milliseconds until the notification should be fired
-     * @param {Function} cb - gets called when the notification has been scheduled
-     */
-    SCHEDULE(payload) {
-        //name, time, cb, station
-        return new Promise((resolve, reject) => {
-            const time = Math.round(payload.time);
-            this.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(
-                crypto
-                    .createHash("md5")
-                    .update(`_notification:${payload.name}_`)
-                    .digest("hex"),
-                "",
-                "PX",
-                time,
-                "NX",
-                (err) => {
-                    if (err) reject(err);
-                    else resolve();
-                }
-            );
-        });
-    }
-
-    /**
-     * Subscribes a callback function to be called when a notification gets called
-     *
-     * @param {String} name - the name of the notification we want to subscribe to
-     * @param {Function} cb - gets called when the subscribed notification gets called
-     * @param {Boolean} unique - only subscribe if another subscription with the same name doesn't already exist
-     * @return {Object} - the subscription object
-     */
-    SUBSCRIBE(payload) {
-        //name, cb, unique = false, station
-        return new Promise((resolve, reject) => {
-            this.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: ${!!subscriptions.find(
-                    (subscription) => subscription.originalName === payload.name
-                )};`
-            );
-            if (
-                payload.unique &&
-                !!subscriptions.find(
-                    (subscription) => subscription.originalName === payload.name
-                )
-            )
-                return resolve({
-                    subscription: subscriptions.find(
-                        (subscription) =>
-                            subscription.originalName === payload.name
-                    ),
-                });
-            let subscription = {
-                originalName: payload.name,
-                name: crypto
-                    .createHash("md5")
-                    .update(`_notification:${payload.name}_`)
-                    .digest("hex"),
-                cb: payload.cb,
-            };
-            subscriptions.push(subscription);
-            resolve({ subscription });
-        });
-    }
-
-    /**
-     * Remove a notification subscription
-     *
-     * @param {Object} subscription - the subscription object returned by {@link subscribe}
-     */
-    REMOVE(payload) {
-        //subscription
-        return new Promise((resolve, reject) => {
-            let index = subscriptions.indexOf(payload.subscription);
-            if (index) subscriptions.splice(index, 1);
-            resolve();
-        });
-    }
-
-    UNSCHEDULE(payload) {
-        //name
-        return new Promise((resolve, reject) => {
-            this.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();
-                }
-            );
-        });
-    }
+	constructor() {
+		super("notifications");
+
+		this.subscriptions = [];
+	}
+
+	initialize() {
+		return new Promise((resolve, reject) => {
+			const url = (this.url = config.get("redis").url);
+			const password = (this.password = config.get("redis").password);
+
+			this.pub = redis.createClient({
+				url,
+				password,
+				retry_strategy: options => {
+					if (this.getStatus() === "LOCKDOWN") return;
+					if (this.getStatus() !== "RECONNECTING") this.setStatus("RECONNECTING");
+
+					this.log("INFO", `Attempting to reconnect.`);
+
+					if (options.attempt >= 10) {
+						this.log("ERROR", `Stopped trying to reconnect.`);
+
+						this.setStatus("FAILED");
+
+						// this.failed = true;
+						// this._lockdown();
+					}
+				}
+			});
+			this.sub = redis.createClient({
+				url,
+				password,
+				retry_strategy: options => {
+					if (this.getStatus() === "LOCKDOWN") return;
+					if (this.getStatus() !== "RECONNECTING") this.setStatus("RECONNECTING");
+
+					this.log("INFO", `Attempting to reconnect.`);
+
+					if (options.attempt >= 10) {
+						this.log("ERROR", `Stopped trying to reconnect.`);
+
+						this.setStatus("FAILED");
+
+						// this.failed = true;
+						// this._lockdown();
+					}
+				}
+			});
+
+			this.sub.on("error", err => {
+				if (this.getStatus() === "INITIALIZING") reject(err);
+				if (this.getStatus() === "LOCKDOWN") return;
+
+				this.log("ERROR", `Error ${err.message}.`);
+			});
+
+			this.pub.on("error", err => {
+				if (this.getStatus() === "INITIALIZING") reject(err);
+				if (this.getStatus() === "LOCKDOWN") return;
+
+				this.log("ERROR", `Error ${err.message}.`);
+			});
+
+			this.sub.on("connect", () => {
+				this.log("INFO", "Sub connected succesfully.");
+
+				if (this.getStatus() === "INITIALIZING") resolve();
+				else if (this.getStatus() === "LOCKDOWN" || this.getStatus() === "RECONNECTING")
+					this.setStatus("READY");
+			});
+
+			this.pub.on("connect", () => {
+				this.log("INFO", "Pub connected succesfully.");
+
+				this.pub.config("GET", "notify-keyspace-events", async (err, response) => {
+					if (err) {
+						const formattedErr = await utils.runJob("GET_ERROR", {
+							error: err
+						});
+						this.log(
+							"ERROR",
+							"NOTIFICATIONS_INITIALIZE",
+							`Getting notify-keyspace-events gave an error. ${formattedErr}`
+						);
+						this.log(
+							"STATION_ISSUE",
+							`Getting notify-keyspace-events gave an error. ${formattedErr}. ${response}`
+						);
+						return;
+					}
+					if (response[1] === "xE") {
+						this.log("INFO", "NOTIFICATIONS_INITIALIZE", `notify-keyspace-events is set correctly`);
+						this.log("STATION_ISSUE", `notify-keyspace-events is set correctly`);
+					} else {
+						this.log(
+							"ERROR",
+							"NOTIFICATIONS_INITIALIZE",
+							`notify-keyspace-events is NOT correctly! It is set to: ${response[1]}`
+						);
+						this.log(
+							"STATION_ISSUE",
+							`notify-keyspace-events is NOT correctly! It is set to: ${response[1]}`
+						);
+					}
+				});
+
+				if (this.getStatus() === "INITIALIZING") resolve();
+				else if (this.getStatus() === "LOCKDOWN" || this.getStatus() === "RECONNECTING")
+					this.setStatus("INITIALIZED");
+			});
+
+			this.sub.on("pmessage", (pattern, channel, expiredKey) => {
+				this.log(
+					"STATION_ISSUE",
+					`PMESSAGE1 - Pattern: ${pattern}; Channel: ${channel}; ExpiredKey: ${expiredKey}`
+				);
+
+				this.subscriptions.forEach(sub => {
+					this.log(
+						"STATION_ISSUE",
+						`PMESSAGE2 - Sub name: ${sub.name}; Calls cb: ${!(sub.name !== expiredKey)}`
+					);
+					if (sub.name !== expiredKey) return;
+					sub.cb();
+				});
+			});
+
+			this.sub.psubscribe(`__keyevent@${this.pub.options.db}__:expired`);
+		});
+	}
+
+	/**
+	 * Schedules a notification to be dispatched in a specific amount of milliseconds,
+	 * notifications are unique by name, and the first one is always kept, as in
+	 * attempting to schedule a notification that already exists won't do anything
+	 *
+	 * @param {object} payload - object containing the payload
+	 * @param {string} payload.name - the name of the notification we want to schedule
+	 * @param {number} payload.time - how long in milliseconds until the notification should be fired
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	SCHEDULE(payload) {
+		// name, time, cb, station
+		return new Promise((resolve, reject) => {
+			const time = Math.round(payload.time);
+			this.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(
+				crypto.createHash("md5").update(`_notification:${payload.name}_`).digest("hex"),
+				"",
+				"PX",
+				time,
+				"NX",
+				err => {
+					if (err) reject(err);
+					else resolve();
+				}
+			);
+		});
+	}
+
+	/**
+	 * Subscribes a callback function to be called when a notification gets called
+	 *
+	 * @param {object} payload - object containing the payload
+	 * @param {string} payload.name - the name of the notification we want to subscribe to
+	 * @param {boolean} payload.unique - only subscribe if another subscription with the same name doesn't already exist
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	SUBSCRIBE(payload) {
+		// name, cb, unique = false, station
+		return new Promise(resolve => {
+			this.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(
+					subscription => subscription.originalName === payload.name
+				)};`
+			);
+			if (payload.unique && !!this.subscriptions.find(subscription => subscription.originalName === payload.name))
+				return resolve({
+					subscription: this.subscriptions.find(subscription => subscription.originalName === payload.name)
+				});
+
+			const subscription = {
+				originalName: payload.name,
+				name: crypto.createHash("md5").update(`_notification:${payload.name}_`).digest("hex"),
+				cb: payload.cb
+			};
+
+			this.subscriptions.push(subscription);
+
+			return resolve({ subscription });
+		});
+	}
+
+	/**
+	 * Remove a notification subscription
+	 *
+	 * @param {object} payload - object containing the payload
+	 * @param {object} payload.subscription - the subscription object returned by {@link subscribe}
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	REMOVE(payload) {
+		// subscription
+		return new Promise(resolve => {
+			const index = this.subscriptions.indexOf(payload.subscription);
+			if (index) this.subscriptions.splice(index, 1);
+			resolve();
+		});
+	}
+
+	UNSCHEDULE(payload) {
+		// name
+		return new Promise((resolve, reject) => {
+			this.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();
+			});
+		});
+	}
 }
 
-module.exports = new NotificationsModule();
+export default new NotificationsModule();

+ 286 - 295
backend/logic/playlists.js

@@ -1,297 +1,288 @@
-const CoreClass = require("../core.js");
-
-const async = require("async");
-
-class ExampleModule extends CoreClass {
-    constructor() {
-        super("playlists");
-    }
-
-    initialize() {
-        return new Promise(async (resolve, reject) => {
-            this.setStage(1);
-
-            this.cache = this.moduleManager.modules["cache"];
-            this.db = this.moduleManager.modules["db"];
-            this.utils = 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.setStage(2);
-
-            async.waterfall(
-                [
-                    (next) => {
-                        this.setStage(3);
-                        this.cache
-                            .runJob("HGETALL", { table: "playlists" })
-                            .then((playlists) => {
-                                next(null, playlists);
-                            })
-                            .catch(next);
-                    },
-
-                    (playlists, next) => {
-                        this.setStage(4);
-                        if (!playlists) return next();
-                        let playlistIds = Object.keys(playlists);
-                        async.each(
-                            playlistIds,
-                            (playlistId, next) => {
-                                playlistModel.findOne(
-                                    { _id: playlistId },
-                                    (err, playlist) => {
-                                        if (err) next(err);
-                                        else if (!playlist) {
-                                            this.cache
-                                                .runJob("HDEL", {
-                                                    table: "playlists",
-                                                    key: playlistId,
-                                                })
-                                                .then(() => {
-                                                    next();
-                                                })
-                                                .catch(next);
-                                        } else next();
-                                    }
-                                );
-                            },
-                            next
-                        );
-                    },
-
-                    (next) => {
-                        this.setStage(5);
-                        playlistModel.find({}, next);
-                    },
-
-                    (playlists, next) => {
-                        this.setStage(6);
-                        async.each(
-                            playlists,
-                            (playlist, next) => {
-                                this.cache
-                                    .runJob("HSET", {
-                                        table: "playlists",
-                                        key: playlist._id,
-                                        value: playlistSchema(playlist),
-                                    })
-                                    .then(() => {
-                                        next();
-                                    })
-                                    .catch(next);
-                            },
-                            next
-                        );
-                    },
-                ],
-                async (err) => {
-                    if (err) {
-                        err = await this.utils.runJob("GET_ERROR", {
-                            error: err,
-                        });
-                        reject(new Error(err));
-                    } else {
-                        resolve();
-                    }
-                }
-            );
-        });
-    }
-
-    /**
-     * Gets a playlist by id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
-     *
-     * @param {String} playlistId - the id of the playlist we are trying to get
-     * @param {Function} cb - gets called once we're done initializing
-     */
-    GET_PLAYLIST(payload) {
-        //playlistId, cb
-        return new Promise(async (resolve, reject) => {
-            const playlistModel = await this.db.runJob("GET_MODEL", {
-                modelName: "playlist",
-            });
-
-            async.waterfall(
-                [
-                    (next) => {
-                        this.cache
-                            .runJob("HGETALL", { table: "playlists" })
-                            .then((playlists) => {
-                                next(null, playlists);
-                            })
-                            .catch(next);
-                    },
-
-                    (playlists, next) => {
-                        if (!playlists) return next();
-                        let playlistIds = Object.keys(playlists);
-                        async.each(
-                            playlistIds,
-                            (playlistId, next) => {
-                                playlistModel.findOne(
-                                    { _id: playlistId },
-                                    (err, playlist) => {
-                                        if (err) next(err);
-                                        else if (!playlist) {
-                                            this.cache
-                                                .runJob("HDEL", {
-                                                    table: "playlists",
-                                                    key: playlistId,
-                                                })
-                                                .then(() => {
-                                                    next();
-                                                })
-                                                .catch(next);
-                                        } else next();
-                                    }
-                                );
-                            },
-                            next
-                        );
-                    },
-
-                    (next) => {
-                        this.cache
-                            .runJob("HGET", {
-                                table: "playlists",
-                                key: payload.playlistId,
-                            })
-                            .then((playlist) => {
-                                next(null, playlist);
-                            })
-                            .catch(next);
-                    },
-
-                    (playlist, next) => {
-                        if (playlist) return next(true, playlist);
-                        playlistModel.findOne(
-                            { _id: payload.playlistId },
-                            next
-                        );
-                    },
-
-                    (playlist, next) => {
-                        if (playlist) {
-                            this.cache
-                                .runJob("HSET", {
-                                    table: "playlists",
-                                    key: payload.playlistId,
-                                    value: playlist,
-                                })
-                                .then((playlist) => {
-                                    next(null, playlist);
-                                })
-                                .catch(next);
-                        } else next("Playlist not found");
-                    },
-                ],
-                (err, playlist) => {
-                    if (err && err !== true) return reject(new Error(err));
-                    resolve(playlist);
-                }
-            );
-        });
-    }
-
-    /**
-     * Gets a playlist from id from Mongo and updates the cache with it
-     *
-     * @param {String} playlistId - the id of the playlist we are trying to update
-     * @param {Function} cb - gets called when an error occurred or when the operation was successful
-     */
-    UPDATE_PLAYLIST(payload) {
-        //playlistId, cb
-        return new Promise(async (resolve, reject) => {
-            const playlistModel = await this.db.runJob("GET_MODEL", {
-                modelName: "playlist",
-            });
-
-            async.waterfall(
-                [
-                    (next) => {
-                        playlistModel.findOne(
-                            { _id: payload.playlistId },
-                            next
-                        );
-                    },
-
-                    (playlist, next) => {
-                        if (!playlist) {
-                            this.cache.runJob("HDEL", {
-                                table: "playlists",
-                                key: payload.playlistId,
-                            });
-                            return next("Playlist not found");
-                        }
-                        this.cache
-                            .runJob("HSET", {
-                                table: "playlists",
-                                key: payload.playlistId,
-                                value: playlist,
-                            })
-                            .then((playlist) => {
-                                next(null, playlist);
-                            })
-                            .catch(next);
-                    },
-                ],
-                (err, playlist) => {
-                    if (err && err !== true) return reject(new Error(err));
-                    resolve(playlist);
-                }
-            );
-        });
-    }
-
-    /**
-     * Deletes playlist from id from Mongo and cache
-     *
-     * @param {String} playlistId - the id of the playlist we are trying to delete
-     * @param {Function} cb - gets called when an error occurred or when the operation was successful
-     */
-    DELETE_PLAYLIST(payload) {
-        //playlistId, cb
-        return new Promise(async (resolve, reject) => {
-            const playlistModel = await this.db.runJob("GET_MODEL", {
-                modelName: "playlist",
-            });
-
-            async.waterfall(
-                [
-                    (next) => {
-                        playlistModel.deleteOne(
-                            { _id: payload.playlistId },
-                            next
-                        );
-                    },
-
-                    (res, next) => {
-                        this.cache
-                            .runJob("HDEL", {
-                                table: "playlists",
-                                key: payload.playlistId,
-                            })
-                            .then(() => {
-                                next();
-                            })
-                            .catch(next);
-                    },
-                ],
-                (err) => {
-                    if (err && err !== true) return reject(new Error(err));
-
-                    resolve();
-                }
-            );
-        });
-    }
+import async from "async";
+
+import CoreClass from "../core";
+
+class PlaylistsModule extends CoreClass {
+	constructor() {
+		super("playlists");
+	}
+
+	async initialize() {
+		this.setStage(1);
+
+		this.cache = this.moduleManager.modules.cache;
+		this.db = this.moduleManager.modules.db;
+		this.utils = this.moduleManager.modules.utils;
+
+		const playlistModel = await this.db.runJob("GET_MODEL", { modelName: "playlist" });
+		const playlistSchema = await this.db.runJob("GET_SCHEMA", { schemaName: "playlist" });
+
+		this.setStage(2);
+
+		return new Promise((resolve, reject) =>
+			async.waterfall(
+				[
+					next => {
+						this.setStage(3);
+						this.cache
+							.runJob("HGETALL", { table: "playlists" })
+							.then(playlists => {
+								next(null, playlists);
+							})
+							.catch(next);
+					},
+
+					(playlists, next) => {
+						this.setStage(4);
+
+						if (!playlists) return next();
+
+						const playlistIds = Object.keys(playlists);
+
+						return async.each(
+							playlistIds,
+							(playlistId, next) => {
+								playlistModel.findOne({ _id: playlistId }, (err, playlist) => {
+									if (err) next(err);
+									else if (!playlist) {
+										this.cache
+											.runJob("HDEL", {
+												table: "playlists",
+												key: playlistId
+											})
+											.then(() => next())
+											.catch(next);
+									} else next();
+								});
+							},
+							next
+						);
+					},
+
+					next => {
+						this.setStage(5);
+						playlistModel.find({}, next);
+					},
+
+					(playlists, next) => {
+						this.setStage(6);
+						async.each(
+							playlists,
+							(playlist, cb) => {
+								this.cache
+									.runJob("HSET", {
+										table: "playlists",
+										key: playlist._id,
+										value: playlistSchema(playlist)
+									})
+									.then(() => cb())
+									.catch(next);
+							},
+							next
+						);
+					}
+				],
+				async err => {
+					if (err) {
+						const formattedErr = await this.utils.runJob("GET_ERROR", {
+							error: err
+						});
+						reject(new Error(formattedErr));
+					} else resolve();
+				}
+			)
+		);
+	}
+
+	/**
+	 * Gets a playlist by id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.playlistId - the id of the playlist we are trying to get
+	 * @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(
+				[
+					next => {
+						this.cache
+							.runJob("HGETALL", { table: "playlists" })
+							.then(playlists => {
+								next(null, playlists);
+							})
+							.catch(next);
+					},
+
+					(playlists, next) => {
+						if (!playlists) return next();
+
+						const playlistIds = Object.keys(playlists);
+
+						return async.each(
+							playlistIds,
+							(playlistId, next) => {
+								playlistModel.findOne({ _id: playlistId }, (err, playlist) => {
+									if (err) next(err);
+									else if (!playlist) {
+										this.cache
+											.runJob("HDEL", {
+												table: "playlists",
+												key: playlistId
+											})
+											.then(() => next())
+											.catch(next);
+									} else next();
+								});
+							},
+							next
+						);
+					},
+
+					next => {
+						this.cache
+							.runJob("HGET", {
+								table: "playlists",
+								key: payload.playlistId
+							})
+							.then(playlist => next(null, playlist))
+							.catch(next);
+					},
+
+					(playlist, next) => {
+						if (playlist) return next(true, playlist);
+						return playlistModel.findOne({ _id: payload.playlistId }, next);
+					},
+
+					(playlist, next) => {
+						if (playlist) {
+							this.cache
+								.runJob("HSET", {
+									table: "playlists",
+									key: payload.playlistId,
+									value: playlist
+								})
+								.then(playlist => {
+									next(null, playlist);
+								})
+								.catch(next);
+						} else next("Playlist not found");
+					}
+				],
+				(err, playlist) => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve(playlist);
+				}
+			);
+		});
+	}
+
+	/**
+	 * Gets a playlist from id from Mongo and updates the cache with it
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.playlistId - the id of the playlist we are trying to update
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	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(
+				[
+					next => {
+						playlistModel.findOne({ _id: payload.playlistId }, next);
+					},
+
+					(playlist, next) => {
+						if (!playlist) {
+							this.cache.runJob("HDEL", {
+								table: "playlists",
+								key: payload.playlistId
+							});
+
+							return next("Playlist not found");
+						}
+
+						return this.cache
+							.runJob("HSET", {
+								table: "playlists",
+								key: payload.playlistId,
+								value: playlist
+							})
+							.then(playlist => {
+								next(null, playlist);
+							})
+							.catch(next);
+					}
+				],
+				(err, playlist) => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve(playlist);
+				}
+			);
+		});
+	}
+
+	/**
+	 * Deletes playlist from id from Mongo and cache
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.playlistId - the id of the playlist we are trying to delete
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	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(
+				[
+					next => {
+						playlistModel.deleteOne({ _id: payload.playlistId }, next);
+					},
+
+					(res, next) => {
+						this.cache
+							.runJob("HDEL", {
+								table: "playlists",
+								key: payload.playlistId
+							})
+							.then(() => next())
+							.catch(next);
+					}
+				],
+				err => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve();
+				}
+			);
+		});
+	}
 }
 
-module.exports = new ExampleModule();
+export default new PlaylistsModule();

+ 297 - 303
backend/logic/punishments.js

@@ -1,331 +1,325 @@
-const CoreClass = require("../core.js");
-
-const async = require("async");
-const mongoose = require("mongoose");
+import async from "async";
+import mongoose from "mongoose";
+import CoreClass from "../core";
 
 class PunishmentsModule extends CoreClass {
-    constructor() {
-        super("punishments");
-    }
+	constructor() {
+		super("punishments");
+	}
+
+	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;
+
+		const punishmentModel = await this.db.runJob("GET_MODEL", { modelName: "punishment" });
+		const punishmentSchema = await this.db.runJob("GET_SCHEMA", { schemaName: "punishment" });
+
+		return new Promise((resolve, reject) =>
+			async.waterfall(
+				[
+					next => {
+						this.setStage(2);
+						this.cache
+							.runJob("HGETALL", { table: "punishments" })
+							.then(punishments => {
+								next(null, punishments);
+							})
+							.catch(next);
+					},
+
+					(punishments, next) => {
+						this.setStage(3);
 
-    initialize() {
-        return new Promise(async (resolve, reject) => {
-            this.setStage(1);
+						if (!punishments) return next();
 
-            this.cache = this.moduleManager.modules["cache"];
-            this.db = this.moduleManager.modules["db"];
-            this.io = this.moduleManager.modules["io"];
-            this.utils = this.moduleManager.modules["utils"];
+						const punishmentIds = Object.keys(punishments);
 
-            const punishmentModel = await this.db.runJob("GET_MODEL", {
-                modelName: "punishment",
-            });
+						return async.each(
+							punishmentIds,
+							(punishmentId, cb) => {
+								punishmentModel.findOne({ _id: punishmentId }, (err, punishment) => {
+									if (err) next(err);
+									else if (!punishment)
+										this.cache
+											.runJob("HDEL", {
+												table: "punishments",
+												key: punishmentId
+											})
+											.then(() => {
+												cb();
+											})
+											.catch(next);
+									else cb();
+								});
+							},
+							next
+						);
+					},
 
-            const punishmentSchema = await this.cache.runJob("GET_SCHEMA", {
-                schemaName: "punishment",
-            });
+					next => {
+						this.setStage(4);
+						punishmentModel.find({}, next);
+					},
 
-            async.waterfall(
-                [
-                    (next) => {
-                        this.setStage(2);
-                        this.cache
-                            .runJob("HGETALL", { table: "punishments" })
-                            .then((punishments) => {
-                                next(null, punishments);
-                            })
-                            .catch(next);
-                    },
+					(punishments, next) => {
+						this.setStage(5);
+						async.each(
+							punishments,
+							(punishment, next) => {
+								if (punishment.active === false || punishment.expiresAt < Date.now()) return next();
 
-                    (punishments, next) => {
-                        this.setStage(3);
-                        if (!punishments) return next();
-                        let punishmentIds = Object.keys(punishments);
-                        async.each(
-                            punishmentIds,
-                            (punishmentId, next) => {
-                                punishmentModel.findOne(
-                                    { _id: punishmentId },
-                                    (err, punishment) => {
-                                        if (err) next(err);
-                                        else if (!punishment)
-                                            this.cache
-                                                .runJob("HDEL", {
-                                                    table: "punishments",
-                                                    key: punishmentId,
-                                                })
-                                                .then(() => {
-                                                    next();
-                                                })
-                                                .catch(next);
-                                        else next();
-                                    }
-                                );
-                            },
-                            next
-                        );
-                    },
+								return this.cache
+									.runJob("HSET", {
+										table: "punishments",
+										key: punishment._id,
+										value: punishmentSchema(punishment, punishment._id)
+									})
+									.then(() => next())
+									.catch(next);
+							},
+							next
+						);
+					}
+				],
+				async err => {
+					if (err) {
+						const formattedErr = await this.utils.runJob("GET_ERROR", { error: err });
+						reject(new Error(formattedErr));
+					} else resolve();
+				}
+			)
+		);
+	}
 
-                    (next) => {
-                        this.setStage(4);
-                        punishmentModel.find({}, next);
-                    },
+	/**
+	 * Gets all punishments in the cache that are active, and removes those that have expired
+	 *
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	GET_PUNISHMENTS() {
+		return new Promise((resolve, reject) => {
+			const punishmentsToRemove = [];
+			async.waterfall(
+				[
+					next => {
+						this.cache
+							.runJob("HGETALL", { table: "punishments" })
+							.then(punishmentsObj => next(null, punishmentsObj))
+							.catch(next);
+					},
 
-                    (punishments, next) => {
-                        this.setStage(5);
-                        async.each(
-                            punishments,
-                            (punishment, next) => {
-                                if (
-                                    punishment.active === false ||
-                                    punishment.expiresAt < Date.now()
-                                )
-                                    return next();
-                                this.cache
-                                    .runJob("HSET", {
-                                        table: "punishments",
-                                        key: punishment._id,
-                                        value: punishmentSchema(
-                                            punishment,
-                                            punishment._id
-                                        ),
-                                    })
-                                    .then(() => {
-                                        next();
-                                    })
-                                    .catch(next);
-                            },
-                            next
-                        );
-                    },
-                ],
-                async (err) => {
-                    if (err) {
-                        err = await utils.runJob("GET_ERROR", { error: err });
-                        reject(new Error(err));
-                    } else {
-                        resolve();
-                    }
-                }
-            );
-        });
-    }
+					(punishments, next) => {
+						let filteredPunishments = [];
 
-    /**
-     * Gets all punishments in the cache that are active, and removes those that have expired
-     *
-     * @param {Function} cb - gets called once we're done initializing
-     */
-    GET_PUNISHMENTS() {
-        //cb
-        return new Promise((resolve, reject) => {
-            let punishmentsToRemove = [];
-            async.waterfall(
-                [
-                    (next) => {
-                        this.cache
-                            .runJob("HGETALL", { table: "punishments" })
-                            .then((punishmentsObj) =>
-                                next(null, punishmentsObj)
-                            )
-                            .catch(next);
-                    },
+						for (
+							let id = 0, punishmentKeys = Object.keys(punishments);
+							id < punishmentKeys.length;
+							id += 1
+						) {
+							const punishment = punishments[id];
+							punishment.punishmentId = id;
+							punishments.push(punishment);
+						}
 
-                    (punishmentsObj, next) => {
-                        let punishments = [];
-                        for (let id in punishmentsObj) {
-                            let obj = punishmentsObj[id];
-                            obj.punishmentId = id;
-                            punishments.push(obj);
-                        }
-                        punishments = punishments.filter((punishment) => {
-                            if (punishment.expiresAt < Date.now())
-                                punishmentsToRemove.push(punishment);
-                            return punishment.expiresAt > Date.now();
-                        });
-                        next(null, punishments);
-                    },
+						filteredPunishments = punishments.filter(punishment => {
+							if (punishment.expiresAt < Date.now()) punishmentsToRemove.push(punishment);
+							return punishment.expiresAt > Date.now();
+						});
 
-                    (punishments, next) => {
-                        async.each(
-                            punishmentsToRemove,
-                            (punishment, next2) => {
-                                this.cache
-                                    .runJob("HDEL", {
-                                        table: "punishments",
-                                        key: punishment.punishmentId,
-                                    })
-                                    .finally(() => {
-                                        next2()
-                                    });
-                            },
-                            () => {
-                                next(null, punishments);
-                            }
-                        );
-                    },
-                ],
-                (err, punishments) => {
-                    if (err && err !== true) return reject(new Error(err));
+						next(null, filteredPunishments);
+					},
 
-                    resolve(punishments);
-                }
-            );
-        });
-    }
+					(punishments, next) => {
+						async.each(
+							punishmentsToRemove,
+							(punishment, next2) => {
+								this.cache
+									.runJob("HDEL", {
+										table: "punishments",
+										key: punishment.punishmentId
+									})
+									.finally(() => next2());
+							},
+							() => {
+								next(null, punishments);
+							}
+						);
+					}
+				],
+				(err, punishments) => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve(punishments);
+				}
+			);
+		});
+	}
 
-    /**
-     * Gets a punishment by id
-     *
-     * @param {String} id - the id of the punishment we are trying to get
-     * @param {Function} cb - gets called once we're done initializing
-     */
-    GET_PUNISHMENT() {
-        //id, cb
-        return new Promise(async (resolve, reject) => {
-            const punishmentModel = await db.runJob("GET_MODEL", {
-                modelName: "punishment",
-            });
+	/**
+	 * Gets a punishment by id
+	 *
+	 * @param {object} payload - object containing the payload
+	 * @param {string} payload.id - the id of the punishment we are trying to get
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	GET_PUNISHMENT(payload) {
+		// id, cb
+		return new Promise((resolve, reject) => {
+			let punishmentModel;
 
-            async.waterfall(
-                [
-                    (next) => {
-                        if (!mongoose.Types.ObjectId.isValid(payload.id))
-                            return next("Id is not a valid ObjectId.");
-                        this.cache
-                            .runJob("HGET", {
-                                table: "punishments",
-                                key: payload.id,
-                            })
-                            .then((punishment) => {
-                                next(null, punishment);
-                            })
-                            .catch(next);
-                    },
+			this.db
+				.runJob("GET_MODEL", { modelName: "punishment" })
+				.then(model => {
+					punishmentModel = model;
+				})
+				.catch(console.error);
 
-                    (punishment, next) => {
-                        if (punishment) return next(true, punishment);
-                        punishmentModel.findOne({ _id: payload.id }, next);
-                    },
+			return async.waterfall(
+				[
+					next => {
+						if (!mongoose.Types.ObjectId.isValid(payload.id)) return next("Id is not a valid ObjectId.");
+						return this.cache
+							.runJob("HGET", {
+								table: "punishments",
+								key: payload.id
+							})
+							.then(punishment => next(null, punishment))
+							.catch(next);
+					},
 
-                    (punishment, next) => {
-                        if (punishment) {
-                            this.cache
-                                .runJob("HSET", {
-                                    table: "punishments",
-                                    key: payload.id,
-                                    value: punishment,
-                                })
-                                .then((punishment) => {
-                                    next(null, punishment);
-                                })
-                                .catch(next);
-                        } else next("Punishment not found.");
-                    },
-                ],
-                (err, punishment) => {
-                    if (err && err !== true) return reject(new Error(err));
+					(punishment, next) => {
+						if (punishment) return next(true, punishment);
+						return punishmentModel.findOne({ _id: payload.id }, next);
+					},
 
-                    resolve(punishment);
-                }
-            );
-        });
-    }
+					(punishment, next) => {
+						if (punishment) {
+							this.cache
+								.runJob("HSET", {
+									table: "punishments",
+									key: payload.id,
+									value: punishment
+								})
+								.then(punishment => {
+									next(null, punishment);
+								})
+								.catch(next);
+						} else next("Punishment not found.");
+					}
+				],
+				(err, punishment) => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve(punishment);
+				}
+			);
+		});
+	}
 
-    /**
-     * Gets all punishments from a userId
-     *
-     * @param {String} userId - the userId of the punishment(s) we are trying to get
-     * @param {Function} cb - gets called once we're done initializing
-     */
-    GET_PUNISHMENTS_FROM_USER_ID(payload) {
-        //userId, cb
-        return new Promise((resolve, reject) => {
-            async.waterfall(
-                [
-                    (next) => {
-                        this.runJob("GET_PUNISHMENTS", {})
-                            .then((punishments) => {
-                                next(null, punishments);
-                            })
-                            .catch(next);
-                    },
-                    (punishments, next) => {
-                        punishments = punishments.filter((punishment) => {
-                            return (
-                                punishment.type === "banUserId" &&
-                                punishment.value === payload.userId
-                            );
-                        });
-                        next(null, punishments);
-                    },
-                ],
-                (err, punishments) => {
-                    if (err && err !== true) return reject(new Error(err));
+	/**
+	 * Gets all punishments from a userId
+	 *
+	 * @param {object} payload - object containing the payload
+	 * @param {string} payload.userId - the userId of the punishment(s) we are trying to get
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	GET_PUNISHMENTS_FROM_USER_ID(payload) {
+		// userId, cb
+		return new Promise((resolve, reject) => {
+			async.waterfall(
+				[
+					next => {
+						this.runJob("GET_PUNISHMENTS", {})
+							.then(punishments => {
+								next(null, punishments);
+							})
+							.catch(next);
+					},
+					(punishments, next) => {
+						const filteredPunishments = punishments.filter(
+							punishment => punishment.type === "banUserId" && punishment.value === payload.userId
+						);
+						next(null, filteredPunishments);
+					}
+				],
+				(err, punishments) => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve(punishments);
+				}
+			);
+		});
+	}
 
-                    resolve(punishments);
-                }
-            );
-        });
-    }
+	/**
+	 * Gets all punishments from a userId
+	 *
+	 * @param {object} payload - object containing the payload
+	 * @param {string} payload.userId - the userId of the punishment(s) we are trying to get
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	ADD_PUNISHMENT(payload) {
+		// type, value, reason, expiresAt, punishedBy, cb
+		return new Promise((resolve, reject) => {
+			let PunishmentModel;
+			let PunishmentSchema;
 
-    ADD_PUNISHMENT(payload) {
-        //type, value, reason, expiresAt, punishedBy, cb
-        return new Promise(async (resolve, reject) => {
-            const punishmentModel = await db.runJob("GET_MODEL", {
-                modelName: "punishment",
-            });
+			this.db
+				.runJob("GET_MODEL", { modelName: "punishment" })
+				.then(model => {
+					PunishmentModel = model;
+				})
+				.catch(console.error);
 
-            const punishmentSchema = await cache.runJob("GET_SCHEMA", {
-                schemaName: "punishment",
-            });
+			this.db
+				.runJob("GET_SCHEMA", { schemaName: "punishment" })
+				.then(model => {
+					PunishmentSchema = model;
+				})
+				.catch(console.error);
 
-            async.waterfall(
-                [
-                    (next) => {
-                        const punishment = new punishmentModel({
-                            type: payload.type,
-                            value: payload.value,
-                            reason: payload.reason,
-                            active: true,
-                            expiresAt: payload.expiresAt,
-                            punishedAt: Date.now(),
-                            punishedBy: payload.punishedBy,
-                        });
-                        punishment.save((err, punishment) => {
-                            if (err) return next(err);
-                            next(null, punishment);
-                        });
-                    },
+			return async.waterfall(
+				[
+					next => {
+						const punishment = new PunishmentModel({
+							type: payload.type,
+							value: payload.value,
+							reason: payload.reason,
+							active: true,
+							expiresAt: payload.expiresAt,
+							punishedAt: Date.now(),
+							punishedBy: payload.punishedBy
+						});
+						punishment.save((err, punishment) => {
+							if (err) return next(err);
+							return next(null, punishment);
+						});
+					},
 
-                    (punishment, next) => {
-                        this.cache
-                            .runJob("HSET", {
-                                table: "punishments",
-                                key: punishment._id,
-                                value: punishmentSchema(
-                                    punishment,
-                                    punishment._id
-                                ),
-                            })
-                            .then(() => {
-                                next();
-                            })
-                            .catch(next);
-                    },
+					(punishment, next) => {
+						this.cache
+							.runJob("HSET", {
+								table: "punishments",
+								key: punishment._id,
+								value: PunishmentSchema(punishment, punishment._id)
+							})
+							.then(() => next())
+							.catch(next);
+					},
 
-                    (punishment, next) => {
-                        // DISCORD MESSAGE
-                        next(null, punishment);
-                    },
-                ],
-                (err, punishment) => {
-                    if (err) return reject(new Error(err));
-                    resolve(punishment);
-                }
-            );
-        });
-    }
+					(punishment, next) => {
+						// DISCORD MESSAGE
+						next(null, punishment);
+					}
+				],
+				(err, punishment) => {
+					if (err) return reject(new Error(err));
+					return resolve(punishment);
+				}
+			);
+		});
+	}
 }
 
-module.exports = new PunishmentsModule();
+export default new PunishmentsModule();

+ 256 - 248
backend/logic/songs.js

@@ -1,273 +1,281 @@
-const CoreClass = require("../core.js");
-
-const async = require("async");
-const mongoose = require("mongoose");
+import async from "async";
+import mongoose from "mongoose";
+import CoreClass from "../core";
 
 class SongsModule extends CoreClass {
-    constructor() {
-        super("songs");
-    }
+	constructor() {
+		super("songs");
+	}
+
+	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;
+
+		const songModel = await this.db.runJob("GET_MODEL", { modelName: "song" });
+		const songSchema = await this.db.runJob("GET_SCHEMA", { schemaName: "song" });
+
+		this.setStage(2);
+
+		return new Promise((resolve, reject) =>
+			async.waterfall(
+				[
+					next => {
+						this.setStage(2);
+						this.cache
+							.runJob("HGETALL", { table: "songs" })
+							.then(songs => {
+								next(null, songs);
+							})
+							.catch(next);
+					},
+
+					(songs, next) => {
+						this.setStage(3);
+
+						if (!songs) return next();
 
-    initialize() {
-        return new Promise(async (resolve, reject) => {
-            this.setStage(1);
+						const songIds = Object.keys(songs);
 
-            this.cache = this.moduleManager.modules["cache"];
-            this.db = this.moduleManager.modules["db"];
-            this.io = this.moduleManager.modules["io"];
-            this.utils = this.moduleManager.modules["utils"];
+						return async.each(
+							songIds,
+							(songId, next) => {
+								songModel.findOne({ songId }, (err, song) => {
+									if (err) next(err);
+									else if (!song)
+										this.cache
+											.runJob("HDEL", {
+												table: "songs",
+												key: songId
+											})
+											.then(() => next())
+											.catch(next);
+									else next();
+								});
+							},
+							next
+						);
+					},
 
-            const songModel = await this.db.runJob("GET_MODEL", {
-                modelName: "song",
-            });
+					next => {
+						this.setStage(4);
+						songModel.find({}, next);
+					},
 
-            const songSchema = await this.cache.runJob("GET_SCHEMA", {
-                schemaName: "song",
-            });
+					(songs, next) => {
+						this.setStage(5);
+						async.each(
+							songs,
+							(song, next) => {
+								this.cache
+									.runJob("HSET", {
+										table: "songs",
+										key: song.songId,
+										value: songSchema(song)
+									})
+									.then(() => next())
+									.catch(next);
+							},
+							next
+						);
+					}
+				],
+				async err => {
+					if (err) {
+						err = await this.utils.runJob("GET_ERROR", { error: err });
+						reject(new Error(err));
+					} else resolve();
+				}
+			)
+		);
+	}
 
-            async.waterfall(
-                [
-                    (next) => {
-                        this.setStage(2);
-                        this.cache
-                            .runJob("HGETALL", { table: "songs" })
-                            .then((songs) => {
-                                next(null, songs);
-                            })
-                            .catch(next);
-                    },
+	/**
+	 * Gets a song by id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
+	 *
+	 * @param {object} payload - object containing the payload
+	 * @param {string} payload.id - the id of the song we are trying to get
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	GET_SONG(payload) {
+		return new Promise((resolve, reject) => {
+			let songModel;
 
-                    (songs, next) => {
-                        this.setStage(3);
-                        if (!songs) return next();
-                        let songIds = Object.keys(songs);
-                        async.each(
-                            songIds,
-                            (songId, next) => {
-                                songModel.findOne({ songId }, (err, song) => {
-                                    if (err) next(err);
-                                    else if (!song)
-                                        this.cache
-                                            .runJob("HDEL", {
-                                                table: "songs",
-                                                key: songId,
-                                            })
-                                            .then(() => {
-                                                next();
-                                            })
-                                            .catch(next);
-                                    else next();
-                                });
-                            },
-                            next
-                        );
-                    },
+			this.db
+				.runJob("GET_MODEL", { modelName: "song" })
+				.then(model => {
+					songModel = model;
+				})
+				.catch(console.error);
 
-                    (next) => {
-                        this.setStage(4);
-                        songModel.find({}, next);
-                    },
+			return 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 })
+							.then(song => {
+								next(null, song);
+							})
+							.catch(next);
+					},
 
-                    (songs, next) => {
-                        this.setStage(5);
-                        async.each(
-                            songs,
-                            (song, next) => {
-                                this.cache
-                                    .runJob("HSET", {
-                                        table: "songs",
-                                        key: song.songId,
-                                        value: songSchema(song),
-                                    })
-                                    .then(() => {
-                                        next();
-                                    })
-                                    .catch(next);
-                            },
-                            next
-                        );
-                    },
-                ],
-                async (err) => {
-                    if (err) {
-                        err = await this.utils.runJob("GET_ERROR", {
-                            error: err,
-                        });
-                        reject(new Error(err));
-                    } else {
-                        resolve();
-                    }
-                }
-            );
-        });
-    }
+					(song, next) => {
+						if (song) return next(true, song);
+						return songModel.findOne({ _id: payload.id }, next);
+					},
 
-    /**
-     * Gets a song by id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
-     *
-     * @param {String} id - the id of the song we are trying to get
-     * @param {Function} cb - gets called once we're done initializing
-     */
-    GET_SONG(payload) {
-        //id, cb
-        return new Promise(async (resolve, reject) => {
-            const songModel = await this.db.runJob("GET_MODEL", {
-                modelName: "song",
-            });
+					(song, next) => {
+						if (song) {
+							this.cache
+								.runJob("HSET", {
+									table: "songs",
+									key: payload.id,
+									value: song
+								})
+								.then(song => next(null, song));
+						} else next("Song not found.");
+					}
+				],
+				(err, song) => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve({ song });
+				}
+			);
+		});
+	}
 
-            async.waterfall(
-                [
-                    (next) => {
-                        if (!mongoose.Types.ObjectId.isValid(payload.id))
-                            return next("Id is not a valid ObjectId.");
-                        this.cache
-                            .runJob("HGET", { table: "songs", key: payload.id })
-                            .then((song) => {
-                                next(null, song);
-                            })
-                            .catch(next);
-                    },
+	/**
+	 * Gets a song by song id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
+	 *
+	 * @param {object} payload - an object containing the payload
+	 * @param {string} payload.songId - the mongo id of the song we are trying to get
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	GET_SONG_FROM_ID(payload) {
+		return new Promise((resolve, reject) => {
+			let songModel;
 
-                    (song, next) => {
-                        if (song) return next(true, song);
-                        songModel.findOne({ _id: payload.id }, next);
-                    },
+			this.db
+				.runJob("GET_MODEL", { modelName: "song" })
+				.then(model => {
+					songModel = model;
+				})
+				.catch(console.error);
 
-                    (song, next) => {
-                        if (song) {
-                            this.cache
-                                .runJob("HSET", {
-                                    table: "songs",
-                                    key: payload.id,
-                                    value: song,
-                                })
-                                .then((song) => {
-                                    next(null, song);
-                                });
-                        } else next("Song not found.");
-                    },
-                ],
-                (err, song) => {
-                    if (err && err !== true) return reject(new Error(err));
+			return async.waterfall(
+				[
+					next => {
+						songModel.findOne({ songId: payload.songId }, next);
+					}
+				],
+				(err, song) => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve({ song });
+				}
+			);
+		});
+	}
 
-                    resolve({ song });
-                }
-            );
-        });
-    }
+	/**
+	 * Gets a song from id from Mongo and updates the cache with it
+	 *
+	 * @param {object} payload - an object containing the payload
+	 * @param {string} payload.songId - the id of the song we are trying to update
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	UPDATE_SONG(payload) {
+		// songId, cb
+		return new Promise((resolve, reject) => {
+			let songModel;
 
-    /**
-     * Gets a song by song id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
-     *
-     * @param {String} songId - the mongo id of the song we are trying to get
-     * @param {Function} cb - gets called once we're done initializing
-     */
-    GET_SONG_FROM_ID(payload) {
-        //songId, cb
-        return new Promise(async (resolve, reject) => {
-            const songModel = await this.db.runJob("GET_MODEL", {
-                modelName: "song",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        songModel.findOne({ songId: payload.songId }, next);
-                    },
-                ],
-                (err, song) => {
-                    if (err && err !== true) return reject(new Error(err));
-                    resolve({ song });
-                }
-            );
-        });
-    }
+			this.db
+				.runJob("GET_MODEL", { modelName: "song" })
+				.then(model => {
+					songModel = model;
+				})
+				.catch(console.error);
 
-    /**
-     * Gets a song from id from Mongo and updates the cache with it
-     *
-     * @param {String} songId - the id of the song we are trying to update
-     * @param {Function} cb - gets called when an error occurred or when the operation was successful
-     */
-    UPDATE_SONG(payload) {
-        //songId, cb
-        return new Promise(async (resolve, reject) => {
-            const songModel = await this.db.runJob("GET_MODEL", {
-                modelName: "song",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        songModel.findOne({ _id: payload.songId }, next);
-                    },
+			return async.waterfall(
+				[
+					next => {
+						songModel.findOne({ _id: payload.songId }, next);
+					},
 
-                    (song, next) => {
-                        if (!song) {
-                            this.cache.runJob("HDEL", {
-                                table: "songs",
-                                key: payload.songId,
-                            });
-                            return next("Song not found.");
-                        }
+					(song, next) => {
+						if (!song) {
+							this.cache.runJob("HDEL", {
+								table: "songs",
+								key: payload.songId
+							});
+							return next("Song not found.");
+						}
 
-                        this.cache
-                            .runJob("HSET", {
-                                table: "songs",
-                                key: payload.songId,
-                                value: song,
-                            })
-                            .then((song) => {
-                                next(null, song);
-                            })
-                            .catch(next);
-                    },
-                ],
-                (err, song) => {
-                    if (err && err !== true) return reject(new Error(err));
+						return this.cache
+							.runJob("HSET", {
+								table: "songs",
+								key: payload.songId,
+								value: song
+							})
+							.then(song => {
+								next(null, song);
+							})
+							.catch(next);
+					}
+				],
+				(err, song) => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve(song);
+				}
+			);
+		});
+	}
 
-                    resolve(song);
-                }
-            );
-        });
-    }
+	/**
+	 * Deletes song from id from Mongo and cache
+	 *
+	 * @param {object} payload - returns an object containing the payload
+	 * @param {string} payload.songId - the id of the song we are trying to delete
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	DELETE_SONG(payload) {
+		// songId, cb
+		return new Promise((resolve, reject) => {
+			let songModel;
 
-    /**
-     * Deletes song from id from Mongo and cache
-     *
-     * @param {String} songId - the id of the song we are trying to delete
-     * @param {Function} cb - gets called when an error occurred or when the operation was successful
-     */
-    DELETE_SONG(payload) {
-        //songId, cb
-        return new Promise(async (resolve, reject) => {
-            const songModel = await this.db.runJob("GET_MODEL", {
-                modelName: "song",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        songModel.deleteOne({ songId: payload.songId }, next);
-                    },
+			this.db
+				.runJob("GET_MODEL", { modelName: "song" })
+				.then(model => {
+					songModel = model;
+				})
+				.catch(console.error);
 
-                    (next) => {
-                        this.cache
-                            .runJob("HDEL", {
-                                table: "songs",
-                                key: payload.songId,
-                            })
-                            .then(() => {
-                                next();
-                            })
-                            .catch(next);
-                    },
-                ],
-                (err) => {
-                    if (err && err !== true) return reject(new Error(err));
+			return async.waterfall(
+				[
+					next => {
+						songModel.deleteOne({ songId: payload.songId }, next);
+					},
 
-                    resolve();
-                }
-            );
-        });
-    }
+					next => {
+						this.cache
+							.runJob("HDEL", {
+								table: "songs",
+								key: payload.songId
+							})
+							.then(() => next())
+							.catch(next);
+					}
+				],
+				err => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve();
+				}
+			);
+		});
+	}
 }
 
-module.exports = new SongsModule();
+export default new SongsModule();

+ 85 - 105
backend/logic/spotify.js

@@ -1,120 +1,100 @@
-const CoreClass = require("../core.js");
+import config from "config";
+import async from "async";
+import { OAuth2 } from "oauth";
 
-const config = require("config"),
-    async = require("async");
+import CoreClass from "../core";
 
 let apiResults = {
-    access_token: "",
-    token_type: "",
-    expires_in: 0,
-    expires_at: 0,
-    scope: "",
+	access_token: "",
+	token_type: "",
+	expires_in: 0,
+	expires_at: 0,
+	scope: ""
 };
 
 class SpotifyModule extends CoreClass {
-    constructor() {
-        super("spotify");
-    }
+	constructor() {
+		super("spotify");
+	}
 
-    initialize() {
-        return new Promise((resolve, reject) => {
-            this.cache = this.moduleManager.modules["cache"];
-            this.utils = this.moduleManager.modules["utils"];
+	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");
+			const client = config.get("apis.spotify.client");
+			const secret = config.get("apis.spotify.secret");
 
-            const OAuth2 = require("oauth").OAuth2;
-            this.SpotifyOauth = new OAuth2(
-                client,
-                secret,
-                "https://accounts.spotify.com/",
-                null,
-                "api/token",
-                null
-            );
+			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);
-                    },
+			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();
-                    }
-                }
-            );
-        });
-    }
+					(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();
+					}
+				}
+			);
+		});
+	}
 
-    GET_TOKEN(payload) {
-        return new Promise((resolve, reject) => {
-            if (Date.now() > apiResults.expires_at) {
-                this.runJob("REQUEST_TOKEN").then(() => {
-                    resolve(apiResults.access_token);
-                });
-            } else resolve(apiResults.access_token);
-        });
-    }
+	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);
+		});
+	}
 
-    REQUEST_TOKEN(payload) {
-        //cb
-        return new Promise((resolve, reject) => {
-            async.waterfall(
-                [
-                    (next) => {
-                        this.log(
-                            "INFO",
-                            "SPOTIFY_REQUEST_TOKEN",
-                            "Requesting new Spotify token."
-                        );
-                        this.SpotifyOauth.getOAuthAccessToken(
-                            "",
-                            { grant_type: "client_credentials" },
-                            next
-                        );
-                    },
-                    (access_token, refresh_token, 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();
-                }
-            );
-        });
-    }
+	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()
+			);
+		});
+	}
 }
 
-module.exports = new SpotifyModule();
+export default new SpotifyModule();

+ 967 - 1192
backend/logic/stations.js

@@ -1,1197 +1,972 @@
-const CoreClass = require("../core.js");
+import async from "async";
 
-const async = require("async");
-
-let subscription = null;
+import CoreClass from "../core";
 
 class StationsModule extends CoreClass {
-    constructor() {
-        super("stations");
-    }
-
-    initialize() {
-        return new Promise(async (resolve, reject) => {
-            this.cache = this.moduleManager.modules["cache"];
-            this.db = this.moduleManager.modules["db"];
-            this.utils = this.moduleManager.modules["utils"];
-            this.songs = this.moduleManager.modules["songs"];
-            this.notifications = this.moduleManager.modules["notifications"];
-
-            this.defaultSong = {
-                songId: "60ItHLz5WEA",
-                title: "Faded - Alan Walker",
-                duration: 212,
-                skipDuration: 0,
-                likes: -1,
-                dislikes: -1,
-            };
-
-            //TEMP
-            this.cache.runJob("SUB", {
-                channel: "station.pause",
-                cb: async (stationId) => {
-                    this.notifications
-                        .runJob("REMOVE", {
-                            subscription: `stations.nextSong?id=${stationId}`,
-                        })
-                        .then();
-                },
-            });
-
-            this.cache.runJob("SUB", {
-                channel: "station.resume",
-                cb: async (stationId) => {
-                    this.runJob("INITIALIZE_STATION", { stationId }).then();
-                },
-            });
-
-            this.cache.runJob("SUB", {
-                channel: "station.queueUpdate",
-                cb: async (stationId) => {
-                    this.runJob("GET_STATION", { stationId }).then(
-                        (station) => {
-                            if (
-                                !station.currentSong &&
-                                station.queue.length > 0
-                            ) {
-                                this.runJob("INITIALIZE_STATION", {
-                                    stationId,
-                                }).then();
-                            }
-                        }
-                    );
-                },
-            });
-
-            this.cache.runJob("SUB", {
-                channel: "station.newOfficialPlaylist",
-                cb: async (stationId) => {
-                    this.cache
-                        .runJob("HGET", {
-                            table: "officialPlaylists",
-                            key: stationId,
-                        })
-                        .then((playlistObj) => {
-                            if (playlistObj) {
-                                this.utils.runJob("EMIT_TO_ROOM", {
-                                    room: `station.${stationId}`,
-                                    args: [
-                                        "event:newOfficialPlaylist",
-                                        playlistObj.songs,
-                                    ],
-                                });
-                            }
-                        });
-                },
-            });
-
-            const stationModel = (this.stationModel = await this.db.runJob(
-                "GET_MODEL",
-                {
-                    modelName: "station",
-                }
-            ));
-
-            const stationSchema = (this.stationSchema = await this.cache.runJob(
-                "GET_SCHEMA",
-                {
-                    schemaName: "station",
-                }
-            ));
-
-            async.waterfall(
-                [
-                    (next) => {
-                        this.setStage(2);
-                        this.cache
-                            .runJob("HGETALL", { table: "stations" })
-                            .then((stations) => {
-                                next(null, stations);
-                            })
-                            .catch(next);
-                    },
-
-                    (stations, next) => {
-                        this.setStage(3);
-                        if (!stations) return next();
-                        let stationIds = Object.keys(stations);
-                        async.each(
-                            stationIds,
-                            (stationId, next) => {
-                                stationModel.findOne(
-                                    { _id: stationId },
-                                    (err, station) => {
-                                        if (err) next(err);
-                                        else if (!station) {
-                                            this.cache
-                                                .runJob("HDEL", {
-                                                    table: "stations",
-                                                    key: stationId,
-                                                })
-                                                .then(() => {
-                                                    next();
-                                                })
-                                                .catch(next);
-                                        } else next();
-                                    }
-                                );
-                            },
-                            next
-                        );
-                    },
-
-                    (next) => {
-                        this.setStage(4);
-                        stationModel.find({}, next);
-                    },
-
-                    (stations, next) => {
-                        this.setStage(5);
-                        async.each(
-                            stations,
-                            (station, next2) => {
-                                async.waterfall(
-                                    [
-                                        (next) => {
-                                            this.cache
-                                                .runJob("HSET", {
-                                                    table: "stations",
-                                                    key: station._id,
-                                                    value: stationSchema(
-                                                        station
-                                                    ),
-                                                })
-                                                .then((station) =>
-                                                    next(null, station)
-                                                )
-                                                .catch(next);
-                                        },
-
-                                        (station, next) => {
-                                            this.runJob(
-                                                "INITIALIZE_STATION",
-                                                {
-                                                    stationId: station._id,
-                                                    bypassQueue: true,
-                                                },
-                                                { bypassQueue: true }
-                                            )
-                                                .then(() => {
-                                                    next();
-                                                })
-                                                .catch(next); // bypassQueue is true because otherwise the module will never initialize
-                                        },
-                                    ],
-                                    (err) => {
-                                        next2(err);
-                                    }
-                                );
-                            },
-                            next
-                        );
-                    },
-                ],
-                async (err) => {
-                    if (err) {
-                        err = await this.utils.runJob("GET_ERROR", {
-                            error: err,
-                        });
-                        reject(new Error(err));
-                    } else {
-                        resolve();
-                    }
-                }
-            );
-        });
-    }
-
-    INITIALIZE_STATION(payload) {
-        //stationId, cb, bypassValidate = false
-        return new Promise((resolve, reject) => {
-            // if (typeof cb !== 'function') cb = ()=>{};
-
-            async.waterfall(
-                [
-                    (next) => {
-                        this.runJob(
-                            "GET_STATION",
-                            {
-                                stationId: payload.stationId,
-                                bypassQueue: payload.bypassQueue,
-                            },
-                            { bypassQueue: payload.bypassQueue }
-                        )
-                            .then((station) => {
-                                next(null, station);
-                            })
-                            .catch(next);
-                    },
-                    (station, next) => {
-                        if (!station) return next("Station not found.");
-                        this.notifications
-                            .runJob("UNSCHEDULE", {
-                                name: `stations.nextSong?id=${station._id}`,
-                            })
-                            .then()
-                            .catch();
-                        this.notifications
-                            .runJob("SUBSCRIBE", {
-                                name: `stations.nextSong?id=${station._id}`,
-                                cb: () =>
-                                    this.runJob("SKIP_STATION", {
-                                        stationId: station._id,
-                                    }),
-                                unique: true,
-                                station,
-                            })
-                            .then()
-                            .catch();
-                        if (station.paused) return next(true, station);
-                        next(null, station);
-                    },
-                    (station, next) => {
-                        if (!station.currentSong) {
-                            return this.runJob(
-                                "SKIP_STATION",
-                                {
-                                    stationId: station._id,
-                                    bypassQueue: payload.bypassQueue,
-                                },
-                                { bypassQueue: payload.bypassQueue }
-                            )
-                                .then((station) => {
-                                    next(true, station);
-                                })
-                                .catch(next)
-                                .finally(() => {});
-                        }
-                        let timeLeft =
-                            station.currentSong.duration * 1000 -
-                            (Date.now() -
-                                station.startedAt -
-                                station.timePaused);
-                        if (isNaN(timeLeft)) timeLeft = -1;
-                        if (
-                            station.currentSong.duration * 1000 < timeLeft ||
-                            timeLeft < 0
-                        ) {
-                            this.runJob(
-                                "SKIP_STATION",
-                                {
-                                    stationId: station._id,
-                                    bypassQueue: payload.bypassQueue,
-                                },
-                                { bypassQueue: payload.bypassQueue }
-                            )
-                                .then((station) => {
-                                    next(null, station);
-                                })
-                                .catch(next);
-                        } else {
-                            //name, time, cb, station
-                            this.notifications.runJob("SCHEDULE", {
-                                name: `stations.nextSong?id=${station._id}`,
-                                time: timeLeft,
-                                station,
-                            });
-                            next(null, station);
-                        }
-                    },
-                ],
-                async (err, station) => {
-                    if (err && err !== true) {
-                        err = await this.utils.runJob("GET_ERROR", {
-                            error: err,
-                        });
-                        reject(new Error(err));
-                    } else resolve(station);
-                }
-            );
-        });
-    }
-
-    CALCULATE_SONG_FOR_STATION(payload) {
-        //station, cb, bypassValidate = false
-        return new Promise(async (resolve, reject) => {
-            const songModel = await this.db.runJob("GET_MODEL", {
-                modelName: "song",
-            });
-            const stationModel = await this.db.runJob("GET_MODEL", {
-                modelName: "station",
-            });
-
-            let songList = [];
-            async.waterfall(
-                [
-                    (next) => {
-                        if (payload.station.genres.length === 0) return next();
-                        let genresDone = [];
-                        payload.station.genres.forEach((genre) => {
-                            songModel.find({ genres: genre }, (err, songs) => {
-                                if (!err) {
-                                    songs.forEach((song) => {
-                                        if (songList.indexOf(song._id) === -1) {
-                                            let found = false;
-                                            song.genres.forEach((songGenre) => {
-                                                if (
-                                                    payload.station.blacklistedGenres.indexOf(
-                                                        songGenre
-                                                    ) !== -1
-                                                )
-                                                    found = true;
-                                            });
-                                            if (!found) {
-                                                songList.push(song._id);
-                                            }
-                                        }
-                                    });
-                                }
-                                genresDone.push(genre);
-                                if (
-                                    genresDone.length ===
-                                    payload.station.genres.length
-                                )
-                                    next();
-                            });
-                        });
-                    },
-
-                    (next) => {
-                        let playlist = [];
-                        songList.forEach(function(songId) {
-                            if (payload.station.playlist.indexOf(songId) === -1)
-                                playlist.push(songId);
-                        });
-                        payload.station.playlist.filter((songId) => {
-                            if (songList.indexOf(songId) !== -1)
-                                playlist.push(songId);
-                        });
-
-                        this.utils
-                            .runJob("SHUFFLE", { array: playlist })
-                            .then((result) => {
-                                next(null, result.array);
-                            })
-                            .catch(next);
-                    },
-
-                    (playlist, next) => {
-                        this.runJob(
-                            "CALCULATE_OFFICIAL_PLAYLIST_LIST",
-                            {
-                                stationId: payload.station._id,
-                                songList: playlist,
-                                bypassQueue: payload.bypassQueue,
-                            },
-                            { bypassQueue: payload.bypassQueue }
-                        )
-                            .then(() => {
-                                next(null, playlist);
-                            })
-                            .catch(next);
-                    },
-
-                    (playlist, next) => {
-                        stationModel.updateOne(
-                            { _id: payload.station._id },
-                            { $set: { playlist: playlist } },
-                            { runValidators: true },
-                            (err) => {
-                                this.runJob(
-                                    "UPDATE_STATION",
-                                    {
-                                        stationId: payload.station._id,
-                                        bypassQueue: payload.bypassQueue,
-                                    },
-                                    { bypassQueue: payload.bypassQueue }
-                                )
-                                    .then(() => {
-                                        next(null, playlist);
-                                    })
-                                    .catch(next);
-                            }
-                        );
-                    },
-                ],
-                (err, newPlaylist) => {
-                    if (err) return reject(new Error(err));
-                    resolve(newPlaylist);
-                }
-            );
-        });
-    }
-
-    // Attempts to get the station from Redis. If it's not in Redis, get it from Mongo and add it to Redis.
-    GET_STATION(payload) {
-        //stationId, cb, bypassValidate = false
-        return new Promise((resolve, reject) => {
-            async.waterfall(
-                [
-                    (next) => {
-                        this.cache
-                            .runJob("HGET", {
-                                table: "stations",
-                                key: payload.stationId,
-                            })
-                            .then((station) => {
-                                next(null, station);
-                            })
-                            .catch(next);
-                    },
-
-                    (station, next) => {
-                        if (station) return next(true, station);
-                        this.stationModel.findOne(
-                            { _id: payload.stationId },
-                            next
-                        );
-                    },
-
-                    (station, next) => {
-                        if (station) {
-                            if (station.type === "official") {
-                                this.runJob(
-                                    "CALCULATE_OFFICIAL_PLAYLIST_LIST",
-                                    {
-                                        stationId: station._id,
-                                        songList: station.playlist,
-                                    }
-                                )
-                                    .then()
-                                    .catch();
-                            }
-                            station = this.stationSchema(station);
-                            this.cache
-                                .runJob("HSET", {
-                                    table: "stations",
-                                    key: payload.stationId,
-                                    value: station,
-                                })
-                                .then()
-                                .catch();
-                            next(true, station);
-                        } else next("Station not found");
-                    },
-                ],
-                async (err, station) => {
-                    if (err && err !== true) {
-                        err = await this.utils.runJob("GET_ERROR", {
-                            error: err,
-                        });
-                        reject(new Error(err));
-                    } else resolve(station);
-                }
-            );
-        });
-    }
-
-    // Attempts to get the station from Redis. If it's not in Redis, get it from Mongo and add it to Redis.
-    GET_STATION_BY_NAME(payload) {
-        //stationName, cb
-        return new Promise(async (resolve, reject) => {
-            const stationModel = await this.db.runJob("GET_MODEL", {
-                modelName: "station",
-            });
-            async.waterfall(
-                [
-                    (next) => {
-                        stationModel.findOne(
-                            { name: payload.stationName },
-                            next
-                        );
-                    },
-
-                    (station, next) => {
-                        if (station) {
-                            if (station.type === "official") {
-                                this.runJob(
-                                    "CALCULATE_OFFICIAL_PLAYLIST_LIST",
-                                    {
-                                        stationId: station._id,
-                                        songList: station.playlist,
-                                    }
-                                );
-                            }
-                            this.cache
-                                .runJob("GET_SCHEMA", { schemaName: "station" })
-                                .then((stationSchema) => {
-                                    station = stationSchema(station);
-                                    this.cache.runJob("HSET", {
-                                        table: "stations",
-                                        key: station._id,
-                                        value: station,
-                                    });
-                                    next(true, station);
-                                });
-                        } else next("Station not found");
-                    },
-                ],
-                (err, station) => {
-                    if (err && err !== true) return reject(new Error(err));
-                    resolve(station);
-                }
-            );
-        });
-    }
-
-    UPDATE_STATION(payload) {
-        //stationId, cb, bypassValidate = false
-        return new Promise((resolve, reject) => {
-            async.waterfall(
-                [
-                    (next) => {
-                        this.stationModel.findOne(
-                            { _id: payload.stationId },
-                            next
-                        );
-                    },
-
-                    (station, next) => {
-                        if (!station) {
-                            this.cache
-                                .runJob("HDEL", {
-                                    table: "stations",
-                                    key: payload.stationId,
-                                })
-                                .then()
-                                .catch();
-                            return next("Station not found");
-                        }
-                        this.cache
-                            .runJob("HSET", {
-                                table: "stations",
-                                key: payload.stationId,
-                                value: station,
-                            })
-                            .then((station) => {
-                                next(null, station);
-                            })
-                            .catch(next);
-                    },
-                ],
-                async (err, station) => {
-                    if (err && err !== true) {
-                        err = await this.utils.runJob("GET_ERROR", {
-                            error: err,
-                        });
-                        reject(new Error(err));
-                    } else resolve(station);
-                }
-            );
-        });
-    }
-
-    CALCULATE_OFFICIAL_PLAYLIST_LIST(payload) {
-        //stationId, songList, cb, bypassValidate = false
-        return new Promise(async (resolve, reject) => {
-            const officialPlaylistSchema = await this.cache.runJob(
-                "GET_SCHEMA",
-                {
-                    schemaName: "officialPlaylist",
-                }
-            );
-
-            let lessInfoPlaylist = [];
-            async.each(
-                payload.songList,
-                (song, next) => {
-                    this.songs
-                        .runJob("GET_SONG", { id: song })
-                        .then((response) => {
-                            const song = response.song;
-                            if (song) {
-                                let newSong = {
-                                    songId: song.songId,
-                                    title: song.title,
-                                    artists: song.artists,
-                                    duration: song.duration,
-                                };
-                                lessInfoPlaylist.push(newSong);
-                            }
-                        })
-                        .finally(() => {
-                            next();
-                        });
-                },
-                () => {
-                    this.cache
-                        .runJob("HSET", {
-                            table: "officialPlaylists",
-                            key: payload.stationId,
-                            value: officialPlaylistSchema(
-                                payload.stationId,
-                                lessInfoPlaylist
-                            ),
-                        })
-                        .finally(() => {
-                            this.cache.runJob("PUB", {
-                                channel: "station.newOfficialPlaylist",
-                                value: payload.stationId,
-                            });
-                            resolve();
-                        });
-                }
-            );
-        });
-    }
-
-    SKIP_STATION(payload) {
-        //stationId
-        return new Promise((resolve, reject) => {
-            this.log("INFO", `Skipping station ${payload.stationId}.`);
-
-            this.log(
-                "STATION_ISSUE",
-                `SKIP_STATION_CB - Station ID: ${payload.stationId}.`
-            );
-
-            async.waterfall(
-                [
-                    (next) => {
-                        this.runJob(
-                            "GET_STATION",
-                            {
-                                stationId: payload.stationId,
-                                bypassQueue: payload.bypassQueue,
-                            },
-                            { bypassQueue: payload.bypassQueue }
-                        )
-                            .then((station) => {
-                                next(null, station);
-                            })
-                            .catch(() => {});
-                    },
-                    (station, next) => {
-                        if (!station) return next("Station not found.");
-
-                        if (
-                            station.type === "community" &&
-                            station.partyMode &&
-                            station.queue.length === 0
-                        )
-                            return next(null, null, -11, station); // Community station with party mode enabled and no songs in the queue
-
-                        if (
-                            station.type === "community" &&
-                            station.partyMode &&
-                            station.queue.length > 0
-                        ) {
-                            // Community station with party mode enabled and songs in the queue
-                            if (station.paused) {
-                                return next(null, null, -19, station);
-                            } else {
-                                return this.stationModel.updateOne(
-                                    { _id: payload.stationId },
-                                    {
-                                        $pull: {
-                                            queue: {
-                                                _id: station.queue[0]._id,
-                                            },
-                                        },
-                                    },
-                                    (err) => {
-                                        if (err) return next(err);
-                                        next(
-                                            null,
-                                            station.queue[0],
-                                            -12,
-                                            station
-                                        );
-                                    }
-                                );
-                            }
-                        }
-                        if (
-                            station.type === "community" &&
-                            !station.partyMode
-                        ) {
-                            this.db
-                                .runJob("GET_MODEL", { modelName: "playlist" })
-                                .then((playlistModel) => {
-                                    return playlistModel.findOne(
-                                        { _id: station.privatePlaylist },
-                                        (err, playlist) => {
-                                            if (err) return next(err);
-                                            if (!playlist)
-                                                return next(
-                                                    null,
-                                                    null,
-                                                    -13,
-                                                    station
-                                                );
-                                            playlist = playlist.songs;
-                                            if (playlist.length > 0) {
-                                                let currentSongIndex;
-                                                if (
-                                                    station.currentSongIndex <
-                                                    playlist.length - 1
-                                                )
-                                                    currentSongIndex =
-                                                        station.currentSongIndex +
-                                                        1;
-                                                else currentSongIndex = 0;
-                                                let callback = (err, song) => {
-                                                    if (err) return next(err);
-                                                    if (song)
-                                                        return next(
-                                                            null,
-                                                            song,
-                                                            currentSongIndex,
-                                                            station
-                                                        );
-                                                    else {
-                                                        let song =
-                                                            playlist[
-                                                                currentSongIndex
-                                                            ];
-                                                        let currentSong = {
-                                                            songId: song.songId,
-                                                            title: song.title,
-                                                            duration:
-                                                                song.duration,
-                                                            likes: -1,
-                                                            dislikes: -1,
-                                                        };
-                                                        return next(
-                                                            null,
-                                                            currentSong,
-                                                            currentSongIndex,
-                                                            station
-                                                        );
-                                                    }
-                                                };
-                                                if (
-                                                    playlist[currentSongIndex]
-                                                        ._id
-                                                )
-                                                    this.songs
-                                                        .runJob("GET_SONG", {
-                                                            id:
-                                                                playlist[
-                                                                    currentSongIndex
-                                                                ]._id,
-                                                        })
-                                                        .then((response) =>
-                                                            callback(
-                                                                null,
-                                                                response.song
-                                                            )
-                                                        )
-                                                        .catch(callback);
-                                                else
-                                                    this.songs
-                                                        .runJob(
-                                                            "GET_SONG_FROM_ID",
-                                                            {
-                                                                songId:
-                                                                    playlist[
-                                                                        currentSongIndex
-                                                                    ].songId,
-                                                            }
-                                                        )
-                                                        .then((response) =>
-                                                            callback(
-                                                                null,
-                                                                response.song
-                                                            )
-                                                        )
-                                                        .catch(callback);
-                                            } else
-                                                return next(
-                                                    null,
-                                                    null,
-                                                    -14,
-                                                    station
-                                                );
-                                        }
-                                    );
-                                });
-                        }
-                        if (
-                            station.type === "official" &&
-                            station.playlist.length === 0
-                        ) {
-                            return this.runJob(
-                                "CALCULATE_SONG_FOR_STATION",
-                                { station, bypassQueue: payload.bypassQueue },
-                                { bypassQueue: payload.bypassQueue }
-                            )
-                                .then((playlist) => {
-                                    if (playlist.length === 0)
-                                        return next(
-                                            null,
-                                            this.defaultSong,
-                                            0,
-                                            station
-                                        );
-                                    else {
-                                        this.songs
-                                            .runJob("GET_SONG", {
-                                                id: playlist[0],
-                                            })
-                                            .then((response) => {
-                                                next(
-                                                    null,
-                                                    response.song,
-                                                    0,
-                                                    station
-                                                );
-                                            })
-                                            .catch((err) => {
-                                                return next(
-                                                    null,
-                                                    this.defaultSong,
-                                                    0,
-                                                    station
-                                                );
-                                            });
-                                    }
-                                })
-                                .catch(next);
-                        }
-                        if (
-                            station.type === "official" &&
-                            station.playlist.length > 0
-                        ) {
-                            async.doUntil(
-                                (next) => {
-                                    if (
-                                        station.currentSongIndex <
-                                        station.playlist.length - 1
-                                    ) {
-                                        this.songs
-                                            .runJob("GET_SONG", {
-                                                id:
-                                                    station.playlist[
-                                                        station.currentSongIndex +
-                                                            1
-                                                    ],
-                                            })
-                                            .then((response) => {
-                                                return next(
-                                                    null,
-                                                    response.song,
-                                                    station.currentSongIndex + 1
-                                                );
-                                            })
-                                            .catch((err) => {
-                                                station.currentSongIndex++;
-                                                next(null, null, null);
-                                            });
-                                    } else {
-                                        this.runJob(
-                                            "CALCULATE_SONG_FOR_STATION",
-                                            {
-                                                station,
-                                                bypassQueue:
-                                                    payload.bypassQueue,
-                                            },
-                                            { bypassQueue: payload.bypassQueue }
-                                        )
-                                            .then((newPlaylist) => {
-                                                this.songs
-                                                    .runJob("GET_SONG", { id: newPlaylist[0] })
-                                                    .then((response) => {
-                                                        station.playlist = newPlaylist;
-                                                        next(null, response.song, 0);
-                                                    })
-                                                    .catch(err => {
-                                                        return next(
-                                                            null,
-                                                            this.defaultSong,
-                                                            0
-                                                        );
-                                                    });
-                                            })
-                                            .catch((err) => {
-                                                next(null, this.defaultSong, 0);
-                                            });
-                                    }
-                                },
-                                (song, currentSongIndex, next) => {
-                                    if (!!song)
-                                        return next(
-                                            null,
-                                            true,
-                                            currentSongIndex
-                                        );
-                                    else return next(null, false);
-                                },
-                                (err, song, currentSongIndex) => {
-                                    return next(
-                                        err,
-                                        song,
-                                        currentSongIndex,
-                                        station
-                                    );
-                                }
-                            );
-                        }
-                    },
-                    (song, currentSongIndex, station, next) => {
-                        let $set = {};
-                        if (song === null) $set.currentSong = null;
-                        else if (song.likes === -1 && song.dislikes === -1) {
-                            $set.currentSong = {
-                                songId: song.songId,
-                                title: song.title,
-                                duration: song.duration,
-                                skipDuration: 0,
-                                likes: -1,
-                                dislikes: -1,
-                            };
-                        } else {
-                            $set.currentSong = {
-                                songId: song.songId,
-                                title: song.title,
-                                artists: song.artists,
-                                duration: song.duration,
-                                likes: song.likes,
-                                dislikes: song.dislikes,
-                                skipDuration: song.skipDuration,
-                                thumbnail: song.thumbnail,
-                            };
-                        }
-                        if (currentSongIndex >= 0)
-                            $set.currentSongIndex = currentSongIndex;
-                        $set.startedAt = Date.now();
-                        $set.timePaused = 0;
-                        if (station.paused) $set.pausedAt = Date.now();
-                        next(null, $set, station);
-                    },
-
-                    ($set, station, next) => {
-                        this.stationModel.updateOne(
-                            { _id: station._id },
-                            { $set },
-                            (err) => {
-                                this.runJob(
-                                    "UPDATE_STATION",
-                                    {
-                                        stationId: station._id,
-                                        bypassQueue: payload.bypassQueue,
-                                    },
-
-                                    { bypassQueue: payload.bypassQueue }
-                                )
-                                    .then((station) => {
-                                        if (
-                                            station.type === "community" &&
-                                            station.partyMode === true
-                                        )
-                                            this.cache
-                                                .runJob("PUB", {
-                                                    channel:
-                                                        "station.queueUpdate",
-                                                    value: payload.stationId,
-                                                })
-                                                .then()
-                                                .catch();
-                                        next(null, station);
-                                    })
-                                    .catch(next);
-                            }
-                        );
-                    },
-                ],
-                async (err, station) => {
-                    if (err) {
-                        err = await this.utils.runJob("GET_ERROR", {
-                            error: err,
-                        });
-                        this.log(
-                            "ERROR",
-                            `Skipping station "${payload.stationId}" failed. "${err}"`
-                        );
-                        reject(new Error(err));
-                    } else {
-                        if (
-                            station.currentSong !== null &&
-                            station.currentSong.songId !== undefined
-                        ) {
-                            station.currentSong.skipVotes = 0;
-                        }
-                        //TODO Pub/Sub this
-
-                        this.utils
-                            .runJob("EMIT_TO_ROOM", {
-                                room: `station.${station._id}`,
-                                args: [
-                                    "event:songs.next",
-                                    {
-                                        currentSong: station.currentSong,
-                                        startedAt: station.startedAt,
-                                        paused: station.paused,
-                                        timePaused: 0,
-                                    },
-                                ],
-                            })
-                            .then()
-                            .catch();
-
-                        if (station.privacy === "public") {
-                            this.utils
-                                .runJob("EMIT_TO_ROOM", {
-                                    room: "home",
-                                    args: [
-                                        "event:station.nextSong",
-                                        station._id,
-                                        station.currentSong,
-                                    ],
-                                })
-                                .then()
-                                .catch();
-                        } else {
-                            let sockets = await this.utils.runJob(
-                                "GET_ROOM_SOCKETS",
-                                { room: "home" }
-                            );
-                            for (let socketId in sockets) {
-                                let socket = sockets[socketId];
-                                let session = sockets[socketId].session;
-                                if (session.sessionId) {
-                                    this.cache
-                                        .runJob("HGET", {
-                                            table: "sessions",
-                                            key: session.sessionId,
-                                        })
-                                        .then((session) => {
-                                            if (session) {
-                                                this.db
-                                                    .runJob("GET_MODEL", {
-                                                        modelName: "user",
-                                                    })
-                                                    .then((userModel) => {
-                                                        userModel.findOne(
-                                                            {
-                                                                _id:
-                                                                    session.userId,
-                                                            },
-                                                            (err, user) => {
-                                                                if (
-                                                                    !err &&
-                                                                    user
-                                                                ) {
-                                                                    if (
-                                                                        user.role ===
-                                                                        "admin"
-                                                                    )
-                                                                        socket.emit(
-                                                                            "event:station.nextSong",
-                                                                            station._id,
-                                                                            station.currentSong
-                                                                        );
-                                                                    else if (
-                                                                        station.type ===
-                                                                            "community" &&
-                                                                        station.owner ===
-                                                                            session.userId
-                                                                    )
-                                                                        socket.emit(
-                                                                            "event:station.nextSong",
-                                                                            station._id,
-                                                                            station.currentSong
-                                                                        );
-                                                                }
-                                                            }
-                                                        );
-                                                    });
-                                            }
-                                        });
-                                }
-                            }
-                        }
-
-                        if (
-                            station.currentSong !== null &&
-                            station.currentSong.songId !== undefined
-                        ) {
-                            this.utils.runJob("SOCKETS_JOIN_SONG_ROOM", {
-                                sockets: await this.utils.runJob(
-                                    "GET_ROOM_SOCKETS",
-                                    { room: `station.${station._id}` }
-                                ),
-                                room: `song.${station.currentSong.songId}`,
-                            });
-                            if (!station.paused) {
-                                this.notifications.runJob("SCHEDULE", {
-                                    name: `stations.nextSong?id=${station._id}`,
-                                    time: station.currentSong.duration * 1000,
-                                    station,
-                                });
-                            }
-                        } else {
-                            this.utils
-                                .runJob("SOCKETS_LEAVE_SONG_ROOMS", {
-                                    sockets: await this.utils.runJob(
-                                        "GET_ROOM_SOCKETS",
-                                        { room: `station.${station._id}` }
-                                    ),
-                                })
-                                .then()
-                                .catch();
-                        }
-
-                        resolve({ station: station });
-                    }
-                }
-            );
-        });
-    }
-
-    CAN_USER_VIEW_STATION(payload) {
-        // station, userId, hideUnlisted, cb
-        return new Promise((resolve, reject) => {
-            async.waterfall(
-                [
-                    (next) => {
-                        if (payload.station.privacy === "public")
-                            return next(true);
-                        if (payload.station.privacy === "unlisted")
-                            if (payload.hideUnlisted === true)
-                                return next();
-                            else
-                                return next(true);
-                        if (!payload.userId) return next("Not allowed");
-                        next();
-                    },
-
-                    (next) => {
-                        this.db
-                            .runJob("GET_MODEL", {
-                                modelName: "user",
-                            })
-                            .then((userModel) => {
-                                userModel.findOne(
-                                    { _id: payload.userId },
-                                    next
-                                );
-                            });
-                    },
-
-                    (user, next) => {
-                        if (!user) return next("Not allowed");
-                        if (user.role === "admin") return next(true);
-                        if (payload.station.type === "official")
-                            return next("Not allowed");
-                        if (payload.station.owner === payload.userId)
-                            return next(true);
-                        next("Not allowed");
-                    },
-                ],
-                async (errOrResult) => {
-                    if (errOrResult !== true && errOrResult !== "Not allowed") {
-                        errOrResult = await this.utils.runJob("GET_ERROR", {
-                            error: errOrResult,
-                        });
-                        reject(new Error(errOrResult));
-                    } else {
-                        resolve(errOrResult === true ? true : false);
-                    }
-                }
-            );
-        });
-    }
+	constructor() {
+		super("stations");
+	}
+
+	async initialize() {
+		this.cache = this.moduleManager.modules.cache;
+		this.db = this.moduleManager.modules.db;
+		this.utils = this.moduleManager.modules.utils;
+		this.songs = this.moduleManager.modules.songs;
+		this.notifications = this.moduleManager.modules.notifications;
+
+		this.defaultSong = {
+			songId: "60ItHLz5WEA",
+			title: "Faded - Alan Walker",
+			duration: 212,
+			skipDuration: 0,
+			likes: -1,
+			dislikes: -1
+		};
+
+		// TEMP
+		this.cache.runJob("SUB", {
+			channel: "station.pause",
+			cb: async stationId => {
+				this.notifications
+					.runJob("REMOVE", {
+						subscription: `stations.nextSong?id=${stationId}`
+					})
+					.then();
+			}
+		});
+
+		this.cache.runJob("SUB", {
+			channel: "station.resume",
+			cb: async stationId => {
+				this.runJob("INITIALIZE_STATION", { stationId }).then();
+			}
+		});
+
+		this.cache.runJob("SUB", {
+			channel: "station.queueUpdate",
+			cb: async stationId => {
+				this.runJob("GET_STATION", { stationId }).then(station => {
+					if (!station.currentSong && station.queue.length > 0) {
+						this.runJob("INITIALIZE_STATION", {
+							stationId
+						}).then();
+					}
+				});
+			}
+		});
+
+		this.cache.runJob("SUB", {
+			channel: "station.newOfficialPlaylist",
+			cb: async stationId => {
+				this.cache
+					.runJob("HGET", {
+						table: "officialPlaylists",
+						key: stationId
+					})
+					.then(playlistObj => {
+						if (playlistObj) {
+							this.utils.runJob("EMIT_TO_ROOM", {
+								room: `station.${stationId}`,
+								args: ["event:newOfficialPlaylist", playlistObj.songs]
+							});
+						}
+					});
+			}
+		});
+
+		const stationModel = (this.stationModel = await this.db.runJob("GET_MODEL", { modelName: "station" }));
+		const stationSchema = (this.stationSchema = await this.cache.runJob("GET_SCHEMA", { schemaName: "station" }));
+
+		return new Promise((resolve, reject) =>
+			async.waterfall(
+				[
+					next => {
+						this.setStage(2);
+						this.cache
+							.runJob("HGETALL", { table: "stations" })
+							.then(stations => {
+								next(null, stations);
+							})
+							.catch(next);
+					},
+
+					(stations, next) => {
+						this.setStage(3);
+
+						if (!stations) return next();
+
+						const stationIds = Object.keys(stations);
+
+						return async.each(
+							stationIds,
+							(stationId, next) => {
+								stationModel.findOne({ _id: stationId }, (err, station) => {
+									if (err) next(err);
+									else if (!station) {
+										this.cache
+											.runJob("HDEL", {
+												table: "stations",
+												key: stationId
+											})
+											.then(() => {
+												next();
+											})
+											.catch(next);
+									} else next();
+								});
+							},
+							next
+						);
+					},
+
+					next => {
+						this.setStage(4);
+						stationModel.find({}, next);
+					},
+
+					(stations, next) => {
+						this.setStage(5);
+						async.each(
+							stations,
+							(station, next2) => {
+								async.waterfall(
+									[
+										next => {
+											this.cache
+												.runJob("HSET", {
+													table: "stations",
+													key: station._id,
+													value: stationSchema(station)
+												})
+												.then(station => next(null, station))
+												.catch(next);
+										},
+
+										(station, next) => {
+											this.runJob(
+												"INITIALIZE_STATION",
+												{
+													stationId: station._id,
+													bypassQueue: true
+												},
+												{ bypassQueue: true }
+											)
+												.then(() => {
+													next();
+												})
+												.catch(next); // bypassQueue is true because otherwise the module will never initialize
+										}
+									],
+									err => {
+										next2(err);
+									}
+								);
+							},
+							next
+						);
+					}
+				],
+				async err => {
+					if (err) {
+						err = await this.utils.runJob("GET_ERROR", {
+							error: err
+						});
+						reject(new Error(err));
+					} else {
+						resolve();
+					}
+				}
+			)
+		);
+	}
+
+	INITIALIZE_STATION(payload) {
+		// stationId, cb, bypassValidate = false
+		return new Promise((resolve, reject) => {
+			// if (typeof cb !== 'function') cb = ()=>{};
+
+			async.waterfall(
+				[
+					next => {
+						this.runJob(
+							"GET_STATION",
+							{
+								stationId: payload.stationId,
+								bypassQueue: payload.bypassQueue
+							},
+							{ bypassQueue: payload.bypassQueue }
+						)
+							.then(station => {
+								next(null, station);
+							})
+							.catch(next);
+					},
+					(station, next) => {
+						if (!station) return next("Station not found.");
+
+						this.notifications
+							.runJob("UNSCHEDULE", {
+								name: `stations.nextSong?id=${station._id}`
+							})
+							.then()
+							.catch();
+
+						this.notifications
+							.runJob("SUBSCRIBE", {
+								name: `stations.nextSong?id=${station._id}`,
+								cb: () =>
+									this.runJob("SKIP_STATION", {
+										stationId: station._id
+									}),
+								unique: true,
+								station
+							})
+							.then()
+							.catch();
+
+						if (station.paused) return next(true, station);
+
+						return next(null, station);
+					},
+					(station, next) => {
+						if (!station.currentSong) {
+							return this.runJob(
+								"SKIP_STATION",
+								{
+									stationId: station._id,
+									bypassQueue: payload.bypassQueue
+								},
+								{ bypassQueue: payload.bypassQueue }
+							)
+								.then(station => {
+									next(true, station);
+								})
+								.catch(next)
+								.finally(() => {});
+						}
+
+						let timeLeft =
+							station.currentSong.duration * 1000 - (Date.now() - station.startedAt - station.timePaused);
+
+						if (Number.isNaN(timeLeft)) timeLeft = -1;
+
+						if (station.currentSong.duration * 1000 < timeLeft || timeLeft < 0) {
+							return this.runJob(
+								"SKIP_STATION",
+								{
+									stationId: station._id,
+									bypassQueue: payload.bypassQueue
+								},
+								{ bypassQueue: payload.bypassQueue }
+							)
+								.then(station => {
+									next(null, station);
+								})
+								.catch(next);
+						}
+						// name, time, cb, station
+						this.notifications.runJob("SCHEDULE", {
+							name: `stations.nextSong?id=${station._id}`,
+							time: timeLeft,
+							station
+						});
+
+						return next(null, station);
+					}
+				],
+				async (err, station) => {
+					if (err && err !== true) {
+						err = await this.utils.runJob("GET_ERROR", {
+							error: err
+						});
+						reject(new Error(err));
+					} else resolve(station);
+				}
+			);
+		});
+	}
+
+	async CALCULATE_SONG_FOR_STATION(payload) {
+		// station, bypassValidate = false
+		const stationModel = await this.db.runJob("GET_MODEL", { modelName: "station" });
+		const songModel = await this.db.runJob("GET_MODEL", { modelName: "song" });
+
+		return new Promise((resolve, reject) => {
+			const songList = [];
+
+			return async.waterfall(
+				[
+					next => {
+						if (payload.station.genres.length === 0) return next();
+
+						const genresDone = [];
+
+						return payload.station.genres.forEach(genre => {
+							songModel.find({ genres: genre }, (err, songs) => {
+								if (!err) {
+									songs.forEach(song => {
+										if (songList.indexOf(song._id) === -1) {
+											let found = false;
+											song.genres.forEach(songGenre => {
+												if (payload.station.blacklistedGenres.indexOf(songGenre) !== -1)
+													found = true;
+											});
+											if (!found) {
+												songList.push(song._id);
+											}
+										}
+									});
+								}
+								genresDone.push(genre);
+								if (genresDone.length === payload.station.genres.length) next();
+							});
+						});
+					},
+
+					next => {
+						const playlist = [];
+						songList.forEach(songId => {
+							if (payload.station.playlist.indexOf(songId) === -1) playlist.push(songId);
+						});
+
+						// eslint-disable-next-line array-callback-return
+						payload.station.playlist.filter(songId => {
+							if (songList.indexOf(songId) !== -1) playlist.push(songId);
+						});
+
+						this.utils
+							.runJob("SHUFFLE", { array: playlist })
+							.then(result => {
+								next(null, result.array);
+							})
+							.catch(next);
+					},
+
+					(playlist, next) => {
+						this.runJob(
+							"CALCULATE_OFFICIAL_PLAYLIST_LIST",
+							{
+								stationId: payload.station._id,
+								songList: playlist,
+								bypassQueue: payload.bypassQueue
+							},
+							{ bypassQueue: payload.bypassQueue }
+						)
+							.then(() => {
+								next(null, playlist);
+							})
+							.catch(next);
+					},
+
+					(playlist, next) => {
+						stationModel.updateOne(
+							{ _id: payload.station._id },
+							{ $set: { playlist } },
+							{ runValidators: true },
+							() => {
+								this.runJob(
+									"UPDATE_STATION",
+									{
+										stationId: payload.station._id,
+										bypassQueue: payload.bypassQueue
+									},
+									{ bypassQueue: payload.bypassQueue }
+								)
+									.then(() => {
+										next(null, playlist);
+									})
+									.catch(next);
+							}
+						);
+					}
+				],
+				(err, newPlaylist) => {
+					if (err) return reject(new Error(err));
+					return resolve(newPlaylist);
+				}
+			);
+		});
+	}
+
+	// Attempts to get the station from Redis. If it's not in Redis, get it from Mongo and add it to Redis.
+	GET_STATION(payload) {
+		// stationId, cb, bypassValidate = false
+		return new Promise((resolve, reject) => {
+			async.waterfall(
+				[
+					next => {
+						this.cache
+							.runJob("HGET", {
+								table: "stations",
+								key: payload.stationId
+							})
+							.then(station => {
+								next(null, station);
+							})
+							.catch(next);
+					},
+
+					(station, next) => {
+						if (station) return next(true, station);
+						return this.stationModel.findOne({ _id: payload.stationId }, next);
+					},
+
+					(station, next) => {
+						if (station) {
+							if (station.type === "official") {
+								this.runJob("CALCULATE_OFFICIAL_PLAYLIST_LIST", {
+									stationId: station._id,
+									songList: station.playlist
+								})
+									.then()
+									.catch();
+							}
+							station = this.stationSchema(station);
+							this.cache
+								.runJob("HSET", {
+									table: "stations",
+									key: payload.stationId,
+									value: station
+								})
+								.then()
+								.catch();
+							next(true, station);
+						} else next("Station not found");
+					}
+				],
+				async (err, station) => {
+					if (err && err !== true) {
+						err = await this.utils.runJob("GET_ERROR", {
+							error: err
+						});
+						reject(new Error(err));
+					} else resolve(station);
+				}
+			);
+		});
+	}
+
+	// Attempts to get the station from Redis. If it's not in Redis, get it from Mongo and add it to Redis.
+	async GET_STATION_BY_NAME(payload) {
+		// stationName
+		const stationModel = await this.db.runJob("GET_MODEL", { modelName: "station" });
+
+		return new Promise((resolve, reject) =>
+			async.waterfall(
+				[
+					next => {
+						stationModel.findOne({ name: payload.stationName }, next);
+					},
+
+					(station, next) => {
+						if (station) {
+							if (station.type === "official") {
+								this.runJob("CALCULATE_OFFICIAL_PLAYLIST_LIST", {
+									stationId: station._id,
+									songList: station.playlist
+								});
+							}
+							this.cache.runJob("GET_SCHEMA", { schemaName: "station" }).then(stationSchema => {
+								station = stationSchema(station);
+								this.cache.runJob("HSET", {
+									table: "stations",
+									key: station._id,
+									value: station
+								});
+								next(true, station);
+							});
+						} else next("Station not found");
+					}
+				],
+				(err, station) => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve(station);
+				}
+			)
+		);
+	}
+
+	UPDATE_STATION(payload) {
+		// stationId, cb, bypassValidate = false
+		return new Promise((resolve, reject) => {
+			async.waterfall(
+				[
+					next => {
+						this.stationModel.findOne({ _id: payload.stationId }, next);
+					},
+
+					(station, next) => {
+						if (!station) {
+							this.cache
+								.runJob("HDEL", {
+									table: "stations",
+									key: payload.stationId
+								})
+								.then()
+								.catch();
+							return next("Station not found");
+						}
+
+						return this.cache
+							.runJob("HSET", {
+								table: "stations",
+								key: payload.stationId,
+								value: station
+							})
+							.then(station => {
+								next(null, station);
+							})
+							.catch(next);
+					}
+				],
+				async (err, station) => {
+					if (err && err !== true) {
+						err = await this.utils.runJob("GET_ERROR", {
+							error: err
+						});
+						reject(new Error(err));
+					} else resolve(station);
+				}
+			);
+		});
+	}
+
+	async CALCULATE_OFFICIAL_PLAYLIST_LIST(payload) {
+		// stationId, songList, cb, bypassValidate = false
+		const officialPlaylistSchema = await this.cache.runJob("GET_SCHEMA", { schemaName: "officialPlaylist" });
+
+		return new Promise(resolve => {
+			const lessInfoPlaylist = [];
+
+			return async.each(
+				payload.songList,
+				(song, next) => {
+					this.songs
+						.runJob("GET_SONG", { id: song })
+						.then(response => {
+							const { song } = response;
+							if (song) {
+								const newSong = {
+									songId: song.songId,
+									title: song.title,
+									artists: song.artists,
+									duration: song.duration
+								};
+								lessInfoPlaylist.push(newSong);
+							}
+						})
+						.finally(() => {
+							next();
+						});
+				},
+				() => {
+					this.cache
+						.runJob("HSET", {
+							table: "officialPlaylists",
+							key: payload.stationId,
+							value: officialPlaylistSchema(payload.stationId, lessInfoPlaylist)
+						})
+						.finally(() => {
+							this.cache.runJob("PUB", {
+								channel: "station.newOfficialPlaylist",
+								value: payload.stationId
+							});
+							resolve();
+						});
+				}
+			);
+		});
+	}
+
+	SKIP_STATION(payload) {
+		// stationId
+		return new Promise((resolve, reject) => {
+			this.log("INFO", `Skipping station ${payload.stationId}.`);
+
+			this.log("STATION_ISSUE", `SKIP_STATION_CB - Station ID: ${payload.stationId}.`);
+
+			async.waterfall(
+				[
+					next => {
+						this.runJob(
+							"GET_STATION",
+							{
+								stationId: payload.stationId,
+								bypassQueue: payload.bypassQueue
+							},
+							{ bypassQueue: payload.bypassQueue }
+						)
+							.then(station => {
+								next(null, station);
+							})
+							.catch(() => {});
+					},
+					// eslint-disable-next-line consistent-return
+					(station, next) => {
+						if (!station) return next("Station not found.");
+
+						if (station.type === "community" && station.partyMode && station.queue.length === 0)
+							return next(null, null, -11, station); // Community station with party mode enabled and no songs in the queue
+
+						if (station.type === "community" && station.partyMode && station.queue.length > 0) {
+							// Community station with party mode enabled and songs in the queue
+							if (station.paused) return next(null, null, -19, station);
+
+							return this.stationModel.updateOne(
+								{ _id: payload.stationId },
+								{
+									$pull: {
+										queue: {
+											_id: station.queue[0]._id
+										}
+									}
+								},
+								err => {
+									if (err) return next(err);
+									return next(null, station.queue[0], -12, station);
+								}
+							);
+						}
+
+						if (station.type === "community" && !station.partyMode) {
+							return this.db.runJob("GET_MODEL", { modelName: "playlist" }).then(playlistModel =>
+								playlistModel.findOne({ _id: station.privatePlaylist }, (err, playlist) => {
+									if (err) return next(err);
+
+									if (!playlist) return next(null, null, -13, station);
+
+									playlist = playlist.songs;
+
+									if (playlist.length > 0) {
+										let currentSongIndex;
+
+										if (station.currentSongIndex < playlist.length - 1)
+											currentSongIndex = station.currentSongIndex + 1;
+										else currentSongIndex = 0;
+
+										const callback = (err, song) => {
+											if (err) return next(err);
+											if (song) return next(null, song, currentSongIndex, station);
+
+											const currentSong = {
+												songId: playlist[currentSongIndex].songId,
+												title: playlist[currentSongIndex].title,
+												duration: playlist[currentSongIndex].duration,
+												likes: -1,
+												dislikes: -1
+											};
+
+											return next(null, currentSong, currentSongIndex, station);
+										};
+
+										if (playlist[currentSongIndex]._id)
+											return this.songs
+												.runJob("GET_SONG", {
+													id: playlist[currentSongIndex]._id
+												})
+												.then(response => callback(null, response.song))
+												.catch(callback);
+										return this.songs
+											.runJob("GET_SONG_FROM_ID", {
+												songId: playlist[currentSongIndex].songId
+											})
+											.then(response => callback(null, response.song))
+											.catch(callback);
+									}
+
+									return next(null, null, -14, station);
+								})
+							);
+						}
+
+						if (station.type === "official" && station.playlist.length === 0) {
+							return this.runJob(
+								"CALCULATE_SONG_FOR_STATION",
+								{ station, bypassQueue: payload.bypassQueue },
+								{ bypassQueue: payload.bypassQueue }
+							)
+								.then(playlist => {
+									if (playlist.length === 0) return next(null, this.defaultSong, 0, station);
+
+									return this.songs
+										.runJob("GET_SONG", {
+											id: playlist[0]
+										})
+										.then(response => {
+											next(null, response.song, 0, station);
+										})
+										.catch(() => next(null, this.defaultSong, 0, station));
+								})
+								.catch(next);
+						}
+
+						if (station.type === "official" && station.playlist.length > 0) {
+							return async.doUntil(
+								next => {
+									if (station.currentSongIndex < station.playlist.length - 1) {
+										this.songs
+											.runJob("GET_SONG", {
+												id: station.playlist[station.currentSongIndex + 1]
+											})
+											.then(response => next(null, response.song, station.currentSongIndex + 1))
+											.catch(() => {
+												station.currentSongIndex += 1;
+												next(null, null, null);
+											});
+									} else {
+										this.runJob(
+											"CALCULATE_SONG_FOR_STATION",
+											{
+												station,
+												bypassQueue: payload.bypassQueue
+											},
+											{ bypassQueue: payload.bypassQueue }
+										)
+											.then(newPlaylist => {
+												this.songs
+													.runJob("GET_SONG", { id: newPlaylist[0] })
+													.then(response => {
+														station.playlist = newPlaylist;
+														next(null, response.song, 0);
+													})
+													.catch(() => next(null, this.defaultSong, 0));
+											})
+											.catch(() => {
+												next(null, this.defaultSong, 0);
+											});
+									}
+								},
+								(song, currentSongIndex, next) => {
+									if (song) return next(null, true, currentSongIndex);
+									return next(null, false);
+								},
+								(err, song, currentSongIndex) => next(err, song, currentSongIndex, station)
+							);
+						}
+					},
+					(song, currentSongIndex, station, next) => {
+						const $set = {};
+						if (song === null) $set.currentSong = null;
+						else if (song.likes === -1 && song.dislikes === -1) {
+							$set.currentSong = {
+								songId: song.songId,
+								title: song.title,
+								duration: song.duration,
+								skipDuration: 0,
+								likes: -1,
+								dislikes: -1
+							};
+						} else {
+							$set.currentSong = {
+								songId: song.songId,
+								title: song.title,
+								artists: song.artists,
+								duration: song.duration,
+								likes: song.likes,
+								dislikes: song.dislikes,
+								skipDuration: song.skipDuration,
+								thumbnail: song.thumbnail
+							};
+						}
+						if (currentSongIndex >= 0) $set.currentSongIndex = currentSongIndex;
+						$set.startedAt = Date.now();
+						$set.timePaused = 0;
+						if (station.paused) $set.pausedAt = Date.now();
+						next(null, $set, station);
+					},
+
+					($set, station, next) => {
+						this.stationModel.updateOne({ _id: station._id }, { $set }, () => {
+							this.runJob(
+								"UPDATE_STATION",
+								{
+									stationId: station._id,
+									bypassQueue: payload.bypassQueue
+								},
+
+								{ bypassQueue: payload.bypassQueue }
+							)
+								.then(station => {
+									if (station.type === "community" && station.partyMode === true)
+										this.cache
+											.runJob("PUB", {
+												channel: "station.queueUpdate",
+												value: payload.stationId
+											})
+											.then()
+											.catch();
+									next(null, station);
+								})
+								.catch(next);
+						});
+					}
+				],
+				async (err, station) => {
+					if (err) {
+						err = await this.utils.runJob("GET_ERROR", {
+							error: err
+						});
+						this.log("ERROR", `Skipping station "${payload.stationId}" failed. "${err}"`);
+						reject(new Error(err));
+					} else {
+						if (station.currentSong !== null && station.currentSong.songId !== undefined) {
+							station.currentSong.skipVotes = 0;
+						}
+						// TODO Pub/Sub this
+
+						this.utils
+							.runJob("EMIT_TO_ROOM", {
+								room: `station.${station._id}`,
+								args: [
+									"event:songs.next",
+									{
+										currentSong: station.currentSong,
+										startedAt: station.startedAt,
+										paused: station.paused,
+										timePaused: 0
+									}
+								]
+							})
+							.then()
+							.catch();
+
+						if (station.privacy === "public") {
+							this.utils
+								.runJob("EMIT_TO_ROOM", {
+									room: "home",
+									args: ["event:station.nextSong", station._id, station.currentSong]
+								})
+								.then()
+								.catch();
+						} else {
+							const sockets = await this.utils.runJob("GET_ROOM_SOCKETS", { room: "home" });
+
+							for (
+								let socketId = 0, socketKeys = Object.keys(sockets);
+								socketId < socketKeys.length;
+								socketId += 1
+							) {
+								const socket = sockets[socketId];
+								const { session } = sockets[socketId];
+								if (session.sessionId) {
+									this.cache
+										.runJob("HGET", {
+											table: "sessions",
+											key: session.sessionId
+										})
+										.then(session => {
+											if (session) {
+												this.db.runJob("GET_MODEL", { modelName: "user" }).then(userModel => {
+													userModel.findOne(
+														{
+															_id: session.userId
+														},
+														(err, user) => {
+															if (!err && user) {
+																if (user.role === "admin")
+																	socket.emit(
+																		"event:station.nextSong",
+																		station._id,
+																		station.currentSong
+																	);
+																else if (
+																	station.type === "community" &&
+																	station.owner === session.userId
+																)
+																	socket.emit(
+																		"event:station.nextSong",
+																		station._id,
+																		station.currentSong
+																	);
+															}
+														}
+													);
+												});
+											}
+										});
+								}
+							}
+						}
+
+						if (station.currentSong !== null && station.currentSong.songId !== undefined) {
+							this.utils.runJob("SOCKETS_JOIN_SONG_ROOM", {
+								sockets: await this.utils.runJob("GET_ROOM_SOCKETS", {
+									room: `station.${station._id}`
+								}),
+								room: `song.${station.currentSong.songId}`
+							});
+							if (!station.paused) {
+								this.notifications.runJob("SCHEDULE", {
+									name: `stations.nextSong?id=${station._id}`,
+									time: station.currentSong.duration * 1000,
+									station
+								});
+							}
+						} else {
+							this.utils
+								.runJob("SOCKETS_LEAVE_SONG_ROOMS", {
+									sockets: await this.utils.runJob("GET_ROOM_SOCKETS", {
+										room: `station.${station._id}`
+									})
+								})
+								.then()
+								.catch();
+						}
+
+						resolve({ station });
+					}
+				}
+			);
+		});
+	}
+
+	CAN_USER_VIEW_STATION(payload) {
+		// station, userId, hideUnlisted
+		return new Promise((resolve, reject) => {
+			async.waterfall(
+				[
+					next => {
+						if (payload.station.privacy === "public") return next(true);
+						if (payload.station.privacy === "unlisted")
+							if (payload.hideUnlisted === true) return next();
+							else return next(true);
+						if (!payload.userId) return next("Not allowed");
+
+						return next();
+					},
+
+					next => {
+						this.db
+							.runJob("GET_MODEL", {
+								modelName: "user"
+							})
+							.then(userModel => {
+								userModel.findOne({ _id: payload.userId }, next);
+							});
+					},
+
+					(user, next) => {
+						if (!user) return next("Not allowed");
+						if (user.role === "admin") return next(true);
+						if (payload.station.type === "official") return next("Not allowed");
+						if (payload.station.owner === payload.userId) return next(true);
+
+						return next("Not allowed");
+					}
+				],
+				async errOrResult => {
+					if (errOrResult !== true && errOrResult !== "Not allowed") {
+						errOrResult = await this.utils.runJob("GET_ERROR", {
+							error: errOrResult
+						});
+						reject(new Error(errOrResult));
+					} else {
+						resolve(errOrResult === true);
+					}
+				}
+			);
+		});
+	}
 }
 
-module.exports = new StationsModule();
+export default new StationsModule();

+ 259 - 318
backend/logic/tasks.js

@@ -1,342 +1,283 @@
-const CoreClass = require("../core.js");
+import async from "async";
+import fs from "fs";
+import path from "path";
+import { fileURLToPath } from "url";
 
-const tasks = {};
+import CoreClass from "../core";
+import Timer from "../classes/Timer.class";
 
-const async = require("async");
-const fs = require("fs");
-
-const Timer = require("../classes/Timer.class");
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
 class TasksModule extends CoreClass {
-    constructor() {
-        super("tasks");
-    }
+	constructor() {
+		super("tasks");
+
+		this.tasks = {};
+	}
+
+	initialize() {
+		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;
+
+			// this.createTask("testTask", testTask, 5000, true);
+
+			this.runJob("CREATE_TASK", {
+				name: "stationSkipTask",
+				fn: this.checkStationSkipTask,
+				timeout: 1000 * 60 * 30
+			});
+
+			this.runJob("CREATE_TASK", {
+				name: "sessionClearTask",
+				fn: this.sessionClearingTask,
+				timeout: 1000 * 60 * 60 * 6
+			});
+
+			this.runJob("CREATE_TASK", {
+				name: "logFileSizeCheckTask",
+				fn: this.logFileSizeCheckTask,
+				timeout: 1000 * 60 * 60
+			});
+
+			resolve();
+		});
+	}
 
-    initialize() {
-        return new Promise((resolve, reject) => {
-            // return reject(new Error("Not fully migrated yet."));
+	CREATE_TASK(payload) {
+		return new Promise((resolve, reject) => {
+			this.tasks[payload.name] = {
+				name: payload.name,
+				fn: payload.fn,
+				timeout: payload.timeout,
+				lastRan: 0,
+				timer: null
+			};
 
-            this.cache = this.moduleManager.modules["cache"];
-            this.stations = this.moduleManager.modules["stations"];
-            this.notifications = this.moduleManager.modules["notifications"];
-            this.utils = this.moduleManager.modules["utils"];
+			if (!payload.paused) {
+				this.runJob("RUN_TASK", { name: payload.name })
+					.then(() => resolve())
+					.catch(err => reject(err));
+			} else resolve();
+		});
+	}
 
-            //this.createTask("testTask", testTask, 5000, true);
+	PAUSE_TASK(payload) {
+		const taskName = { payload };
 
-            this.runJob("CREATE_TASK", {
-                name: "stationSkipTask",
-                fn: this.checkStationSkipTask,
-                timeout: 1000 * 60 * 30,
-            });
+		return new Promise(resolve => {
+			if (this.tasks[taskName].timer) this.tasks[taskName].timer.pause();
+			resolve();
+		});
+	}
 
-            this.runJob("CREATE_TASK", {
-                name: "sessionClearTask",
-                fn: this.sessionClearingTask,
-                timeout: 1000 * 60 * 60 * 6,
-            });
+	RESUME_TASK(payload) {
+		return new Promise(resolve => {
+			this.tasks[payload.name].timer.resume();
+			resolve();
+		});
+	}
 
-            this.runJob("CREATE_TASK", {
-                name: "logFileSizeCheckTask",
-                fn: this.logFileSizeCheckTask,
-                timeout: 1000 * 60 * 60,
-            });
+	RUN_TASK(payload) {
+		return new Promise(resolve => {
+			const task = this.tasks[payload.name];
+			if (task.timer) task.timer.pause();
 
-            resolve();
-        });
-    }
+			task.fn.apply(this).then(() => {
+				task.lastRan = Date.now();
+				task.timer = new Timer(() => this.runJob("RUN_TASK", { name: payload.name }), task.timeout, false);
+				resolve();
+			});
+		});
+	}
 
-    CREATE_TASK(payload) {
-        return new Promise((resolve, reject) => {
-            tasks[payload.name] = {
-                name: payload.name,
-                fn: payload.fn,
-                timeout: payload.timeout,
-                lastRan: 0,
-                timer: null,
-            };
+	checkStationSkipTask() {
+		return new Promise(resolve => {
+			this.log("INFO", "TASK_STATIONS_SKIP_CHECK", `Checking for stations to be skipped.`, false);
+			async.waterfall(
+				[
+					next => {
+						this.cache
+							.runJob("HGETALL", { table: "stations" })
+							.then(response => next(null, response))
+							.catch(next);
+					},
+					(stations, next) => {
+						async.each(
+							stations,
+							(station, next2) => {
+								if (station.paused || !station.currentSong || !station.currentSong.title)
+									return next2();
+								const timeElapsed = Date.now() - station.startedAt - station.timePaused;
+								if (timeElapsed <= station.currentSong.duration) return next2();
 
-            if (!payload.paused) {
-                this.runJob("RUN_TASK", { name: payload.name })
-                    .then(() => resolve())
-                    .catch((err) => reject(err));
-            } else resolve();
-        });
-    }
+								this.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());
+							},
+							() => next()
+						);
+					}
+				],
+				() => resolve()
+			);
+		});
+	}
 
-    PAUSE_TASK(payload) {
-        return new Promise((resolve, reject) => {
-            if (tasks[payload.name].timer) tasks[name].timer.pause();
-            resolve();
-        });
-    }
+	sessionClearingTask() {
+		return new Promise(resolve => {
+			this.log("INFO", "TASK_SESSION_CLEAR", `Checking for sessions to be cleared.`);
 
-    RESUME_TASK(payload) {
-        return new Promise((resolve, reject) => {
-            tasks[payload.name].timer.resume();
-            resolve();
-        });
-    }
+			async.waterfall(
+				[
+					next => {
+						this.cache
+							.runJob("HGETALL", { table: "sessions" })
+							.then(sessions => next(null, sessions))
+							.catch(next);
+					},
+					(sessions, next) => {
+						if (!sessions) return next();
 
-    RUN_TASK(payload) {
-        return new Promise((resolve, reject) => {
-            const task = tasks[payload.name];
-            if (task.timer) task.timer.pause();
+						const keys = Object.keys(sessions);
 
-            task.fn.apply(this).then(() => {
-                task.lastRan = Date.now();
-                task.timer = new Timer(
-                    () => {
-                        this.runJob("RUN_TASK", { name: payload.name });
-                    },
-                    task.timeout,
-                    false
-                );
+						return async.each(
+							keys,
+							(sessionId, next2) => {
+								const session = sessions[sessionId];
 
-                resolve();
-            });
-        });
-    }
+								if (
+									session &&
+									session.refreshDate &&
+									Date.now() - session.refreshDate < 60 * 60 * 24 * 30 * 1000
+								)
+									return next2();
 
-    checkStationSkipTask(callback) {
-        return new Promise((resolve, reject) => {
-            this.log(
-                "INFO",
-                "TASK_STATIONS_SKIP_CHECK",
-                `Checking for stations to be skipped.`,
-                false
-            );
-            async.waterfall(
-                [
-                    (next) => {
-                        this.cache
-                            .runJob("HGETALL", {
-                                table: "stations",
-                            })
-                            .then((response) => {
-                                next(null, response);
-                            })
-                            .catch(next);
-                    },
-                    (stations, next) => {
-                        async.each(
-                            stations,
-                            (station, next2) => {
-                                if (
-                                    station.paused ||
-                                    !station.currentSong ||
-                                    !station.currentSong.title
-                                )
-                                    return next2();
-                                const timeElapsed =
-                                    Date.now() -
-                                    station.startedAt -
-                                    station.timePaused;
-                                if (timeElapsed <= station.currentSong.duration)
-                                    return next2();
-                                else {
-                                    this.log(
-                                        "ERROR",
-                                        "TASK_STATIONS_SKIP_CHECK",
-                                        `Skipping ${station._id} as it should have skipped already.`
-                                    );
-                                    this.stations
-                                        .runJob("INITIALIZE_STATION", {
-                                            stationId: station._id,
-                                        })
-                                        .then(() => {
-                                            next2();
-                                        });
-                                }
-                            },
-                            () => {
-                                next();
-                            }
-                        );
-                    },
-                ],
-                () => {
-                    resolve();
-                }
-            );
-        });
-    }
+								if (!session) {
+									this.log("INFO", "TASK_SESSION_CLEAR", "Removing an empty session.");
+									return this.cache
+										.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());
+								}
+								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());
+											}
+										});
+								}
+								this.log("ERROR", "TASK_SESSION_CLEAR", "This should never log.");
+								return next2();
+							},
+							() => next()
+						);
+					}
+				],
+				() => resolve()
+			);
+		});
+	}
 
-    sessionClearingTask() {
-        return new Promise((resolve, reject) => {
-            this.log(
-                "INFO",
-                "TASK_SESSION_CLEAR",
-                `Checking for sessions to be cleared.`
-            );
+	logFileSizeCheckTask() {
+		return new Promise((resolve, reject) => {
+			this.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) => {
+					try {
+						const stats = fs.statSync(path.resolve(__dirname, "../../log/", fileName));
+						const mb = stats.size / 1000000;
+						if (mb > 25) return next(true);
 
-            async.waterfall(
-                [
-                    (next) => {
-                        this.cache
-                            .runJob("HGETALL", {
-                                table: "sessions",
-                            })
-                            .then((sessions) => {
-                                next(null, sessions);
-                            })
-                            .catch(next);
-                    },
-                    (sessions, next) => {
-                        if (!sessions) return next();
-                        let keys = Object.keys(sessions);
-                        async.each(
-                            keys,
-                            (sessionId, next2) => {
-                                let session = sessions[sessionId];
-                                if (
-                                    session &&
-                                    session.refreshDate &&
-                                    Date.now() - session.refreshDate <
-                                        60 * 60 * 24 * 30 * 1000
-                                )
-                                    return next2();
-                                if (!session) {
-                                    this.log(
-                                        "INFO",
-                                        "TASK_SESSION_CLEAR",
-                                        "Removing an empty session."
-                                    );
-                                    this.cache
-                                        .runJob("HDEL", {
-                                            table: "sessions",
-                                            key: sessionId,
-                                        })
-                                        .finally(() => {
-                                            next2();
-                                        });
-                                } else if (!session.refreshDate) {
-                                    session.refreshDate = Date.now();
-                                    this.cache
-                                        .runJob("HSET", {
-                                            table: "sessions",
-                                            key: sessionId,
-                                            value: session,
-                                        })
-                                        .finally(() => {
-                                            next2()
-                                        });
-                                } else if (
-                                    Date.now() - session.refreshDate >
-                                    60 * 60 * 24 * 30 * 1000
-                                ) {
-                                    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();
-                                                    });
-                                            }
-                                        });
-                                } else {
-                                    this.log(
-                                        "ERROR",
-                                        "TASK_SESSION_CLEAR",
-                                        "This should never log."
-                                    );
-                                    next2();
-                                }
-                            },
-                            () => {
-                                next();
-                            }
-                        );
-                    },
-                ],
-                () => {
-                    resolve();
-                }
-            );
-        });
-    }
+						return next();
+					} catch (err) {
+						return next(err);
+					}
+				},
+				async err => {
+					if (err && err !== true) {
+						err = await this.utils.runJob("GET_ERROR", { error: err });
+						return reject(new Error(err));
+					}
+					if (err === true) {
+						this.log(
+							"ERROR",
+							"LOGGER_FILE_SIZE_WARNING",
+							"************************************WARNING*************************************"
+						);
+						this.log(
+							"ERROR",
+							"LOGGER_FILE_SIZE_WARNING",
+							"***************ONE OR MORE LOG FILES APPEAR TO BE MORE THAN 25MB****************"
+						);
+						this.log(
+							"ERROR",
+							"LOGGER_FILE_SIZE_WARNING",
+							"****MAKE SURE TO REGULARLY CLEAR UP THE LOG FILES, MANUALLY OR AUTOMATICALLY****"
+						);
+						this.log(
+							"ERROR",
+							"LOGGER_FILE_SIZE_WARNING",
+							"********************************************************************************"
+						);
+					}
 
-    logFileSizeCheckTask() {
-        return new Promise((resolve, reject) => {
-            this.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) => {
-                    try {
-                        const stats = fs.statSync(
-                            `${__dirname}/../../log/${fileName}`
-                        );
-                        const mb = stats.size / 1000000;
-                        if (mb > 25) return next(true);
-                        else next();
-                    } catch(err) {
-                        next(err);
-                    }
-                },
-                async (err) => {
-                    if (err && err !== true) {
-                        err = await this.utils.runJob("GET_ERROR", { error: err });
-                        return reject(new Error(err));
-                    } else if (err === true) {
-                        this.log(
-                            "ERROR",
-                            "LOGGER_FILE_SIZE_WARNING",
-                            "************************************WARNING*************************************"
-                        );
-                        this.log(
-                            "ERROR",
-                            "LOGGER_FILE_SIZE_WARNING",
-                            "***************ONE OR MORE LOG FILES APPEAR TO BE MORE THAN 25MB****************"
-                        );
-                        this.log(
-                            "ERROR",
-                            "LOGGER_FILE_SIZE_WARNING",
-                            "****MAKE SURE TO REGULARLY CLEAR UP THE LOG FILES, MANUALLY OR AUTOMATICALLY****"
-                        );
-                        this.log(
-                            "ERROR",
-                            "LOGGER_FILE_SIZE_WARNING",
-                            "********************************************************************************"
-                        );
-                    }
-                    
-                    resolve();
-                }
-            );
-        });
-    }
+					return resolve();
+				}
+			);
+		});
+	}
 }
 
-module.exports = new TasksModule();
+export default new TasksModule();

+ 791 - 806
backend/logic/utils.js

@@ -1,811 +1,796 @@
-const CoreClass = require("../core.js");
+import config from "config";
 
-const config = require("config");
-const async = require("async");
-const request = require("request");
-const crypto = require("crypto");
-
-let youtubeRequestCallbacks = [];
-let youtubeRequestsPending = 0;
-let youtubeRequestsActive = false;
+import async from "async";
+import crypto from "crypto";
+import request from "request";
+import CoreClass from "../core";
 
 class UtilsModule extends CoreClass {
-    constructor() {
-        super("utils");
-    }
-
-    initialize() {
-        return new Promise((resolve, reject) => {
-            this.io = this.moduleManager.modules["io"];
-            this.db = this.moduleManager.modules["db"];
-            this.spotify = this.moduleManager.modules["spotify"];
-            this.cache = this.moduleManager.modules["cache"];
-
-            resolve();
-        });
-    }
-
-    PARSE_COOKIES(payload) {
-        //cookieString
-        return new Promise((resolve, reject) => {
-            let cookies = {};
-            if (typeof payload.cookieString !== "string") return reject("Cookie string is not a string");
-            payload.cookieString.split("; ").map((cookie) => {
-                cookies[
-                    cookie.substring(0, cookie.indexOf("="))
-                ] = cookie.substring(cookie.indexOf("=") + 1, cookie.length);
-            });
-            resolve(cookies);
-        });
-    }
-
-    // COOKIES_TO_STRING() {//cookies
-    // 	return new Promise((resolve, reject) => {
-    //         let newCookie = [];
-    //         for (let prop in cookie) {
-    //             newCookie.push(prop + "=" + cookie[prop]);
-    //         }
-    //         return newCookie.join("; ");
-    //     });
-    // }
-
-    REMOVE_COOKIE(payload) {
-        //cookieString, cookieName
-        return new Promise(async (resolve, reject) => {
-            var cookies = await this.runJob("PARSE_COOKIES", {
-                cookieString: payload.cookieString,
-            });
-            delete cookies[payload.cookieName];
-            resolve(this.toString(cookies));
-        });
-    }
-
-    HTML_ENTITIES(payload) {
-        //str
-        return new Promise((resolve, reject) => {
-            resolve(
-                String(payload.str)
-                    .replace(/&/g, "&amp;")
-                    .replace(/</g, "&lt;")
-                    .replace(/>/g, "&gt;")
-                    .replace(/"/g, "&quot;")
-            );
-        });
-    }
-
-    GENERATE_RANDOM_STRING(payload) {
-        //length
-        return new Promise(async (resolve, reject) => {
-            let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".split(
-                ""
-            );
-            let result = [];
-            for (let i = 0; i < payload.length; i++) {
-                result.push(
-                    chars[
-                        await this.runJob("GET_RANDOM_NUMBER", {
-                            min: 0,
-                            max: chars.length - 1,
-                        })
-                    ]
-                );
-            }
-            resolve(result.join(""));
-        });
-    }
-
-    GET_SOCKET_FROM_ID(payload) {
-        //socketId
-        return new Promise(async (resolve, reject) => {
-            let io = await this.io.runJob("IO", {});
-            resolve(io.sockets.sockets[payload.socketId]);
-        });
-    }
-
-    GET_RANDOM_NUMBER(payload) {
-        //min, max
-        return new Promise((resolve, reject) => {
-            resolve(
-                Math.floor(Math.random() * (payload.max - payload.min + 1)) +
-                    payload.min
-            );
-        });
-    }
-
-    CONVERT_TIME(payload) {
-        //duration
-        return new Promise((resolve, reject) => {
-            let duration = payload.duration;
-            let a = duration.match(/\d+/g);
-
-            if (
-                duration.indexOf("M") >= 0 &&
-                duration.indexOf("H") == -1 &&
-                duration.indexOf("S") == -1
-            ) {
-                a = [0, a[0], 0];
-            }
-
-            if (duration.indexOf("H") >= 0 && duration.indexOf("M") == -1) {
-                a = [a[0], 0, a[1]];
-            }
-            if (
-                duration.indexOf("H") >= 0 &&
-                duration.indexOf("M") == -1 &&
-                duration.indexOf("S") == -1
-            ) {
-                a = [a[0], 0, 0];
-            }
-
-            duration = 0;
-
-            if (a.length == 3) {
-                duration = duration + parseInt(a[0]) * 3600;
-                duration = duration + parseInt(a[1]) * 60;
-                duration = duration + parseInt(a[2]);
-            }
-
-            if (a.length == 2) {
-                duration = duration + parseInt(a[0]) * 60;
-                duration = duration + parseInt(a[1]);
-            }
-
-            if (a.length == 1) {
-                duration = duration + parseInt(a[0]);
-            }
-
-            let hours = Math.floor(duration / 3600);
-            let minutes = Math.floor((duration % 3600) / 60);
-            let seconds = Math.floor((duration % 3600) % 60);
-
-            resolve(
-                (hours < 10 ? "0" + hours + ":" : hours + ":") +
-                    (minutes < 10 ? "0" + minutes + ":" : minutes + ":") +
-                    (seconds < 10 ? "0" + seconds : seconds)
-            );
-        });
-    }
-
-    GUID(payload) {
-        return new Promise((resolve, reject) => {
-            resolve(
-                [1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1]
-                    .map((b) =>
-                        b
-                            ? Math.floor((1 + Math.random()) * 0x10000)
-                                  .toString(16)
-                                  .substring(1)
-                            : "-"
-                    )
-                    .join("")
-            );
-        });
-    }
-
-    SOCKET_FROM_SESSION(payload) {
-        //socketId
-        return new Promise(async (resolve, reject) => {
-            let io = await this.io.runJob("IO", {});
-            let ns = io.of("/");
-            if (ns) {
-                resolve(ns.connected[payload.socketId]);
-            }
-        });
-    }
-
-    SOCKETS_FROM_SESSION_ID(payload) {
-        return new Promise(async (resolve, reject) => {
-            let io = await this.io.runJob("IO", {});
-            let ns = io.of("/");
-            let sockets = [];
-            if (ns) {
-                async.each(
-                    Object.keys(ns.connected),
-                    (id, next) => {
-                        let session = ns.connected[id].session;
-                        if (session.sessionId === payload.sessionId)
-                            sockets.push(session.sessionId);
-                        next();
-                    },
-                    () => {
-                        resolve({ sockets });
-                    }
-                );
-            }
-        });
-    }
-
-    SOCKETS_FROM_USER(payload) {
-        return new Promise(async (resolve, reject) => {
-            let io = await this.io.runJob("IO", {});
-            let ns = io.of("/");
-            let sockets = [];
-
-            if (ns) {
-                async.each(
-                    Object.keys(ns.connected),
-                    (id, next) => {
-                        let session = ns.connected[id].session;
-                        this.cache
-                            .runJob("HGET", {
-                                table: "sessions",
-                                key: session.sessionId,
-                            })
-                            .then((session) => {
-                                if (
-                                    session &&
-                                    session.userId === payload.userId
-                                )
-                                    sockets.push(ns.connected[id]);
-                                next();
-                            })
-                            .catch(err => {
-                                next(err);
-                            });
-                    },
-                    err => {
-                        if (err) return reject(err);
-                        return resolve({ sockets });
-                    }
-                );
-            }
-        });
-    }
-
-    SOCKETS_FROM_IP(payload) {
-        return new Promise(async (resolve, reject) => {
-            let io = await this.io.runJob("IO", {});
-            let ns = io.of("/");
-            let sockets = [];
-            if (ns) {
-                async.each(
-                    Object.keys(ns.connected),
-                    (id, next) => {
-                        let session = ns.connected[id].session;
-                        this.cache
-                            .runJob("HGET", {
-                                table: "sessions",
-                                key: session.sessionId,
-                            })
-                            .then((session) => {
-                                if (
-                                    session &&
-                                    ns.connected[id].ip === payload.ip
-                                )
-                                    sockets.push(ns.connected[id]);
-                                next();
-                            })
-                            .catch((err) => {
-                                next();
-                            });
-                    },
-                    () => {
-                        resolve({ sockets });
-                    }
-                );
-            }
-        });
-    }
-
-    SOCKETS_FROM_USER_WITHOUT_CACHE(payload) {
-        return new Promise(async (resolve, reject) => {
-            let io = await this.io.runJob("IO", {});
-            let ns = io.of("/");
-            let sockets = [];
-            if (ns) {
-                async.each(
-                    Object.keys(ns.connected),
-                    (id, next) => {
-                        let session = ns.connected[id].session;
-                        if (session.userId === payload.userId)
-                            sockets.push(ns.connected[id]);
-                        next();
-                    },
-                    () => {
-                        resolve({ sockets });
-                    }
-                );
-            }
-        });
-    }
-
-    SOCKET_LEAVE_ROOMS(payload) {
-        //socketId
-        return new Promise(async (resolve, reject) => {
-            let socket = await this.runJob("SOCKET_FROM_SESSION", {
-                socketId: payload.socketId,
-            });
-            let rooms = socket.rooms;
-            for (let room in rooms) {
-                socket.leave(room);
-            }
-
-            resolve();
-        });
-    }
-
-    SOCKET_JOIN_ROOM(payload) {
-        //socketId, room
-        return new Promise(async (resolve, reject) => {
-            let socket = await this.runJob("SOCKET_FROM_SESSION", {
-                socketId: payload.socketId,
-            });
-            let rooms = socket.rooms;
-            for (let room in rooms) {
-                socket.leave(room);
-            }
-            socket.join(payload.room);
-            resolve();
-        });
-    }
-
-    SOCKET_JOIN_SONG_ROOM(payload) {
-        //socketId, room
-        return new Promise(async (resolve, reject) => {
-            let socket = await this.runJob("SOCKET_FROM_SESSION", {
-                socketId: payload.socketId,
-            });
-            let rooms = socket.rooms;
-            for (let room in rooms) {
-                if (room.indexOf("song.") !== -1) socket.leave(rooms);
-            }
-            socket.join(payload.room);
-            resolve();
-        });
-    }
-
-    SOCKETS_JOIN_SONG_ROOM(payload) {
-        //sockets, room
-        return new Promise((resolve, reject) => {
-            for (let id in payload.sockets) {
-                let socket = payload.sockets[id];
-                let rooms = socket.rooms;
-                for (let room in rooms) {
-                    if (room.indexOf("song.") !== -1) socket.leave(room);
-                }
-                socket.join(payload.room);
-            }
-            resolve();
-        });
-    }
-
-    SOCKETS_LEAVE_SONG_ROOMS(payload) {
-        //sockets
-        return new Promise((resolve, reject) => {
-            for (let id in payload.sockets) {
-                let socket = payload.sockets[id];
-                let rooms = socket.rooms;
-                for (let room in rooms) {
-                    if (room.indexOf("song.") !== -1) socket.leave(room);
-                }
-            }
-            resolve();
-        });
-    }
-
-    EMIT_TO_ROOM(payload) {
-        //room, ...args
-        return new Promise(async (resolve, reject) => {
-            let io = await this.io.runJob("IO", {});
-            let sockets = io.sockets.sockets;
-            for (let id in sockets) {
-                let socket = sockets[id];
-                if (socket.rooms[payload.room]) {
-                    socket.emit.apply(socket, payload.args);
-                }
-            }
-            resolve();
-        });
-    }
-
-    GET_ROOM_SOCKETS(payload) {
-        //room
-        return new Promise(async (resolve, reject) => {
-            let io = await this.io.runJob("IO", {});
-            let sockets = io.sockets.sockets;
-            let roomSockets = [];
-            for (let id in sockets) {
-                let socket = sockets[id];
-                if (socket.rooms[payload.room]) roomSockets.push(socket);
-            }
-            resolve(roomSockets);
-        });
-    }
-
-    GET_SONG_FROM_YOUTUBE(payload) {
-        //songId, cb
-        return new Promise((resolve, reject) => {
-            youtubeRequestCallbacks.push({
-                cb: (test) => {
-                    youtubeRequestsActive = true;
-                    const youtubeParams = [
-                        "part=snippet,contentDetails,statistics,status",
-                        `id=${encodeURIComponent(payload.songId)}`,
-                        `key=${config.get("apis.youtube.key")}`,
-                    ].join("&");
-
-                    request(
-                        `https://www.googleapis.com/youtube/v3/videos?${youtubeParams}`,
-                        (err, res, body) => {
-                            youtubeRequestCallbacks.splice(0, 1);
-                            if (youtubeRequestCallbacks.length > 0) {
-                                youtubeRequestCallbacks[0].cb(
-                                    youtubeRequestCallbacks[0].songId
-                                );
-                            } else youtubeRequestsActive = false;
-
-                            if (err) {
-                                console.error(err);
-                                return null;
-                            }
-
-                            body = JSON.parse(body);
-
-                            if (body.error) {
-                                console.log(
-                                    "ERROR",
-                                    "GET_SONG_FROM_YOUTUBE",
-                                    `${body.error.message}`
-                                );
-                                return reject(new Error("An error has occured. Please try again later."));
-                            }
-
-                            if (body.items[0] === undefined)
-                                return reject(new Error("The specified video does not exist or cannot be publicly accessed."));
-
-                            //TODO Clean up duration converter
-                            let dur = body.items[0].contentDetails.duration;
-                            dur = dur.replace("PT", "");
-                            let duration = 0;
-                            dur = dur.replace(/([\d]*)H/, (v, v2) => {
-                                v2 = Number(v2);
-                                duration = v2 * 60 * 60;
-                                return "";
-                            });
-                            dur = dur.replace(/([\d]*)M/, (v, v2) => {
-                                v2 = Number(v2);
-                                duration += v2 * 60;
-                                return "";
-                            });
-                            dur = dur.replace(/([\d]*)S/, (v, v2) => {
-                                v2 = Number(v2);
-                                duration += v2;
-                                return "";
-                            });
-
-                            let song = {
-                                songId: body.items[0].id,
-                                title: body.items[0].snippet.title,
-                                duration,
-                            };
-                            resolve({ song });
-                        }
-                    );
-                },
-                songId: payload.songId,
-            });
-
-            if (!youtubeRequestsActive) {
-                youtubeRequestCallbacks[0].cb(
-                    youtubeRequestCallbacks[0].songId
-                );
-            }
-        });
-    }
-
-    FILTER_MUSIC_VIDEOS_YOUTUBE(payload) {
-        //videoIds, cb
-        return new Promise((resolve, reject) => {
-            function getNextPage(cb2) {
-                let localVideoIds = payload.videoIds.splice(0, 50);
-
-                const youtubeParams = [
-                    "part=topicDetails",
-                    `id=${encodeURIComponent(localVideoIds.join(","))}`,
-                    `maxResults=50`,
-                    `key=${config.get("apis.youtube.key")}`,
-                ].join("&");
-
-                request(
-                    `https://www.googleapis.com/youtube/v3/videos?${youtubeParams}`,
-                    async (err, res, body) => {
-                        if (err) {
-                            console.error(err);
-                            return next("Failed to find playlist from YouTube");
-                        }
-
-                        body = JSON.parse(body);
-
-                        if (body.error) {
-                            console.log(
-                                "ERROR",
-                                "FILTER_MUSIC_VIDEOS_YOUTUBE",
-                                `${body.error.message}`
-                            );
-                            return reject(new Error("An error has occured. Please try again later."));
-                        }
-
-                        let songIds = [];
-                        body.items.forEach((item) => {
-                            const songId = item.id;
-                            if (!item.topicDetails) return;
-                            else if (
-                                item.topicDetails.relevantTopicIds.indexOf(
-                                    "/m/04rlf"
-                                ) !== -1
-                            ) {
-                                songIds.push(songId);
-                            }
-                        });
-
-                        if (payload.videoIds.length > 0) {
-                            getNextPage((newSongIds) => {
-                                cb2(songIds.concat(newSongIds));
-                            });
-                        } else cb2(songIds);
-                    }
-                );
-            }
-
-            if (payload.videoIds.length === 0) resolve({ songIds: [] });
-            else
-                getNextPage((songIds) => {
-                    resolve({ songIds });
-                });
-        });
-    }
-
-    GET_PLAYLIST_FROM_YOUTUBE(payload) {
-        // payload includes: url, musicOnly
-        return new Promise((resolve, reject) => {
-            let local = this;
-
-            let name = "list".replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
-
-            const regex = new RegExp("[\\?&]" + name + "=([^&#]*)");
-            const splitQuery = regex.exec(payload.url);
-
-            if (!splitQuery) {
-                console.log(
-                    "ERROR",
-                    "GET_PLAYLIST_FROM_YOUTUBE",
-                    "Invalid YouTube playlist URL query."
-                );
-                return reject(new Error("An error has occured. Please try again later."));
-            }
-
-            let playlistId = splitQuery[1];
-
-            function getPage(pageToken, songs) {
-                let nextPageToken = pageToken ? `pageToken=${pageToken}` : "";
-                const youtubeParams = [
-                    "part=contentDetails",
-                    `playlistId=${encodeURIComponent(playlistId)}`,
-                    `maxResults=50`,
-                    `key=${config.get("apis.youtube.key")}`,
-                    nextPageToken,
-                ].join("&");
-
-                request(
-                    `https://www.googleapis.com/youtube/v3/playlistItems?${youtubeParams}`,
-                    async (err, res, body) => {
-                        if (err) {
-                            console.error(err);
-                            return next("Failed to find playlist from YouTube");
-                        }
-
-                        body = JSON.parse(body);
-
-                        if (body.error) {
-                            console.log(
-                                "ERROR",
-                                "GET_PLAYLIST_FROM_YOUTUBE",
-                                `${body.error.message}`
-                            );
-                            return reject(new Error("An error has occured. Please try again later."));
-                        }
-
-                        songs = songs.concat(body.items);
-
-                        if (body.nextPageToken)
-                            getPage(body.nextPageToken, songs);
-                        else {
-                            songs = songs.map(
-                                (song) => song.contentDetails.videoId
-                            );
-                            if (!payload.musicOnly) resolve({ songs });
-                            else {
-                                local
-                                    .runJob("FILTER_MUSIC_VIDEOS_YOUTUBE", {
-                                        videoIds: songs.slice(),
-                                    })
-                                    .then((filteredSongs) => {
-                                        resolve({ filteredSongs, songs });
-                                    });
-                            }
-                        }
-                    }
-                );
-            }
-            getPage(null, []);
-        });
-    }
-
-    GET_SONG_FROM_SPOTIFY(payload) {
-        //song, cb
-        return new Promise(async (resolve, reject) => {
-            if (!config.get("apis.spotify.enabled"))
-                return reject(new Error("Spotify is not enabled."));
-
-            const song = Object.assign({}, payload.song);
-
-            const spotifyParams = [
-                `q=${encodeURIComponent(payload.song.title)}`,
-                `type=track`,
-            ].join("&");
-
-            const token = await this.spotify.runJob("GET_TOKEN", {});
-            const options = {
-                url: `https://api.spotify.com/v1/search?${spotifyParams}`,
-                headers: {
-                    Authorization: `Bearer ${token}`,
-                },
-            };
-
-            request(options, (err, res, body) => {
-                if (err) console.error(err);
-                body = JSON.parse(body);
-                if (body.error) console.error(body.error);
-
-                durationArtistLoop: for (let i in body) {
-                    let items = body[i].items;
-                    for (let j in items) {
-                        let item = items[j];
-                        let hasArtist = false;
-                        for (let k = 0; k < item.artists.length; k++) {
-                            let 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) => {
-                                return artist.name;
-                            });
-                            song.title = item.name;
-                            song.explicit = item.explicit;
-                            song.thumbnail = item.album.images[1].url;
-                            break durationArtistLoop;
-                        }
-                    }
-                }
-
-                resolve({ song });
-            });
-        });
-    }
-
-    GET_SONGS_FROM_SPOTIFY(payload) {
-        //title, artist, cb
-        return new Promise(async (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 token = await this.spotify.runJob("GET_TOKEN", {});
-            const options = {
-                url: `https://api.spotify.com/v1/search?${spotifyParams}`,
-                headers: {
-                    Authorization: `Bearer ${token}`,
-                },
-            };
-
-            request(options, (err, res, body) => {
-                if (err) return console.error(err);
-                body = JSON.parse(body);
-                if (body.error) return console.error(body.error);
-
-                let songs = [];
-
-                for (let i in body) {
-                    let items = body[i].items;
-                    for (let j in items) {
-                        let item = items[j];
-                        let hasArtist = false;
-                        for (let k = 0; k < item.artists.length; k++) {
-                            let 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)
-                        ) {
-                            let song = {};
-                            song.duration = item.duration_ms / 1000;
-                            song.artists = item.artists.map((artist) => {
-                                return artist.name;
-                            });
-                            song.title = item.name;
-                            song.explicit = item.explicit;
-                            song.thumbnail = item.album.images[1].url;
-                            songs.push(song);
-                        }
-                    }
-                }
-
-                resolve({ songs });
-            });
-        });
-    }
-
-    SHUFFLE(payload) {
-        //array
-        return new Promise((resolve, reject) => {
-            const array = payload.array.slice();
-
-            let currentIndex = payload.array.length,
-                temporaryValue,
-                randomIndex;
-
-            // While there remain elements to shuffle...
-            while (0 !== currentIndex) {
-                // Pick a remaining element...
-                randomIndex = Math.floor(Math.random() * currentIndex);
-                currentIndex -= 1;
-
-                // And swap it with the current element.
-                temporaryValue = array[currentIndex];
-                array[currentIndex] = array[randomIndex];
-                array[randomIndex] = temporaryValue;
-            }
-
-            resolve({ array });
-        });
-    }
-
-    GET_ERROR(payload) {
-        //err
-        return new Promise((resolve, reject) => {
-            let error = "An error occurred.";
-            if (typeof payload.error === "string") error = payload.error;
-            else if (payload.error.message) {
-                if (payload.error.message !== "Validation failed")
-                    error = payload.error.message;
-                else
-                    error =
-                        payload.error.errors[Object.keys(payload.error.errors)]
-                            .message;
-            }
-            resolve(error);
-        });
-    }
-
-    CREATE_GRAVATAR(payload) {
-        //email
-        return new Promise((resolve, reject) => {
-            const hash = crypto
-                .createHash("md5")
-                .update(payload.email)
-                .digest("hex");
-
-            resolve(`https://www.gravatar.com/avatar/${hash}`);
-        });
-    }
-
-    DEBUG(payload) {
-        return new Promise((resolve, reject) => {
-            resolve();
-        });
-    }
+	constructor() {
+		super("utils");
+
+		this.youtubeRequestCallbacks = [];
+		this.youtubeRequestsPending = 0;
+		this.youtubeRequestsActive = false;
+	}
+
+	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;
+
+			resolve();
+		});
+	}
+
+	PARSE_COOKIES(payload) {
+		// cookieString
+		return new Promise((resolve, reject) => {
+			const cookies = {};
+
+			if (typeof payload.cookieString !== "string") return reject(new Error("Cookie string is not a string"));
+
+			// eslint-disable-next-line array-callback-return
+			payload.cookieString.split("; ").map(cookie => {
+				cookies[cookie.substring(0, cookie.indexOf("="))] = cookie.substring(
+					cookie.indexOf("=") + 1,
+					cookie.length
+				);
+			});
+
+			return resolve(cookies);
+		});
+	}
+
+	// COOKIES_TO_STRING() {//cookies
+	// 	return new Promise((resolve, reject) => {
+	//         let newCookie = [];
+	//         for (let prop in cookie) {
+	//             newCookie.push(prop + "=" + cookie[prop]);
+	//         }
+	//         return newCookie.join("; ");
+	//     });
+	// }
+
+	REMOVE_COOKIE(payload) {
+		// cookieString, cookieName
+		return new Promise((resolve, reject) => {
+			let cookies;
+
+			try {
+				cookies = this.runJob("PARSE_COOKIES", {
+					cookieString: payload.cookieString
+				});
+			} catch (err) {
+				return reject(err);
+			}
+
+			delete cookies[payload.cookieName];
+
+			return resolve(this.toString(cookies));
+		});
+	}
+
+	HTML_ENTITIES(payload) {
+		// str
+		return new Promise(resolve => {
+			resolve(
+				String(payload.str)
+					.replace(/&/g, "&amp;")
+					.replace(/</g, "&lt;")
+					.replace(/>/g, "&gt;")
+					.replace(/"/g, "&quot;")
+			);
+		});
+	}
+
+	GENERATE_RANDOM_STRING(payload) {
+		// length
+		return new Promise((resolve, reject) => {
+			const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".split("");
+
+			let randomNum;
+
+			try {
+				randomNum = this.runJob("GET_RANDOM_NUMBER", {
+					min: 0,
+					max: chars.length - 1
+				});
+			} catch (err) {
+				return reject(err);
+			}
+
+			const result = [];
+			for (let i = 0; i < payload.length; i += 1) {
+				result.push(chars[randomNum]);
+			}
+
+			return resolve(result.join(""));
+		});
+	}
+
+	async GET_SOCKET_FROM_ID(payload) {
+		// socketId
+		const io = await this.io.runJob("IO", {});
+
+		return new Promise(resolve => resolve(io.sockets.sockets[payload.socketId]));
+	}
+
+	GET_RANDOM_NUMBER(payload) {
+		// min, max
+		return new Promise(resolve => {
+			resolve(Math.floor(Math.random() * (payload.max - payload.min + 1)) + payload.min);
+		});
+	}
+
+	CONVERT_TIME(payload) {
+		// duration
+		return new Promise(resolve => {
+			let { duration } = payload;
+			let a = duration.match(/\d+/g);
+
+			if (duration.indexOf("M") >= 0 && duration.indexOf("H") === -1 && duration.indexOf("S") === -1) {
+				a = [0, a[0], 0];
+			}
+
+			if (duration.indexOf("H") >= 0 && duration.indexOf("M") === -1) {
+				a = [a[0], 0, a[1]];
+			}
+			if (duration.indexOf("H") >= 0 && duration.indexOf("M") === -1 && duration.indexOf("S") === -1) {
+				a = [a[0], 0, 0];
+			}
+
+			duration = 0;
+
+			if (a.length === 3) {
+				duration += parseInt(a[0]) * 3600;
+				duration += parseInt(a[1]) * 60;
+				duration += parseInt(a[2]);
+			}
+
+			if (a.length === 2) {
+				duration += parseInt(a[0]) * 60;
+				duration += parseInt(a[1]);
+			}
+
+			if (a.length === 1) {
+				duration += parseInt(a[0]);
+			}
+
+			const hours = Math.floor(duration / 3600);
+			const minutes = Math.floor((duration % 3600) / 60);
+			const seconds = Math.floor((duration % 3600) % 60);
+
+			resolve(
+				(hours < 10 ? `0${hours}:` : `${hours}:`) +
+					(minutes < 10 ? `0${minutes}:` : `${minutes}:`) +
+					(seconds < 10 ? `0${seconds}` : seconds)
+			);
+		});
+	}
+
+	GUID() {
+		return new Promise(resolve => {
+			resolve(
+				[1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1]
+					.map(b =>
+						b
+							? Math.floor((1 + Math.random()) * 0x10000)
+									.toString(16)
+									.substring(1)
+							: "-"
+					)
+					.join("")
+			);
+		});
+	}
+
+	async SOCKET_FROM_SESSION(payload) {
+		// socketId
+		const io = await this.io.runJob("IO", {});
+
+		return new Promise((resolve, reject) => {
+			const ns = io.of("/");
+			if (ns) {
+				return resolve(ns.connected[payload.socketId]);
+			}
+
+			return reject();
+		});
+	}
+
+	async SOCKETS_FROM_SESSION_ID(payload) {
+		const io = await this.io.runJob("IO", {});
+
+		return new Promise(resolve => {
+			const ns = io.of("/");
+			const sockets = [];
+
+			if (ns) {
+				return async.each(
+					Object.keys(ns.connected),
+					(id, next) => {
+						const { session } = ns.connected[id];
+						if (session.sessionId === payload.sessionId) sockets.push(session.sessionId);
+						next();
+					},
+					() => {
+						resolve({ sockets });
+					}
+				);
+			}
+
+			return resolve();
+		});
+	}
+
+	async SOCKETS_FROM_USER(payload) {
+		const io = await this.io.runJob("IO", {});
+
+		return new Promise((resolve, reject) => {
+			const ns = io.of("/");
+			const sockets = [];
+
+			if (ns) {
+				return async.each(
+					Object.keys(ns.connected),
+					(id, next) => {
+						const { session } = ns.connected[id];
+						this.cache
+							.runJob("HGET", {
+								table: "sessions",
+								key: session.sessionId
+							})
+							.then(session => {
+								if (session && session.userId === payload.userId) sockets.push(ns.connected[id]);
+								next();
+							})
+							.catch(err => {
+								next(err);
+							});
+					},
+					err => {
+						if (err) return reject(err);
+						return resolve({ sockets });
+					}
+				);
+			}
+
+			return resolve();
+		});
+	}
+
+	async SOCKETS_FROM_IP(payload) {
+		const io = await this.io.runJob("IO", {});
+
+		return new Promise(resolve => {
+			const ns = io.of("/");
+			const sockets = [];
+			if (ns) {
+				return async.each(
+					Object.keys(ns.connected),
+					(id, next) => {
+						const { session } = ns.connected[id];
+						this.cache
+							.runJob("HGET", {
+								table: "sessions",
+								key: session.sessionId
+							})
+							.then(session => {
+								if (session && ns.connected[id].ip === payload.ip) sockets.push(ns.connected[id]);
+								next();
+							})
+							.catch(() => next());
+					},
+					() => {
+						resolve({ sockets });
+					}
+				);
+			}
+
+			return resolve();
+		});
+	}
+
+	async SOCKETS_FROM_USER_WITHOUT_CACHE(payload) {
+		const io = await this.io.runJob("IO", {});
+
+		return new Promise(resolve => {
+			const ns = io.of("/");
+			const sockets = [];
+
+			if (ns) {
+				return async.each(
+					Object.keys(ns.connected),
+					(id, next) => {
+						const { session } = ns.connected[id];
+						if (session.userId === payload.userId) sockets.push(ns.connected[id]);
+						next();
+					},
+					() => {
+						resolve({ sockets });
+					}
+				);
+			}
+
+			return resolve();
+		});
+	}
+
+	SOCKET_LEAVE_ROOMS(payload) {
+		// socketId
+		return new Promise((resolve, reject) => {
+			let socket;
+
+			try {
+				socket = this.runJob("SOCKET_FROM_SESSION", {
+					socketId: payload.socketId
+				});
+			} catch (err) {
+				return reject(err);
+			}
+
+			const { rooms } = socket;
+			for (let room = 0, roomKeys = Object.keys(rooms); room < roomKeys.length; room += 1) {
+				socket.leave(room);
+			}
+
+			return resolve();
+		});
+	}
+
+	async SOCKET_JOIN_ROOM(payload) {
+		const socket = await this.runJob("SOCKET_FROM_SESSION", {
+			socketId: payload.socketId
+		});
+
+		// socketId, room
+		return new Promise(resolve => {
+			const { rooms } = socket;
+			for (let room = 0, roomKeys = Object.keys(rooms); room < roomKeys.length; room += 1) {
+				socket.leave(room);
+			}
+
+			socket.join(payload.room);
+
+			return resolve();
+		});
+	}
+
+	async SOCKET_JOIN_SONG_ROOM(payload) {
+		// socketId, room
+		const socket = await this.runJob("SOCKET_FROM_SESSION", {
+			socketId: payload.socketId
+		});
+
+		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);
+			}
+
+			socket.join(payload.room);
+
+			return resolve();
+		});
+	}
+
+	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]];
+
+				const { rooms } = socket;
+				for (let room = 0, roomKeys = Object.keys(rooms); room < roomKeys.length; room += 1) {
+					if (room.indexOf("song.") !== -1) socket.leave(room);
+				}
+
+				socket.join(payload.room);
+			}
+
+			return resolve();
+		});
+	}
+
+	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]];
+				const { rooms } = socket;
+				for (let room = 0, roomKeys = Object.keys(rooms); room < roomKeys.length; room += 1) {
+					if (room.indexOf("song.") !== -1) socket.leave(room);
+				}
+			}
+			resolve();
+		});
+	}
+
+	async EMIT_TO_ROOM(payload) {
+		// room, ...args
+		const io = await this.io.runJob("IO", {});
+
+		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]];
+				if (socket.rooms[payload.room]) {
+					socket.emit(...payload.args);
+				}
+			}
+
+			return resolve();
+		});
+	}
+
+	async GET_ROOM_SOCKETS(payload) {
+		const io = await this.io.runJob("IO", {});
+
+		// room
+		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]];
+				if (socket.rooms[payload.room]) roomSockets.push(socket);
+			}
+
+			return resolve(roomSockets);
+		});
+	}
+
+	GET_SONG_FROM_YOUTUBE(payload) {
+		// songId, cb
+		return new Promise((resolve, reject) => {
+			this.youtubeRequestCallbacks.push({
+				cb: () => {
+					this.youtubeRequestsActive = true;
+					const youtubeParams = [
+						"part=snippet,contentDetails,statistics,status",
+						`id=${encodeURIComponent(payload.songId)}`,
+						`key=${config.get("apis.youtube.key")}`
+					].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;
+
+						if (err) {
+							console.error(err);
+							return null;
+						}
+
+						body = JSON.parse(body);
+
+						if (body.error) {
+							console.log("ERROR", "GET_SONG_FROM_YOUTUBE", `${body.error.message}`);
+							return reject(new Error("An error has occured. Please try again later."));
+						}
+
+						if (body.items[0] === undefined)
+							return reject(
+								new Error("The specified video does not exist or cannot be publicly accessed.")
+							);
+
+						// TODO Clean up duration converter
+						let dur = body.items[0].contentDetails.duration;
+
+						dur = dur.replace("PT", "");
+
+						let duration = 0;
+
+						dur = dur.replace(/([\d]*)H/, (v, v2) => {
+							v2 = Number(v2);
+							duration = v2 * 60 * 60;
+							return "";
+						});
+
+						dur = dur.replace(/([\d]*)M/, (v, v2) => {
+							v2 = Number(v2);
+							duration += v2 * 60;
+							return "";
+						});
+
+						// eslint-disable-next-line no-unused-vars
+						dur = dur.replace(/([\d]*)S/, (v, v2) => {
+							v2 = Number(v2);
+							duration += v2;
+							return "";
+						});
+
+						const song = {
+							songId: body.items[0].id,
+							title: body.items[0].snippet.title,
+							duration
+						};
+
+						return resolve({ song });
+					});
+				},
+				songId: payload.songId
+			});
+
+			if (!this.youtubeRequestsActive) {
+				this.youtubeRequestCallbacks[0].cb(this.youtubeRequestCallbacks[0].songId);
+			}
+		});
+	}
+
+	FILTER_MUSIC_VIDEOS_YOUTUBE(payload) {
+		// videoIds, cb
+		return new Promise((resolve, reject) => {
+			/**
+			 * @param {Function} cb2 - callback
+			 */
+			function getNextPage(cb2) {
+				const localVideoIds = payload.videoIds.splice(0, 50);
+
+				const youtubeParams = [
+					"part=topicDetails",
+					`id=${encodeURIComponent(localVideoIds.join(","))}`,
+					`maxResults=50`,
+					`key=${config.get("apis.youtube.key")}`
+				].join("&");
+
+				request(`https://www.googleapis.com/youtube/v3/videos?${youtubeParams}`, (err, res, body) => {
+					if (err) {
+						console.error(err);
+						return reject(new Error("Failed to find playlist from YouTube"));
+					}
+
+					body = JSON.parse(body);
+
+					if (body.error) {
+						console.log("ERROR", "FILTER_MUSIC_VIDEOS_YOUTUBE", `${body.error.message}`);
+						return reject(new Error("An error has occured. Please try again later."));
+					}
+
+					const songIds = [];
+					body.items.forEach(item => {
+						const songId = item.id;
+						if (!item.topicDetails) return;
+						if (item.topicDetails.relevantTopicIds.indexOf("/m/04rlf") !== -1) {
+							songIds.push(songId);
+						}
+					});
+
+					if (payload.videoIds.length > 0) {
+						return getNextPage(newSongIds => {
+							cb2(songIds.concat(newSongIds));
+						});
+					}
+
+					return cb2(songIds);
+				});
+			}
+
+			if (payload.videoIds.length === 0) resolve({ songIds: [] });
+			else getNextPage(songIds => resolve({ songIds }));
+		});
+	}
+
+	GET_PLAYLIST_FROM_YOUTUBE(payload) {
+		// payload includes: url, musicOnly
+		return new Promise((resolve, reject) => {
+			const local = this;
+
+			const name = "list".replace(/[\\[]/, "\\[").replace(/[\]]/, "\\]");
+
+			const regex = new RegExp(`[\\?&]${name}=([^&#]*)`);
+			const splitQuery = regex.exec(payload.url);
+
+			if (!splitQuery) {
+				console.log("ERROR", "GET_PLAYLIST_FROM_YOUTUBE", "Invalid YouTube playlist URL query.");
+				return reject(new Error("An error has occured. Please try again later."));
+			}
+
+			const playlistId = splitQuery[1];
+
+			/**
+			 * @param {string} pageToken - page token for YouTube API
+			 * @param {Array} songs - array of sogns
+			 */
+			function getPage(pageToken, songs) {
+				const nextPageToken = pageToken ? `pageToken=${pageToken}` : "";
+				const youtubeParams = [
+					"part=contentDetails",
+					`playlistId=${encodeURIComponent(playlistId)}`,
+					`maxResults=50`,
+					`key=${config.get("apis.youtube.key")}`,
+					nextPageToken
+				].join("&");
+
+				request(
+					`https://www.googleapis.com/youtube/v3/playlistItems?${youtubeParams}`,
+					async (err, res, body) => {
+						if (err) {
+							console.error(err);
+							return reject(new Error("Failed to find playlist from YouTube"));
+						}
+
+						body = JSON.parse(body);
+
+						if (body.error) {
+							console.log("ERROR", "GET_PLAYLIST_FROM_YOUTUBE", `${body.error.message}`);
+							return reject(new Error("An error has occured. Please try again later."));
+						}
+
+						songs = songs.concat(body.items);
+
+						if (body.nextPageToken) return getPage(body.nextPageToken, songs);
+
+						songs = songs.map(song => song.contentDetails.videoId);
+
+						if (!payload.musicOnly) return resolve({ songs });
+						return local
+							.runJob("FILTER_MUSIC_VIDEOS_YOUTUBE", {
+								videoIds: songs.slice()
+							})
+							.then(filteredSongs => {
+								resolve({ filteredSongs, songs });
+							});
+					}
+				);
+			}
+
+			return getPage(null, []);
+		});
+	}
+
+	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 });
+			});
+		});
+	}
+
+	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 });
+			});
+		});
+	}
+
+	SHUFFLE(payload) {
+		// array
+		return new Promise(resolve => {
+			const array = payload.array.slice();
+
+			let currentIndex = payload.array.length;
+			let temporaryValue;
+			let randomIndex;
+
+			// While there remain elements to shuffle...
+			while (currentIndex !== 0) {
+				// Pick a remaining element...
+				randomIndex = Math.floor(Math.random() * currentIndex);
+				currentIndex -= 1;
+
+				// And swap it with the current element.
+				temporaryValue = array[currentIndex];
+				array[currentIndex] = array[randomIndex];
+				array[randomIndex] = temporaryValue;
+			}
+
+			resolve({ array });
+		});
+	}
+
+	GET_ERROR(payload) {
+		// err
+		return new Promise(resolve => {
+			let error = "An error occurred.";
+			if (typeof payload.error === "string") error = payload.error;
+			else if (payload.error.message) {
+				if (payload.error.message !== "Validation failed") error = payload.error.message;
+				else error = payload.error.errors[Object.keys(payload.error.errors)].message;
+			}
+			resolve(error);
+		});
+	}
+
+	CREATE_GRAVATAR(payload) {
+		// email
+		return new Promise(resolve => {
+			const hash = crypto.createHash("md5").update(payload.email).digest("hex");
+
+			resolve(`https://www.gravatar.com/avatar/${hash}`);
+		});
+	}
+
+	DEBUG() {
+		return new Promise(resolve => resolve());
+	}
 }
 
-module.exports = new UtilsModule();
+export default new UtilsModule();

File diff suppressed because it is too large
+ 9170 - 2223
backend/package-lock.json


+ 12 - 3
backend/package.json

@@ -2,15 +2,17 @@
   "name": "musare-backend",
   "private": true,
   "version": "2.1.0",
+  "type": "module",
   "description": "A modern, open-source, collaborative music app https://musare.com",
   "main": "index.js",
   "author": "Musare Team",
   "license": "GPL-3.0",
   "repository": "https://github.com/Musare/MusareNode",
   "scripts": {
-    "dev": "nodemon",
-    "docker:dev": "nodemon -L /opt/app",
-    "docker:prod": "node /opt/app"
+    "dev": "nodemon --es-module-specifier-resolution=node",
+    "docker:dev": "nodemon --es-module-specifier-resolution=node -L /opt/app",
+    "docker:prod": "node --es-module-specifier-resolution=node /opt/app",
+    "lint": "npx eslint logic"
   },
   "dependencies": {
     "async": "3.1.0",
@@ -33,6 +35,13 @@
     "underscore": "^1.10.2"
   },
   "devDependencies": {
+    "eslint": "^7.16.0",
+    "eslint-config-airbnb-base": "^14.2.1",
+    "eslint-config-prettier": "^7.1.0",
+    "eslint-plugin-import": "^2.22.1",
+    "eslint-plugin-jsdoc": "^30.7.9",
+    "eslint-plugin-prettier": "^3.3.0",
+    "prettier": "^2.2.1",
     "trace-unhandled": "^1.2.1"
   }
 }

+ 1 - 0
frontend/src/api/auth.js

@@ -54,6 +54,7 @@ export default {
 
 			io.getSocket(socket => {
 				socket.emit("users.login", email, password, res => {
+					console.log(123, res);
 					if (res.status === "success") {
 						return lofig.get("cookie").then(cookie => {
 							const date = new Date();

Some files were not shown because too many files changed in this diff