Selaa lähdekoodia

Merge pull request #66 from Musare/refactor-websockets

Refactor websockets
Jonathan Graham 4 vuotta sitten
vanhempi
sitoutus
4c6f5bb1ba
76 muutettua tiedostoa jossa 2768 lisäystä ja 3358 poistoa
  1. 3 2
      README.md
  2. 2 2
      backend/index.js
  3. 8 8
      backend/logic/actions/activities.js
  4. 3 3
      backend/logic/actions/apis.js
  5. 11 10
      backend/logic/actions/news.js
  6. 49 52
      backend/logic/actions/playlists.js
  7. 6 6
      backend/logic/actions/punishments.js
  8. 13 17
      backend/logic/actions/queueSongs.js
  9. 8 8
      backend/logic/actions/reports.js
  10. 34 37
      backend/logic/actions/songs.js
  11. 77 82
      backend/logic/actions/stations.js
  12. 69 80
      backend/logic/actions/users.js
  13. 14 14
      backend/logic/activities.js
  14. 3 2
      backend/logic/app.js
  15. 31 47
      backend/logic/stations.js
  16. 8 5
      backend/logic/tasks.js
  17. 1 20
      backend/logic/utils.js
  18. 271 356
      backend/logic/ws.js
  19. 24 575
      backend/package-lock.json
  20. 2 2
      backend/package.json
  21. 3 2
      frontend/dist/config/template.json
  22. 0 1
      frontend/dist/index.tpl.html
  23. 0 5
      frontend/dist/vendor/socket.io.2.2.0.js
  24. 36 37
      frontend/src/App.vue
  25. 3 1
      frontend/src/api/admin/index.js
  26. 11 11
      frontend/src/api/admin/reports.js
  27. 64 69
      frontend/src/api/auth.js
  28. 17 17
      frontend/src/components/modals/AddSongToQueue.vue
  29. 5 8
      frontend/src/components/modals/CreateCommunityStation.vue
  30. 21 20
      frontend/src/components/modals/CreatePlaylist.vue
  31. 32 29
      frontend/src/components/modals/EditNews.vue
  32. 80 80
      frontend/src/components/modals/EditPlaylist/index.vue
  33. 142 151
      frontend/src/components/modals/EditSong.vue
  34. 98 100
      frontend/src/components/modals/EditStation.vue
  35. 23 27
      frontend/src/components/modals/EditUser.vue
  36. 3 3
      frontend/src/components/modals/Login.vue
  37. 3 3
      frontend/src/components/modals/Register.vue
  38. 5 7
      frontend/src/components/modals/Report.vue
  39. 22 26
      frontend/src/components/modals/ViewPunishment.vue
  40. 18 23
      frontend/src/components/modals/ViewReport.vue
  41. 24 28
      frontend/src/components/modals/WhatIsNew.vue
  42. 29 30
      frontend/src/components/ui/AddToPlaylistDropdown.vue
  43. 0 103
      frontend/src/io.js
  44. 104 91
      frontend/src/main.js
  45. 2 2
      frontend/src/mixins/SearchYoutube.vue
  46. 2 2
      frontend/src/mixins/SortablePlaylists.vue
  47. 10 8
      frontend/src/pages/Admin/tabs/NewStatistics.vue
  48. 39 35
      frontend/src/pages/Admin/tabs/News.vue
  49. 9 9
      frontend/src/pages/Admin/tabs/Playlists.vue
  50. 14 13
      frontend/src/pages/Admin/tabs/Punishments.vue
  51. 26 30
      frontend/src/pages/Admin/tabs/QueueSongs.vue
  52. 30 29
      frontend/src/pages/Admin/tabs/Reports.vue
  53. 19 24
      frontend/src/pages/Admin/tabs/Songs.vue
  54. 18 19
      frontend/src/pages/Admin/tabs/Stations.vue
  55. 8 7
      frontend/src/pages/Admin/tabs/Statistics.vue
  56. 9 9
      frontend/src/pages/Admin/tabs/Users.vue
  57. 153 153
      frontend/src/pages/Home.vue
  58. 21 22
      frontend/src/pages/News.vue
  59. 22 24
      frontend/src/pages/Profile/index.vue
  60. 70 66
      frontend/src/pages/Profile/tabs/Playlists.vue
  61. 42 34
      frontend/src/pages/Profile/tabs/RecentActivity.vue
  62. 8 11
      frontend/src/pages/ResetPassword.vue
  63. 41 44
      frontend/src/pages/Settings/index.vue
  64. 16 17
      frontend/src/pages/Settings/tabs/Account.vue
  65. 23 23
      frontend/src/pages/Settings/tabs/Preferences.vue
  66. 15 16
      frontend/src/pages/Settings/tabs/Profile.vue
  67. 9 16
      frontend/src/pages/Settings/tabs/Security.vue
  68. 66 66
      frontend/src/pages/Station/components/Sidebar/MyPlaylists.vue
  69. 1 1
      frontend/src/pages/Station/components/Sidebar/Queue/index.vue
  70. 488 446
      frontend/src/pages/Station/index.vue
  71. 4 0
      frontend/src/store/index.js
  72. 5 9
      frontend/src/store/modules/admin.js
  73. 1 0
      frontend/src/store/modules/modals/viewReport.js
  74. 19 23
      frontend/src/store/modules/user.js
  75. 42 0
      frontend/src/store/modules/websockets.js
  76. 156 0
      frontend/src/ws.js

+ 3 - 2
README.md

@@ -2,7 +2,7 @@
 
 Based off of the original [Musare](https://github.com/Musare/MusareMeteor), which utilized Meteor.
 
-MusareNode now uses NodeJS, Express, SocketIO and VueJS - among other technologies. We have also implemented the ability to host Musare in [Docker Containers](https://www.docker.com/).
+MusareNode now uses NodeJS, Express, VueJS and websockets - among other technologies. We have also implemented the ability to host Musare in [Docker Containers](https://www.docker.com/).
 
 The master branch is available at [musare.com](https://musare.com)
 You can also find the staging branch at [musare.dev](https://musare.dev)
@@ -68,7 +68,8 @@ We currently only utilize 1 backend, 1 MongoDB server and 1 Redis server running
 
     | Property | Description |
     | - | - |
-    | `serverDomain` | Should be the url where the backend will be accessible from, usually `http://localhost:8080` for non-Docker. |
+    | `apiDomain` | Should be the url where the backend will be accessible from, usually `http://localhost:8080` for non-Docker. |
+    | `websocketsDomain` | Should be the same as the `serverDomain`, except using the `ws://` protocol instead of `http://` and with `/ws` at the end. |
     | `frontendDomain` | Should be the url where the frontend will be accessible from, usually `http://localhost` for docker or `http://localhost:80` for non-Docker. |
     | `frontendPort` | Should be the port where the frontend will be accessible from, should always be port `81` for Docker, and is recommended to be port `80` for non-Docker. |
     | `recaptcha.key` | Can be obtained by setting up a [ReCaptcha Site (v3)](https://www.google.com/recaptcha/admin). |

+ 2 - 2
backend/index.js

@@ -196,7 +196,7 @@ if (config.debug && config.debug.traceUnhandledPromises === true) {
 // moduleManager.addModule("mail");
 // moduleManager.addModule("api");
 // moduleManager.addModule("app");
-// moduleManager.addModule("io");
+// moduleManager.addModule("ws");
 // moduleManager.addModule("logger");
 // moduleManager.addModule("notifications");
 // moduleManager.addModule("activities");
@@ -381,7 +381,7 @@ if (!config.get("migration")) {
 	moduleManager.addModule("activities");
 	moduleManager.addModule("api");
 	moduleManager.addModule("app");
-	moduleManager.addModule("io");
+	moduleManager.addModule("ws");
 	moduleManager.addModule("notifications");
 	moduleManager.addModule("playlists");
 	moduleManager.addModule("punishments");

+ 8 - 8
backend/logic/actions/activities.js

@@ -6,17 +6,17 @@ import moduleManager from "../../index";
 
 const DBModule = moduleManager.modules.db;
 const CacheModule = moduleManager.modules.cache;
-const IOModule = moduleManager.modules.io;
+const WSModule = moduleManager.modules.ws;
 const UtilsModule = moduleManager.modules.utils;
 
 CacheModule.runJob("SUB", {
 	channel: "activity.removeAllForUser",
 	cb: userId => {
-		IOModule.runJob("SOCKETS_FROM_USER", { userId }, this).then(res =>
-			res.sockets.forEach(socket => socket.emit("event:activity.removeAllForUser"))
+		WSModule.runJob("SOCKETS_FROM_USER", { userId }, this).then(sockets =>
+			sockets.forEach(socket => socket.dispatch("event:activity.removeAllForUser"))
 		);
 
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `profile-${userId}-activities`,
 			args: ["event:activity.removeAllForUser"]
 		});
@@ -26,11 +26,11 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "activity.hide",
 	cb: res => {
-		IOModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(response =>
-			response.sockets.forEach(socket => socket.emit("event:activity.hide", res.activityId))
+		WSModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(sockets =>
+			sockets.forEach(socket => socket.dispatch("event:activity.hide", res.activityId))
 		);
 
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `profile-${res.userId}-activities`,
 			args: ["event:activity.hide", res.activityId]
 		});
@@ -41,7 +41,7 @@ export default {
 	/**
 	 * Returns how many activities there are for a user
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} userId - the id of the user in question
 	 * @param {Function} cb - callback
 	 */

+ 3 - 3
backend/logic/actions/apis.js

@@ -7,7 +7,7 @@ import { isAdminRequired } from "./hooks";
 import moduleManager from "../../index";
 
 const UtilsModule = moduleManager.modules.utils;
-const IOModule = moduleManager.modules.io;
+const WSModule = moduleManager.modules.ws;
 const YouTubeModule = moduleManager.modules.youtube;
 
 export default {
@@ -122,7 +122,7 @@ export default {
 	 */
 	joinRoom(session, page, cb) {
 		if (page === "home" || page.startsWith("profile-")) {
-			IOModule.runJob("SOCKET_JOIN_ROOM", {
+			WSModule.runJob("SOCKET_JOIN_ROOM", {
 				socketId: session.socketId,
 				room: page
 			})
@@ -152,7 +152,7 @@ export default {
 			page === "statistics" ||
 			page === "punishments"
 		) {
-			IOModule.runJob("SOCKET_JOIN_ROOM", {
+			WSModule.runJob("SOCKET_JOIN_ROOM", {
 				socketId: session.socketId,
 				room: `admin.${page}`
 			});

+ 11 - 10
backend/logic/actions/news.js

@@ -6,13 +6,13 @@ import moduleManager from "../../index";
 
 const DBModule = moduleManager.modules.db;
 const UtilsModule = moduleManager.modules.utils;
-const IOModule = moduleManager.modules.io;
+const WSModule = moduleManager.modules.ws;
 const CacheModule = moduleManager.modules.cache;
 
 CacheModule.runJob("SUB", {
 	channel: "news.create",
 	cb: news => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: "admin.news",
 			args: ["event:admin.news.created", news]
 		});
@@ -22,7 +22,7 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "news.remove",
 	cb: news => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: "admin.news",
 			args: ["event:admin.news.removed", news]
 		});
@@ -32,7 +32,7 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "news.update",
 	cb: news => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: "admin.news",
 			args: ["event:admin.news.updated", news]
 		});
@@ -43,7 +43,7 @@ export default {
 	/**
 	 * Gets all news items
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
 	async index(session, cb) {
@@ -69,7 +69,7 @@ export default {
 	/**
 	 * Gets a news item by id
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} newsId - the news id
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -95,7 +95,7 @@ export default {
 	/**
 	 * Creates a news item
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {object} data - the object of the news data
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -128,7 +128,7 @@ export default {
 	/**
 	 * Gets the latest news item
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
 	async newest(session, cb) {
@@ -145,6 +145,7 @@ export default {
 					this.log("ERROR", "NEWS_NEWEST", `Getting the latest news failed. "${err}"`);
 					return cb({ status: "failure", message: err });
 				}
+
 				this.log("SUCCESS", "NEWS_NEWEST", `Successfully got the latest news.`, false);
 				return cb({ status: "success", data: news });
 			}
@@ -154,7 +155,7 @@ export default {
 	/**
 	 * Removes a news item
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {object} news - the news object
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -184,7 +185,7 @@ export default {
 	/**
 	 * Removes a news item
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} _id - the news id
 	 * @param {object} news - the news object
 	 * @param {Function} cb - gets called with the result

+ 49 - 52
backend/logic/actions/playlists.js

@@ -6,7 +6,7 @@ import moduleManager from "../../index";
 
 const DBModule = moduleManager.modules.db;
 const UtilsModule = moduleManager.modules.utils;
-const IOModule = moduleManager.modules.io;
+const WSModule = moduleManager.modules.ws;
 const SongsModule = moduleManager.modules.songs;
 const CacheModule = moduleManager.modules.cache;
 const PlaylistsModule = moduleManager.modules.playlists;
@@ -16,14 +16,12 @@ const ActivitiesModule = moduleManager.modules.activities;
 CacheModule.runJob("SUB", {
 	channel: "playlist.create",
 	cb: playlist => {
-		IOModule.runJob("SOCKETS_FROM_USER", { userId: playlist.createdBy }, this).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("event:playlist.create", playlist);
-			});
+		WSModule.runJob("SOCKETS_FROM_USER", { userId: playlist.createdBy }, this).then(sockets => {
+			sockets.forEach(socket => socket.dispatch("event:playlist.create", playlist));
 		});
 
 		if (playlist.privacy === "public")
-			IOModule.runJob("EMIT_TO_ROOM", {
+			WSModule.runJob("EMIT_TO_ROOM", {
 				room: `profile-${playlist.createdBy}-playlists`,
 				args: ["event:playlist.create", playlist]
 			});
@@ -33,13 +31,13 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "playlist.delete",
 	cb: res => {
-		IOModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("event:playlist.delete", res.playlistId);
+		WSModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("event:playlist.delete", res.playlistId);
 			});
 		});
 
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `profile-${res.userId}-playlists`,
 			args: ["event:playlist.delete", res.playlistId]
 		});
@@ -49,9 +47,9 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "playlist.repositionSongs",
 	cb: res => {
-		IOModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(response =>
-			response.sockets.forEach(socket =>
-				socket.emit("event:playlist.repositionSongs", {
+		WSModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(sockets =>
+			sockets.forEach(socket =>
+				socket.dispatch("event:playlist.repositionSongs", {
 					playlistId: res.playlistId,
 					songsBeingChanged: res.songsBeingChanged
 				})
@@ -63,9 +61,9 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "playlist.addSong",
 	cb: res => {
-		IOModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("event:playlist.addSong", {
+		WSModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("event:playlist.addSong", {
 					playlistId: res.playlistId,
 					song: res.song
 				});
@@ -73,7 +71,7 @@ CacheModule.runJob("SUB", {
 		});
 
 		if (res.privacy === "public")
-			IOModule.runJob("EMIT_TO_ROOM", {
+			WSModule.runJob("EMIT_TO_ROOM", {
 				room: `profile-${res.userId}-playlists`,
 				args: [
 					"event:playlist.addSong",
@@ -89,9 +87,9 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "playlist.removeSong",
 	cb: res => {
-		IOModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("event:playlist.removeSong", {
+		WSModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("event:playlist.removeSong", {
 					playlistId: res.playlistId,
 					songId: res.songId
 				});
@@ -99,7 +97,7 @@ CacheModule.runJob("SUB", {
 		});
 
 		if (res.privacy === "public")
-			IOModule.runJob("EMIT_TO_ROOM", {
+			WSModule.runJob("EMIT_TO_ROOM", {
 				room: `profile-${res.userId}-playlists`,
 				args: [
 					"event:playlist.removeSong",
@@ -115,9 +113,9 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "playlist.updateDisplayName",
 	cb: res => {
-		IOModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("event:playlist.updateDisplayName", {
+		WSModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("event:playlist.updateDisplayName", {
 					playlistId: res.playlistId,
 					displayName: res.displayName
 				});
@@ -125,7 +123,7 @@ CacheModule.runJob("SUB", {
 		});
 
 		if (res.privacy === "public")
-			IOModule.runJob("EMIT_TO_ROOM", {
+			WSModule.runJob("EMIT_TO_ROOM", {
 				room: `profile-${res.userId}-playlists`,
 				args: [
 					"event:playlist.updateDisplayName",
@@ -141,21 +139,21 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "playlist.updatePrivacy",
 	cb: res => {
-		IOModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("event:playlist.updatePrivacy", {
+		WSModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("event:playlist.updatePrivacy", {
 					playlist: res.playlist
 				});
 			});
 		});
 
 		if (res.playlist.privacy === "public")
-			return IOModule.runJob("EMIT_TO_ROOM", {
+			return WSModule.runJob("EMIT_TO_ROOM", {
 				room: `profile-${res.userId}-playlists`,
 				args: ["event:playlist.create", res.playlist]
 			});
 
-		return IOModule.runJob("EMIT_TO_ROOM", {
+		return WSModule.runJob("EMIT_TO_ROOM", {
 			room: `profile-${res.userId}-playlists`,
 			args: ["event:playlist.delete", res.playlist._id]
 		});
@@ -166,7 +164,7 @@ export default {
 	/**
 	 * Gets all playlists
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
 	index: isAdminRequired(async function index(session, cb) {
@@ -193,7 +191,7 @@ export default {
 	/**
 	 * Gets the first song from a private playlist
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} playlistId - the id of the playlist we are getting the first song from
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -237,7 +235,7 @@ export default {
 	/**
 	 * Gets a list of all the playlists for a specific user
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} userId - the user id in question
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -310,7 +308,7 @@ export default {
 	/**
 	 * Gets all playlists for the user requesting it
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {boolean} showNonModifiablePlaylists - whether or not to show non modifiable playlists e.g. liked songs
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -373,7 +371,7 @@ export default {
 	/**
 	 * Creates a new private playlist
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {object} data - the data for the new private playlist
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -463,7 +461,7 @@ export default {
 	/**
 	 * Gets a playlist from id
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} playlistId - the id of the playlist we are getting
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -512,7 +510,7 @@ export default {
 	/**
 	 * Obtains basic metadata of a playlist in order to format an activity
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} playlistId - the playlist id
 	 * @param {Function} cb - callback
 	 */
@@ -556,7 +554,7 @@ export default {
 	/**
 	 * Updates a private playlist
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @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
@@ -616,7 +614,7 @@ export default {
 	/**
 	 * Shuffles songs in a private playlist
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} playlistId - the id of the playlist we are updating
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -677,7 +675,7 @@ export default {
 	/**
 	 * Changes the order of song(s) in a private playlist
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} playlistId - the id of the playlist we are targeting
 	 * @param {Array} songsBeingChanged - the songs to be repositioned, each element contains "songId" and "position" properties
 	 * @param {Function} cb - gets called with the result
@@ -753,7 +751,7 @@ export default {
 	/**
 	 * 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 {object} session - the session object automatically added by the websocket
 	 * @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
@@ -793,7 +791,7 @@ export default {
 					});
 
 					// update position property on songs that need to be changed
-					return IOModule.runJob(
+					return WSModule.runJob(
 						"RUN_ACTION2",
 						{
 							session,
@@ -838,7 +836,7 @@ export default {
 	/**
 	 * Adds a song to a private playlist
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @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
@@ -957,7 +955,7 @@ export default {
 	/**
 	 * Adds a set of songs to a private playlist
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @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
@@ -996,7 +994,7 @@ export default {
 						songIds,
 						1,
 						(songId, next) => {
-							IOModule.runJob(
+							WSModule.runJob(
 								"RUN_ACTION2",
 								{
 									session,
@@ -1080,7 +1078,7 @@ export default {
 	/**
 	 * Removes a song from a private playlist
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @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
@@ -1126,7 +1124,7 @@ export default {
 					});
 
 					// update position property on songs that need to be changed
-					return IOModule.runJob(
+					return WSModule.runJob(
 						"RUN_ACTION2",
 						{
 							session,
@@ -1225,7 +1223,7 @@ export default {
 	/**
 	 * Updates the displayName of a private playlist
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} playlistId - the id of the playlist we are updating the displayName for
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -1307,7 +1305,7 @@ export default {
 	/**
 	 * Removes a private playlist
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @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
 	 */
@@ -1416,8 +1414,7 @@ export default {
 					userId: playlist.createdBy,
 					type: "playlist__remove",
 					payload: {
-						message: `Removed playlist <playlistId>${playlist.displayName}</playlistId>`,
-						playlistId
+						message: `Removed playlist ${playlist.displayName}`
 					}
 				});
 
@@ -1432,7 +1429,7 @@ export default {
 	/**
 	 * Updates the privacy of a private playlist
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} playlistId - the id of the playlist we are updating the privacy for
 	 * @param {string} privacy - what the new privacy of the playlist should be e.g. public
 	 * @param {Function} cb - gets called with the result

+ 6 - 6
backend/logic/actions/punishments.js

@@ -6,19 +6,19 @@ import moduleManager from "../../index";
 
 const DBModule = moduleManager.modules.db;
 const UtilsModule = moduleManager.modules.utils;
-const IOModule = moduleManager.modules.io;
+const WSModule = moduleManager.modules.ws;
 const CacheModule = moduleManager.modules.cache;
 const PunishmentsModule = moduleManager.modules.punishments;
 
 CacheModule.runJob("SUB", {
 	channel: "ip.ban",
 	cb: data => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: "admin.punishments",
 			args: ["event:admin.punishment.added", data.punishment]
 		});
 
-		IOModule.runJob("SOCKETS_FROM_IP", { ip: data.ip }, this).then(sockets => {
+		WSModule.runJob("SOCKETS_FROM_IP", { ip: data.ip }, this).then(sockets => {
 			sockets.forEach(socket => {
 				socket.disconnect(true);
 			});
@@ -30,7 +30,7 @@ export default {
 	/**
 	 * Gets all punishments
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
 	index: isAdminRequired(async function index(session, cb) {
@@ -62,7 +62,7 @@ export default {
 	/**
 	 * Gets a punishment by id
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} punishmentId - the punishment id
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -99,7 +99,7 @@ export default {
 	/**
 	 * Bans an IP address
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @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

+ 13 - 17
backend/logic/actions/queueSongs.js

@@ -8,7 +8,7 @@ import moduleManager from "../../index";
 
 const DBModule = moduleManager.modules.db;
 const UtilsModule = moduleManager.modules.utils;
-const IOModule = moduleManager.modules.io;
+const WSModule = moduleManager.modules.ws;
 const YouTubeModule = moduleManager.modules.youtube;
 const CacheModule = moduleManager.modules.cache;
 
@@ -19,7 +19,7 @@ CacheModule.runJob("SUB", {
 			modelName: "queueSong"
 		});
 		queueSongModel.findOne({ _id: songId }, (err, song) => {
-			IOModule.runJob("EMIT_TO_ROOM", {
+			WSModule.runJob("EMIT_TO_ROOM", {
 				room: "admin.queue",
 				args: ["event:admin.queueSong.added", song]
 			});
@@ -30,7 +30,7 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "queue.removedSong",
 	cb: songId => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: "admin.queue",
 			args: ["event:admin.queueSong.removed", songId]
 		});
@@ -43,8 +43,9 @@ CacheModule.runJob("SUB", {
 		const queueSongModel = await DBModule.runJob("GET_MODEL", {
 			modelName: "queueSong"
 		});
+
 		queueSongModel.findOne({ _id: songId }, (err, song) => {
-			IOModule.runJob("EMIT_TO_ROOM", {
+			WSModule.runJob("EMIT_TO_ROOM", {
 				room: "admin.queue",
 				args: ["event:admin.queueSong.updated", song]
 			});
@@ -60,13 +61,8 @@ export default {
 	 * @param cb
 	 */
 	length: isAdminRequired(async function length(session, cb) {
-		const queueSongModel = await DBModule.runJob(
-			"GET_MODEL",
-			{
-				modelName: "queueSong"
-			},
-			this
-		);
+		const queueSongModel = await DBModule.runJob("GET_MODEL", { modelName: "queueSong" }, this);
+
 		async.waterfall(
 			[
 				next => {
@@ -125,7 +121,7 @@ export default {
 	/**
 	 * Gets a song from the Musare song id
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} songId - the Musare song id
 	 * @param {Function} cb
 	 */
@@ -159,7 +155,7 @@ export default {
 	/**
 	 * Updates a queuesong
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @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
@@ -221,7 +217,7 @@ export default {
 	/**
 	 * Removes a queuesong
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} songId - the id of the queuesong that gets removed
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -269,7 +265,7 @@ export default {
 	/**
 	 * Creates a queuesong
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} songId - the id of the song that gets added
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -361,7 +357,7 @@ export default {
 	/**
 	 * Adds a set of songs to the queue
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @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
@@ -395,7 +391,7 @@ export default {
 						songIds,
 						1,
 						(songId, next) => {
-							IOModule.runJob(
+							WSModule.runJob(
 								"RUN_ACTION2",
 								{
 									session,

+ 8 - 8
backend/logic/actions/reports.js

@@ -6,7 +6,7 @@ import moduleManager from "../../index";
 
 const DBModule = moduleManager.modules.db;
 const UtilsModule = moduleManager.modules.utils;
-const IOModule = moduleManager.modules.io;
+const WSModule = moduleManager.modules.ws;
 const SongsModule = moduleManager.modules.songs;
 const CacheModule = moduleManager.modules.cache;
 const ActivitiesModule = moduleManager.modules.activities;
@@ -37,7 +37,7 @@ const reportableIssues = [
 CacheModule.runJob("SUB", {
 	channel: "report.resolve",
 	cb: reportId => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: "admin.reports",
 			args: ["event:admin.report.resolved", reportId]
 		});
@@ -47,7 +47,7 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "report.create",
 	cb: report => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: "admin.reports",
 			args: ["event:admin.report.created", report]
 		});
@@ -58,7 +58,7 @@ export default {
 	/**
 	 * Gets all reports
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
 	index: isAdminRequired(async function index(session, cb) {
@@ -90,7 +90,7 @@ export default {
 	/**
 	 * Gets a specific report
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} reportId - the id of the report to return
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -123,7 +123,7 @@ export default {
 	/**
 	 * Gets all reports for a songId (_id)
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} songId - the id of the song to index reports for
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -167,7 +167,7 @@ export default {
 	/**
 	 * Resolves a report
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} reportId - the id of the report that is getting resolved
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -220,7 +220,7 @@ export default {
 	/**
 	 * Creates a new report
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {object} data - the object of the report data
 	 * @param {Function} cb - gets called with the result
 	 */

+ 34 - 37
backend/logic/actions/songs.js

@@ -6,7 +6,7 @@ import moduleManager from "../../index";
 
 const DBModule = moduleManager.modules.db;
 const UtilsModule = moduleManager.modules.utils;
-const IOModule = moduleManager.modules.io;
+const WSModule = moduleManager.modules.ws;
 const CacheModule = moduleManager.modules.cache;
 const SongsModule = moduleManager.modules.songs;
 const ActivitiesModule = moduleManager.modules.activities;
@@ -15,7 +15,7 @@ const PlaylistsModule = moduleManager.modules.playlists;
 CacheModule.runJob("SUB", {
 	channel: "song.removed",
 	cb: songId => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: "admin.songs",
 			args: ["event:admin.song.removed", songId]
 		});
@@ -26,8 +26,8 @@ CacheModule.runJob("SUB", {
 	channel: "song.added",
 	cb: async songId => {
 		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" });
-		songModel.findOne({ _id: songId }, (err, song) => {
-			IOModule.runJob("EMIT_TO_ROOM", {
+		songModel.findOne({ songId }, (err, song) => {
+			WSModule.runJob("EMIT_TO_ROOM", {
 				room: "admin.songs",
 				args: ["event:admin.song.added", song]
 			});
@@ -39,8 +39,8 @@ CacheModule.runJob("SUB", {
 	channel: "song.updated",
 	cb: async songId => {
 		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" });
-		songModel.findOne({ _id: songId }, (err, song) => {
-			IOModule.runJob("EMIT_TO_ROOM", {
+		songModel.findOne({ songId }, (err, song) => {
+			WSModule.runJob("EMIT_TO_ROOM", {
 				room: "admin.songs",
 				args: ["event:admin.song.updated", song]
 			});
@@ -51,7 +51,7 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "song.like",
 	cb: data => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `song.${data.songId}`,
 			args: [
 				"event:song.like",
@@ -62,9 +62,10 @@ CacheModule.runJob("SUB", {
 				}
 			]
 		});
-		IOModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("event:song.newRatings", {
+
+		WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("event:song.newRatings", {
 					songId: data.songId,
 					liked: true,
 					disliked: false
@@ -77,7 +78,7 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "song.dislike",
 	cb: data => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `song.${data.songId}`,
 			args: [
 				"event:song.dislike",
@@ -88,9 +89,9 @@ CacheModule.runJob("SUB", {
 				}
 			]
 		});
-		IOModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("event:song.newRatings", {
+		WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("event:song.newRatings", {
 					songId: data.songId,
 					liked: false,
 					disliked: true
@@ -103,7 +104,7 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "song.unlike",
 	cb: data => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `song.${data.songId}`,
 			args: [
 				"event:song.unlike",
@@ -114,9 +115,9 @@ CacheModule.runJob("SUB", {
 				}
 			]
 		});
-		IOModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("event:song.newRatings", {
+		WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("event:song.newRatings", {
 					songId: data.songId,
 					liked: false,
 					disliked: false
@@ -129,7 +130,7 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "song.undislike",
 	cb: data => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `song.${data.songId}`,
 			args: [
 				"event:song.undislike",
@@ -140,9 +141,9 @@ CacheModule.runJob("SUB", {
 				}
 			]
 		});
-		IOModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("event:song.newRatings", {
+		WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("event:song.newRatings", {
 					songId: data.songId,
 					liked: false,
 					disliked: false
@@ -156,7 +157,7 @@ export default {
 	/**
 	 * Returns the length of the songs list
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param cb
 	 */
 	length: isAdminRequired(async function length(session, cb) {
@@ -182,7 +183,7 @@ export default {
 	/**
 	 * Gets a set of songs
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param set - the set number to return
 	 * @param cb
 	 */
@@ -213,7 +214,7 @@ export default {
 	/**
 	 * Gets a song from the YouTube song id
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} songId - the YouTube song id
 	 * @param {Function} cb
 	 */
@@ -245,7 +246,7 @@ export default {
 	/**
 	 * Gets a song from the Musare song id
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} songId - the Musare song id
 	 * @param {Function} cb
 	 */
@@ -254,12 +255,8 @@ export default {
 			[
 				next => {
 					SongsModule.runJob("GET_SONG", { id: songId }, this)
-						.then(response => {
-							next(null, response.song);
-						})
-						.catch(err => {
-							next(err);
-						});
+						.then(response => next(null, response.song))
+						.catch(err => next(err));
 				}
 			],
 			async (err, song) => {
@@ -277,7 +274,7 @@ export default {
 	/**
 	 * Obtains basic metadata of a song in order to format an activity
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} songId - the song id
 	 * @param {Function} cb - callback
 	 */
@@ -333,7 +330,7 @@ export default {
 	/**
 	 * Updates a song
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} songId - the song id
 	 * @param {object} song - the updated song object
 	 * @param {Function} cb
@@ -435,18 +432,18 @@ export default {
 				if (err) {
 					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
 
-					this.log("ERROR", "SONGS_UPDATE", `Failed to remove song "${songId}". "${err}"`);
+					this.log("ERROR", "SONGS_REMOVE", `Failed to remove song "${songId}". "${err}"`);
 
 					return cb({ status: "failure", message: err });
 				}
 
-				this.log("SUCCESS", "SONGS_UPDATE", `Successfully remove song "${songId}".`);
+				this.log("SUCCESS", "SONGS_REMOVE", `Successfully remove song "${songId}".`);
 
 				CacheModule.runJob("PUB", { channel: "song.removed", value: songId });
 
 				return cb({
 					status: "success",
-					message: "Song has been successfully updated"
+					message: "Song has been successfully removed"
 				});
 			}
 		);

+ 77 - 82
backend/logic/actions/stations.js

@@ -7,7 +7,7 @@ import moduleManager from "../../index";
 
 const DBModule = moduleManager.modules.db;
 const UtilsModule = moduleManager.modules.utils;
-const IOModule = moduleManager.modules.io;
+const WSModule = moduleManager.modules.ws;
 const SongsModule = moduleManager.modules.songs;
 const PlaylistsModule = moduleManager.modules.playlists;
 const CacheModule = moduleManager.modules.cache;
@@ -19,7 +19,7 @@ const YouTubeModule = moduleManager.modules.youtube;
 CacheModule.runJob("SUB", {
 	channel: "station.updateUsers",
 	cb: ({ stationId, usersPerStation }) => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `station.${stationId}`,
 			args: ["event:users.updated", usersPerStation]
 		});
@@ -31,39 +31,44 @@ CacheModule.runJob("SUB", {
 	cb: ({ stationId, usersPerStationCount }) => {
 		const count = usersPerStationCount || 0;
 
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `station.${stationId}`,
 			args: ["event:userCount.updated", count]
 		});
 
 		StationsModule.runJob("GET_STATION", { stationId }).then(async station => {
 			if (station.privacy === "public")
-				IOModule.runJob("EMIT_TO_ROOM", {
+				WSModule.runJob("EMIT_TO_ROOM", {
 					room: "home",
 					args: ["event:userCount.updated", stationId, count]
 				});
 			else {
-				const sockets = await IOModule.runJob("GET_ROOM_SOCKETS", {
+				const sockets = await WSModule.runJob("GET_SOCKETS_FOR_ROOM", {
 					room: "home"
 				});
 
-				Object.keys(sockets).forEach(socketKey => {
-					const socket = sockets[socketKey];
+				sockets.forEach(async socketId => {
+					const socket = await WSModule.runJob("SOCKET_FROM_SOCKET_ID", { socketId }, this);
 					const { session } = socket;
+
 					if (session.sessionId) {
 						CacheModule.runJob("HGET", {
 							table: "sessions",
 							key: session.sessionId
 						}).then(session => {
 							if (session)
-								DBModule.runJob("GET_MODEL", {
-									modelName: "user"
-								}).then(userModel =>
+								DBModule.runJob(
+									"GET_MODEL",
+									{
+										modelName: "user"
+									},
+									this
+								).then(userModel =>
 									userModel.findOne({ _id: session.userId }, (err, user) => {
 										if (user.role === "admin")
-											socket.emit("event:userCount.updated", stationId, count);
+											socket.dispatch("event:userCount.updated", stationId, count);
 										else if (station.type === "community" && station.owner === session.userId)
-											socket.emit("event:userCount.updated", stationId, count);
+											socket.dispatch("event:userCount.updated", stationId, count);
 									})
 								);
 						});
@@ -77,7 +82,7 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "station.updateTheme",
 	cb: data => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `station.${data.stationId}`,
 			args: ["event:theme.updated", data.theme]
 		});
@@ -87,7 +92,7 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "station.queueLockToggled",
 	cb: data => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `station.${data.stationId}`,
 			args: ["event:queueLockToggled", data.locked]
 		});
@@ -97,7 +102,7 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "station.updatePartyMode",
 	cb: data => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `station.${data.stationId}`,
 			args: ["event:partyMode.updated", data.partyMode]
 		});
@@ -107,7 +112,7 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "privatePlaylist.selected",
 	cb: data => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `station.${data.stationId}`,
 			args: ["event:privatePlaylist.selected", data.playlistId]
 		});
@@ -117,7 +122,7 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "privatePlaylist.deselected",
 	cb: data => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `station.${data.stationId}`,
 			args: ["event:privatePlaylist.deselected"]
 		});
@@ -128,7 +133,7 @@ CacheModule.runJob("SUB", {
 	channel: "station.pause",
 	cb: stationId => {
 		StationsModule.runJob("GET_STATION", { stationId }).then(station => {
-			IOModule.runJob("EMIT_TO_ROOM", {
+			WSModule.runJob("EMIT_TO_ROOM", {
 				room: `station.${stationId}`,
 				args: ["event:stations.pause", { pausedAt: station.pausedAt }]
 			});
@@ -139,7 +144,7 @@ CacheModule.runJob("SUB", {
 			}).then(response => {
 				const { socketsThatCan } = response;
 				socketsThatCan.forEach(socket => {
-					socket.emit("event:station.pause", { stationId });
+					socket.dispatch("event:station.pause", { stationId });
 				});
 			});
 		});
@@ -150,7 +155,7 @@ CacheModule.runJob("SUB", {
 	channel: "station.resume",
 	cb: stationId => {
 		StationsModule.runJob("GET_STATION", { stationId }).then(station => {
-			IOModule.runJob("EMIT_TO_ROOM", {
+			WSModule.runJob("EMIT_TO_ROOM", {
 				room: `station.${stationId}`,
 				args: ["event:stations.resume", { timePaused: station.timePaused }]
 			});
@@ -162,7 +167,7 @@ CacheModule.runJob("SUB", {
 				.then(response => {
 					const { socketsThatCan } = response;
 					socketsThatCan.forEach(socket => {
-						socket.emit("event:station.resume", { stationId });
+						socket.dispatch("event:station.resume", { stationId });
 					});
 				})
 				.catch(console.log);
@@ -179,7 +184,7 @@ CacheModule.runJob("SUB", {
 				if (station.privacy === "public") {
 					// Station became public
 
-					IOModule.runJob("EMIT_TO_ROOM", {
+					WSModule.runJob("EMIT_TO_ROOM", {
 						room: "home",
 						args: ["event:stations.created", station]
 					});
@@ -192,10 +197,10 @@ CacheModule.runJob("SUB", {
 					}).then(response => {
 						const { socketsThatCan, socketsThatCannot } = response;
 						socketsThatCan.forEach(socket => {
-							socket.emit("event:station.updatePrivacy", { stationId, privacy: station.privacy });
+							socket.dispatch("event:station.updatePrivacy", { stationId, privacy: station.privacy });
 						});
 						socketsThatCannot.forEach(socket => {
-							socket.emit("event:station.removed", { stationId });
+							socket.dispatch("event:station.removed", { stationId });
 						});
 					});
 				} else {
@@ -207,7 +212,7 @@ CacheModule.runJob("SUB", {
 					}).then(response => {
 						const { socketsThatCan } = response;
 						socketsThatCan.forEach(socket => {
-							socket.emit("event:station.updatePrivacy", { stationId, privacy: station.privacy });
+							socket.dispatch("event:station.updatePrivacy", { stationId, privacy: station.privacy });
 						});
 					});
 				}
@@ -227,11 +232,11 @@ CacheModule.runJob("SUB", {
 				station
 			}).then(response => {
 				const { socketsThatCan } = response;
-				socketsThatCan.forEach(socket => socket.emit("event:station.updateName", { stationId, name }));
+				socketsThatCan.forEach(socket => socket.dispatch("event:station.updateName", { stationId, name }));
 			})
 		);
 
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `station.${stationId}`,
 			args: ["event:station.updateName", { stationId, name }]
 		});
@@ -250,12 +255,12 @@ CacheModule.runJob("SUB", {
 			}).then(response => {
 				const { socketsThatCan } = response;
 				socketsThatCan.forEach(socket =>
-					socket.emit("event:station.updateDisplayName", { stationId, displayName })
+					socket.dispatch("event:station.updateDisplayName", { stationId, displayName })
 				);
 			})
 		);
 
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `station.${stationId}`,
 			args: ["event:station.updateDisplayName", { stationId, displayName }]
 		});
@@ -274,12 +279,12 @@ CacheModule.runJob("SUB", {
 			}).then(response => {
 				const { socketsThatCan } = response;
 				socketsThatCan.forEach(socket =>
-					socket.emit("event:station.updateDescription", { stationId, description })
+					socket.dispatch("event:station.updateDescription", { stationId, description })
 				);
 			})
 		);
 
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `station.${stationId}`,
 			args: ["event:station.updateDescription", { stationId, description }]
 		});
@@ -291,7 +296,7 @@ CacheModule.runJob("SUB", {
 	cb: response => {
 		const { stationId } = response;
 		StationsModule.runJob("GET_STATION", { stationId }).then(station => {
-			IOModule.runJob("EMIT_TO_ROOM", {
+			WSModule.runJob("EMIT_TO_ROOM", {
 				room: `station.${stationId}`,
 				args: ["event:station.themeUpdated", station.theme]
 			});
@@ -301,7 +306,7 @@ CacheModule.runJob("SUB", {
 			}).then(response => {
 				const { socketsThatCan } = response;
 				socketsThatCan.forEach(socket => {
-					socket.emit("event:station.themeUpdated", { stationId, theme: station.theme });
+					socket.dispatch("event:station.themeUpdated", { stationId, theme: station.theme });
 				});
 			});
 		});
@@ -312,7 +317,7 @@ CacheModule.runJob("SUB", {
 	channel: "station.queueUpdate",
 	cb: stationId => {
 		StationsModule.runJob("GET_STATION", { stationId }).then(station => {
-			IOModule.runJob("EMIT_TO_ROOM", {
+			WSModule.runJob("EMIT_TO_ROOM", {
 				room: `station.${stationId}`,
 				args: ["event:queue.update", station.queue]
 			});
@@ -323,7 +328,7 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "station.voteSkipSong",
 	cb: stationId => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `station.${stationId}`,
 			args: ["event:song.voteSkipSong"]
 		});
@@ -333,16 +338,16 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "station.remove",
 	cb: stationId => {
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `station.${stationId}`,
 			args: ["event:stations.remove"]
 		});
 		console.log(111, "REMOVED");
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `home`,
 			args: ["event:station.removed", { stationId }]
 		});
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: "admin.stations",
 			args: ["event:admin.station.removed", stationId]
 		});
@@ -357,23 +362,27 @@ CacheModule.runJob("SUB", {
 		StationsModule.runJob("INITIALIZE_STATION", { stationId }).then(async response => {
 			const { station } = response;
 			station.userCount = StationsModule.usersPerStationCount[stationId] || 0;
-			IOModule.runJob("EMIT_TO_ROOM", {
+
+			WSModule.runJob("EMIT_TO_ROOM", {
 				room: "admin.stations",
 				args: ["event:admin.station.added", station]
-			});
+			}).then(() => {});
+
 			// TODO If community, check if on whitelist
 			if (station.privacy === "public")
-				IOModule.runJob("EMIT_TO_ROOM", {
+				WSModule.runJob("EMIT_TO_ROOM", {
 					room: "home",
 					args: ["event:stations.created", station]
-				});
+				}).then(() => {});
 			else {
-				const sockets = await IOModule.runJob("GET_ROOM_SOCKETS", {
+				const sockets = await WSModule.runJob("GET_SOCKETS_FOR_ROOM", {
 					room: "home"
 				});
-				Object.keys(sockets).forEach(socketKey => {
-					const socket = sockets[socketKey];
+
+				sockets.forEach(async socketId => {
+					const socket = await WSModule.runJob("SOCKET_FROM_SOCKET_ID", { socketId }, this);
 					const { session } = socket;
+
 					if (session.sessionId) {
 						CacheModule.runJob("HGET", {
 							table: "sessions",
@@ -381,9 +390,9 @@ CacheModule.runJob("SUB", {
 						}).then(session => {
 							if (session) {
 								userModel.findOne({ _id: session.userId }, (err, user) => {
-									if (user.role === "admin") socket.emit("event:stations.created", station);
+									if (user.role === "admin") socket.dispatch("event:stations.created", station);
 									else if (station.type === "community" && station.owner === session.userId)
-										socket.emit("event:stations.created", station);
+										socket.dispatch("event:stations.created", station);
 								});
 							}
 						});
@@ -405,13 +414,12 @@ export default {
 		async.waterfall(
 			[
 				next => {
-					CacheModule.runJob("HGETALL", { table: "stations" }, this).then(stations => {
-						next(null, stations);
-					});
+					CacheModule.runJob("HGETALL", { table: "stations" }, this).then(stations => next(null, stations));
 				},
 
 				(items, next) => {
 					const filteredStations = [];
+
 					async.each(
 						items,
 						(station, nextStation) => {
@@ -420,14 +428,10 @@ export default {
 									callback => {
 										// only relevant if user logged in
 										if (session.userId) {
-											return StationsModule.runJob(
-												"HAS_USER_FAVORITED_STATION",
-												{
-													userId: session.userId,
-													stationId: station._id
-												},
-												this
-											)
+											return StationsModule.runJob("HAS_USER_FAVORITED_STATION", {
+												userId: session.userId,
+												stationId: station._id
+											})
 												.then(isStationFavorited => {
 													station.isFavorited = isStationFavorited;
 													return callback();
@@ -458,7 +462,6 @@ export default {
 									station.userCount = StationsModule.usersPerStationCount[station._id] || 0;
 
 									if (exists) filteredStations.push(station);
-
 									return nextStation();
 								}
 							);
@@ -473,7 +476,9 @@ export default {
 					this.log("ERROR", "STATIONS_INDEX", `Indexing stations failed. "${err}"`);
 					return cb({ status: "failure", message: err });
 				}
+
 				this.log("SUCCESS", "STATIONS_INDEX", `Indexing stations successful.`, false);
+
 				return cb({ status: "success", stations });
 			}
 		);
@@ -728,7 +733,7 @@ export default {
 				},
 
 				(station, next) => {
-					IOModule.runJob("SOCKET_JOIN_ROOM", {
+					WSModule.runJob("SOCKET_JOIN_ROOM", {
 						socketId: session.socketId,
 						room: `station.${station._id}`
 					});
@@ -767,20 +772,14 @@ export default {
 
 					if (!data.currentSong || !data.currentSong.title) return next(null, data);
 
-					IOModule.runJob("SOCKET_JOIN_SONG_ROOM", {
+					WSModule.runJob("SOCKET_JOIN_SONG_ROOM", {
 						socketId: session.socketId,
 						room: `song.${data.currentSong.songId}`
 					});
 
 					data.currentSong.skipVotes = data.currentSong.skipVotes.length;
 
-					return SongsModule.runJob(
-						"GET_SONG_FROM_ID",
-						{
-							songId: data.currentSong.songId
-						},
-						this
-					)
+					return SongsModule.runJob("GET_SONG_FROM_ID", { songId: data.currentSong.songId }, this)
 						.then(response => {
 							const { song } = response;
 							if (song) {
@@ -825,6 +824,7 @@ export default {
 					this.log("ERROR", "STATIONS_JOIN", `Joining station "${stationIdentifier}" failed. "${err}"`);
 					return cb({ status: "failure", message: err });
 				}
+
 				this.log("SUCCESS", "STATIONS_JOIN", `Joined station "${data._id}" successfully.`);
 				return cb({ status: "success", data });
 			}
@@ -1180,16 +1180,8 @@ export default {
 
 				(station, next) => {
 					skipVotes = station.currentSong.skipVotes.length;
-					IOModule.runJob(
-						"GET_ROOM_SOCKETS",
-						{
-							room: `station.${stationId}`
-						},
-						this
-					)
-						.then(sockets => {
-							next(null, sockets);
-						})
+					WSModule.runJob("GET_SOCKETS_FOR_ROOM", { room: `station.${stationId}` }, this)
+						.then(sockets => next(null, sockets))
 						.catch(next);
 				},
 
@@ -1205,6 +1197,7 @@ export default {
 					return cb({ status: "failure", message: err });
 				}
 				this.log("SUCCESS", "STATIONS_VOTE_SKIP", `Vote skipping "${stationId}" successful.`);
+
 				CacheModule.runJob("PUB", {
 					channel: "station.voteSkipSong",
 					value: stationId
@@ -1291,7 +1284,7 @@ export default {
 
 				this.log("SUCCESS", "STATIONS_LEAVE", `Left station "${stationId}" successfully.`);
 
-				IOModule.runJob("SOCKET_LEAVE_ROOMS", { socketId: session });
+				WSModule.runJob("SOCKET_LEAVE_ROOMS", { socketId: session });
 
 				delete StationsModule.userList[session.socketId];
 
@@ -2261,13 +2254,15 @@ export default {
 
 				(station, next) => {
 					CacheModule.runJob("HDEL", { table: "stations", key: stationId }, this)
-						.then(next(null, station))
+						.then(() => next(null, station))
 						.catch(next);
 				},
 
 				(station, next) => {
 					if (station.playlist2)
-						PlaylistsModule.runJob("DELETE_PLAYLIST", { playlistId: station.playlist2 }).then().catch();
+						PlaylistsModule.runJob("DELETE_PLAYLIST", { playlistId: station.playlist2 })
+							.then(() => {})
+							.catch(next);
 					next(null, station);
 				}
 			],
@@ -2288,7 +2283,7 @@ export default {
 				ActivitiesModule.runJob("ADD_ACTIVITY", {
 					userId: session.userId,
 					type: "station__remove",
-					payload: { message: `Removed a station named <stationId>${station.displayName}</stationId>` }
+					payload: { message: `Removed a station named ${station.displayName}` }
 				});
 
 				return cb({

+ 69 - 80
backend/logic/actions/users.js

@@ -11,7 +11,7 @@ import moduleManager from "../../index";
 
 const DBModule = moduleManager.modules.db;
 const UtilsModule = moduleManager.modules.utils;
-const IOModule = moduleManager.modules.io;
+const WSModule = moduleManager.modules.ws;
 const CacheModule = moduleManager.modules.cache;
 const MailModule = moduleManager.modules.mail;
 const PunishmentsModule = moduleManager.modules.punishments;
@@ -21,9 +21,9 @@ const PlaylistsModule = moduleManager.modules.playlists;
 CacheModule.runJob("SUB", {
 	channel: "user.updatePreferences",
 	cb: res => {
-		IOModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("keep.event:user.preferences.changed", res.preferences);
+		WSModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("keep.event:user.preferences.changed", res.preferences);
 			});
 		});
 	}
@@ -32,13 +32,13 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "user.updateOrderOfPlaylists",
 	cb: res => {
-		IOModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("event:user.orderOfPlaylists.changed", res.orderOfPlaylists);
+		WSModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("event:user.orderOfPlaylists.changed", res.orderOfPlaylists);
 			});
 		});
 
-		IOModule.runJob("EMIT_TO_ROOM", {
+		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `profile-${res.userId}-playlists`,
 			args: ["event:user.orderOfPlaylists.changed", res.orderOfPlaylists]
 		});
@@ -48,9 +48,9 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "user.updateUsername",
 	cb: user => {
-		IOModule.runJob("SOCKETS_FROM_USER", { userId: user._id }).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("event:user.username.changed", user.username);
+		WSModule.runJob("SOCKETS_FROM_USER", { userId: user._id }).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("event:user.username.changed", user.username);
 			});
 		});
 	}
@@ -59,9 +59,9 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "user.removeSessions",
 	cb: userId => {
-		IOModule.runJob("SOCKETS_FROM_USER_WITHOUT_CACHE", { userId }).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("keep.event:user.session.removed");
+		WSModule.runJob("SOCKETS_FROM_USER_WITHOUT_CACHE", { userId }).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("keep.event:user.session.removed");
 			});
 		});
 	}
@@ -70,9 +70,9 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "user.linkPassword",
 	cb: userId => {
-		IOModule.runJob("SOCKETS_FROM_USER", { userId }).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("event:user.linkPassword");
+		WSModule.runJob("SOCKETS_FROM_USER", { userId }).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("event:user.linkPassword");
 			});
 		});
 	}
@@ -81,9 +81,9 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "user.unlinkPassword",
 	cb: userId => {
-		IOModule.runJob("SOCKETS_FROM_USER", { userId }).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("event:user.unlinkPassword");
+		WSModule.runJob("SOCKETS_FROM_USER", { userId }).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("event:user.unlinkPassword");
 			});
 		});
 	}
@@ -92,9 +92,9 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "user.linkGithub",
 	cb: userId => {
-		IOModule.runJob("SOCKETS_FROM_USER", { userId }).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("event:user.linkGithub");
+		WSModule.runJob("SOCKETS_FROM_USER", { userId }).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("event:user.linkGithub");
 			});
 		});
 	}
@@ -103,9 +103,9 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "user.unlinkGithub",
 	cb: userId => {
-		IOModule.runJob("SOCKETS_FROM_USER", { userId }).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("event:user.unlinkGithub");
+		WSModule.runJob("SOCKETS_FROM_USER", { userId }).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("event:user.unlinkGithub");
 			});
 		});
 	}
@@ -114,9 +114,9 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "user.ban",
 	cb: data => {
-		IOModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("keep.event:banned", data.punishment);
+		WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("keep.event:banned", data.punishment);
 				socket.disconnect(true);
 			});
 		});
@@ -126,9 +126,9 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "user.favoritedStation",
 	cb: data => {
-		IOModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("event:user.favoritedStation", data.stationId);
+		WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("event:user.favoritedStation", data.stationId);
 			});
 		});
 	}
@@ -137,9 +137,9 @@ CacheModule.runJob("SUB", {
 CacheModule.runJob("SUB", {
 	channel: "user.unfavoritedStation",
 	cb: data => {
-		IOModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
-			response.sockets.forEach(socket => {
-				socket.emit("event:user.unfavoritedStation", data.stationId);
+		WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
+			sockets.forEach(socket => {
+				socket.dispatch("event:user.unfavoritedStation", data.stationId);
 			});
 		});
 	}
@@ -149,7 +149,7 @@ export default {
 	/**
 	 * Lists all Users
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
 	index: isAdminRequired(async function index(session, cb) {
@@ -199,7 +199,7 @@ export default {
 	/**
 	 * Removes all data held on a user, including their ability to login
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
 	remove: isLoginRequired(async function remove(session, cb) {
@@ -256,7 +256,7 @@ export default {
 	/**
 	 * Logs user in
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} identifier - the email of the user
 	 * @param {string} password - the plaintext of the user
 	 * @param {Function} cb - gets called with the result
@@ -340,7 +340,7 @@ export default {
 	/**
 	 * Registers a new user
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @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
@@ -349,13 +349,8 @@ export default {
 	 */
 	async register(session, username, email, password, recaptcha, cb) {
 		email = email.toLowerCase();
-		const verificationToken = await UtilsModule.runJob(
-			"GENERATE_RANDOM_STRING",
-			{
-				length: 64
-			},
-			this
-		);
+		const verificationToken = await UtilsModule.runJob("GENERATE_RANDOM_STRING", { length: 64 }, this);
+
 		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
 		const verifyEmailSchema = await MailModule.runJob(
 			"GET_SCHEMA",
@@ -391,7 +386,7 @@ export default {
 							})
 							.then(res => next(null, res.data))
 							.catch(err => next(err));
-					else next(null, null, null);
+					else next(null, null);
 				},
 
 				// check if the response from Google recaptcha is successful
@@ -447,13 +442,7 @@ export default {
 
 				// generate the url for gravatar avatar
 				(user, next) => {
-					UtilsModule.runJob(
-						"CREATE_GRAVATAR",
-						{
-							email: user.email.address
-						},
-						this
-					).then(url => {
+					UtilsModule.runJob("CREATE_GRAVATAR", { email: user.email.address }, this).then(url => {
 						user.avatar = {
 							type: "gravatar",
 							url
@@ -563,7 +552,7 @@ export default {
 	/**
 	 * Logs out a user
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
 	logout(session, cb) {
@@ -605,7 +594,7 @@ export default {
 	/**
 	 * Removes all sessions for a user
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} userId - the id of the user we are trying to delete the sessions of
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -689,7 +678,7 @@ export default {
 	/**
 	 * Updates the order of a user's playlists
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Array} orderOfPlaylists - array of playlist ids (with a specific order)
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -745,7 +734,7 @@ export default {
 	/**
 	 * Updates a user's preferences
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {object} preferences - object containing preferences
 	 * @param {boolean} preferences.nightmode - whether or not the user is using the night mode theme
 	 * @param {boolean} preferences.autoSkipDisliked - whether to automatically skip disliked songs
@@ -832,7 +821,7 @@ export default {
 	/**
 	 * Retrieves a user's preferences
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
 	getPreferences: isLoginRequired(async function updatePreferences(session, cb) {
@@ -875,7 +864,7 @@ export default {
 	/**
 	 * Gets user object from username (only a few properties)
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} username - the username of the user we are trying to find
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -924,7 +913,7 @@ export default {
 	/**
 	 * Gets a username from an userId
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} userId - the userId of the person we are trying to get the username from
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -969,7 +958,7 @@ export default {
 	/**
 	 * Gets a user from a userId
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} userId - the userId of the person we are trying to get the username from
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -1024,7 +1013,7 @@ export default {
 	/**
 	 * Gets user info from session
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
 	async findBySession(session, cb) {
@@ -1092,7 +1081,7 @@ export default {
 	/**
 	 * Updates a user's username
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} updatingUserId - the updating user's id
 	 * @param {string} newUsername - the new username
 	 * @param {Function} cb - gets called with the result
@@ -1176,7 +1165,7 @@ export default {
 	/**
 	 * Updates a user's email
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} updatingUserId - the updating user's id
 	 * @param {string} newEmail - the new email
 	 * @param {Function} cb - gets called with the result
@@ -1280,7 +1269,7 @@ export default {
 	/**
 	 * Updates a user's name
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} updatingUserId - the updating user's id
 	 * @param {string} newBio - the new name
 	 * @param {Function} cb - gets called with the result
@@ -1346,7 +1335,7 @@ export default {
 	/**
 	 * Updates a user's location
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} updatingUserId - the updating user's id
 	 * @param {string} newLocation - the new location
 	 * @param {Function} cb - gets called with the result
@@ -1418,7 +1407,7 @@ export default {
 	/**
 	 * Updates a user's bio
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} updatingUserId - the updating user's id
 	 * @param {string} newBio - the new bio
 	 * @param {Function} cb - gets called with the result
@@ -1478,7 +1467,7 @@ export default {
 	/**
 	 * Updates the type of a user's avatar
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} updatingUserId - the updating user's id
 	 * @param {string} newType - the new type
 	 * @param {Function} cb - gets called with the result
@@ -1542,7 +1531,7 @@ export default {
 	/**
 	 * Updates a user's role
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} updatingUserId - the updating user's id
 	 * @param {string} newRole - the new role
 	 * @param {Function} cb - gets called with the result
@@ -1601,7 +1590,7 @@ export default {
 	/**
 	 * Updates a user's password
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} previousPassword - the previous password
 	 * @param {string} newPassword - the new password
 	 * @param {Function} cb - gets called with the result
@@ -1678,7 +1667,7 @@ export default {
 	/**
 	 * Requests a password for a session
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} email - the email of the user that requests a password reset
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -1756,7 +1745,7 @@ export default {
 	/**
 	 * Verifies a password code
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} code - the password code
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -1800,7 +1789,7 @@ export default {
 	/**
 	 * Adds a password to a user with a code
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} code - the password code
 	 * @param {string} newPassword - the new password code
 	 * @param {Function} cb - gets called with the result
@@ -1880,7 +1869,7 @@ export default {
 	/**
 	 * Unlinks password from user
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
 	unlinkPassword: isLoginRequired(async function unlinkPassword(session, cb) {
@@ -1927,7 +1916,7 @@ export default {
 	/**
 	 * Unlinks GitHub from user
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
 	unlinkGitHub: isLoginRequired(async function unlinkGitHub(session, cb) {
@@ -1974,7 +1963,7 @@ export default {
 	/**
 	 * Requests a password reset for an email
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} email - the email of the user that requests a password reset
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -2053,7 +2042,7 @@ export default {
 	/**
 	 * Verifies a reset code
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} code - the password reset code
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -2092,7 +2081,7 @@ export default {
 	/**
 	 * Changes a user's password with a reset code
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} code - the password reset code
 	 * @param {string} newPassword - the new password reset code
 	 * @param {Function} cb - gets called with the result
@@ -2165,7 +2154,7 @@ export default {
 	/**
 	 * Bans a user by userId
 	 *
-	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} session - the session object automatically added by the websocket
 	 * @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

+ 14 - 14
backend/logic/activities.js

@@ -6,7 +6,7 @@ let ActivitiesModule;
 let DBModule;
 let CacheModule;
 let UtilsModule;
-let IOModule;
+let WSModule;
 let PlaylistsModule;
 
 class _ActivitiesModule extends CoreClass {
@@ -27,7 +27,7 @@ class _ActivitiesModule extends CoreClass {
 			DBModule = this.moduleManager.modules.db;
 			CacheModule = this.moduleManager.modules.cache;
 			UtilsModule = this.moduleManager.modules.utils;
-			IOModule = this.moduleManager.modules.io;
+			WSModule = this.moduleManager.modules.ws;
 			PlaylistsModule = this.moduleManager.modules.playlists;
 
 			resolve();
@@ -72,16 +72,16 @@ class _ActivitiesModule extends CoreClass {
 					},
 
 					(activity, next) => {
-						IOModule.runJob("SOCKETS_FROM_USER", { userId: activity.userId }, this)
-							.then(res => {
-								res.sockets.forEach(socket => socket.emit("event:activity.create", activity));
+						WSModule.runJob("SOCKETS_FROM_USER", { userId: activity.userId }, this)
+							.then(sockets => {
+								sockets.forEach(socket => socket.dispatch("event:activity.create", activity));
 								next(null, activity);
 							})
 							.catch(next);
 					},
 
 					(activity, next) => {
-						IOModule.runJob("EMIT_TO_ROOM", {
+						WSModule.runJob("EMIT_TO_ROOM", {
 							room: `profile-${activity.userId}-activities`,
 							args: ["event:activity.create", activity]
 						});
@@ -225,13 +225,13 @@ class _ActivitiesModule extends CoreClass {
 						activities.forEach(activity => {
 							activityModel.updateOne({ _id: activity._id }, { $set: { hidden: true } }).catch(next);
 
-							IOModule.runJob("SOCKETS_FROM_USER", { userId: payload.userId }, this)
-								.then(res =>
-									res.sockets.forEach(socket => socket.emit("event:activity.hide", activity._id))
+							WSModule.runJob("SOCKETS_FROM_USER", { userId: payload.userId }, this)
+								.then(sockets =>
+									sockets.forEach(socket => socket.dispatch("event:activity.hide", activity._id))
 								)
 								.catch(next);
 
-							IOModule.runJob("EMIT_TO_ROOM", {
+							WSModule.runJob("EMIT_TO_ROOM", {
 								room: `profile-${payload.userId}-activities`,
 								args: ["event:activity.hide", activity._id]
 							});
@@ -330,13 +330,13 @@ class _ActivitiesModule extends CoreClass {
 						activities.forEach(activity => {
 							activityModel.updateOne({ _id: activity._id }, { $set: { hidden: true } }).catch(next);
 
-							IOModule.runJob("SOCKETS_FROM_USER", { userId: payload.userId }, this)
-								.then(res =>
-									res.sockets.forEach(socket => socket.emit("event:activity.hide", activity._id))
+							WSModule.runJob("SOCKETS_FROM_USER", { userId: payload.userId }, this)
+								.then(sockets =>
+									sockets.forEach(socket => socket.dispatch("event:activity.hide", activity._id))
 								)
 								.catch(next);
 
-							IOModule.runJob("EMIT_TO_ROOM", {
+							WSModule.runJob("EMIT_TO_ROOM", {
 								room: `profile-${payload.userId}-activities`,
 								args: ["event:activity.hide", activity._id]
 							});

+ 3 - 2
backend/logic/app.js

@@ -6,6 +6,7 @@ import cookieParser from "cookie-parser";
 import bodyParser from "body-parser";
 import express from "express";
 import oauth from "oauth";
+import http from "http";
 import CoreClass from "../core";
 
 const { OAuth2 } = oauth;
@@ -42,7 +43,7 @@ class _AppModule extends CoreClass {
 
 			const app = (this.app = express());
 			const SIDname = config.get("cookie.SIDname");
-			this.server = app.listen(config.get("serverPort"));
+			this.server = http.createServer(app).listen(config.get("serverPort"));
 
 			app.use(cookieParser());
 
@@ -134,7 +135,7 @@ class _AppModule extends CoreClass {
 
 				const { state } = req.query;
 
-				const verificationToken = await UtilsModule.runJob("GENERATE_RANDOM_STRING", { length: 64 });
+				const verificationToken = await UtilsModule.runJob("GENERATE_RANDOM_STRING", { length: 64 }, this);
 
 				return async.waterfall(
 					[

+ 31 - 47
backend/logic/stations.js

@@ -7,7 +7,7 @@ let StationsModule;
 let CacheModule;
 let DBModule;
 let UtilsModule;
-let IOModule;
+let WSModule;
 let SongsModule;
 let PlaylistsModule;
 let NotificationsModule;
@@ -29,7 +29,7 @@ class _StationsModule extends CoreClass {
 		CacheModule = this.moduleManager.modules.cache;
 		DBModule = this.moduleManager.modules.db;
 		UtilsModule = this.moduleManager.modules.utils;
-		IOModule = this.moduleManager.modules.io;
+		WSModule = this.moduleManager.modules.ws;
 		SongsModule = this.moduleManager.modules.songs;
 		PlaylistsModule = this.moduleManager.modules.playlists;
 		NotificationsModule = this.moduleManager.modules.notifications;
@@ -86,7 +86,7 @@ class _StationsModule extends CoreClass {
 					key: stationId
 				}).then(playlistObj => {
 					if (playlistObj) {
-						IOModule.runJob("EMIT_TO_ROOM", {
+						WSModule.runJob("EMIT_TO_ROOM", {
 							room: `station.${stationId}`,
 							args: ["event:newOfficialPlaylist", playlistObj.songs]
 						});
@@ -878,7 +878,7 @@ class _StationsModule extends CoreClass {
 						}
 						// TODO Pub/Sub this
 
-						IOModule.runJob("EMIT_TO_ROOM", {
+						WSModule.runJob("EMIT_TO_ROOM", {
 							room: `station.${station._id}`,
 							args: [
 								"event:songs.next",
@@ -894,25 +894,23 @@ class _StationsModule extends CoreClass {
 							.catch();
 
 						if (station.privacy === "public") {
-							IOModule.runJob("EMIT_TO_ROOM", {
+							WSModule.runJob("EMIT_TO_ROOM", {
 								room: "home",
 								args: ["event:station.nextSong", station._id, station.currentSong]
 							})
 								.then()
 								.catch();
 						} else {
-							const sockets = await IOModule.runJob("GET_ROOM_SOCKETS", { room: "home" }, this);
+							const sockets = await WSModule.runJob("GET_SOCKETS_FOR_ROOM", { room: "home" }, this);
 
-							Object.keys(sockets).forEach(socketKey => {
-								const socket = sockets[socketKey];
+							sockets.forEach(async socketId => {
+								const socket = await WSModule.runJob("SOCKET_FROM_SOCKET_ID", { socketId }, this);
 								const { session } = socket;
+
 								if (session.sessionId) {
 									CacheModule.runJob(
 										"HGET",
-										{
-											table: "sessions",
-											key: session.sessionId
-										},
+										{ table: "sessions", key: session.sessionId },
 										this
 										// eslint-disable-next-line no-loop-func
 									).then(session => {
@@ -926,7 +924,7 @@ class _StationsModule extends CoreClass {
 														(err, user) => {
 															if (!err && user) {
 																if (user.role === "admin")
-																	socket.emit(
+																	socket.dispatch(
 																		"event:station.nextSong",
 																		station._id,
 																		station.currentSong
@@ -935,7 +933,7 @@ class _StationsModule extends CoreClass {
 																	station.type === "community" &&
 																	station.owner === session.userId
 																)
-																	socket.emit(
+																	socket.dispatch(
 																		"event:station.nextSong",
 																		station._id,
 																		station.currentSong
@@ -952,12 +950,10 @@ class _StationsModule extends CoreClass {
 						}
 
 						if (station.currentSong !== null && station.currentSong.songId !== undefined) {
-							IOModule.runJob("SOCKETS_JOIN_SONG_ROOM", {
-								sockets: await IOModule.runJob(
-									"GET_ROOM_SOCKETS",
-									{
-										room: `station.${station._id}`
-									},
+							WSModule.runJob("SOCKETS_JOIN_SONG_ROOM", {
+								sockets: await WSModule.runJob(
+									"GET_SOCKETS_FOR_ROOM",
+									{ room: `station.${station._id}` },
 									this
 								),
 								room: `song.${station.currentSong.songId}`
@@ -970,17 +966,17 @@ class _StationsModule extends CoreClass {
 								});
 							}
 						} else {
-							IOModule.runJob("SOCKETS_LEAVE_SONG_ROOMS", {
-								sockets: await IOModule.runJob(
-									"GET_ROOM_SOCKETS",
-									{
-										room: `station.${station._id}`
-									},
-									this
-								)
-							})
-								.then()
-								.catch();
+							WSModule.runJob(
+								"SOCKETS_LEAVE_SONG_ROOMS",
+								{
+									sockets: await WSModule.runJob(
+										"GET_SOCKETS_FOR_ROOM",
+										{ room: `station.${station._id}` },
+										this
+									)
+								},
+								this
+							).then(() => {});
 						}
 
 						resolve({ station });
@@ -1014,13 +1010,7 @@ class _StationsModule extends CoreClass {
 					},
 
 					next => {
-						DBModule.runJob(
-							"GET_MODEL",
-							{
-								modelName: "user"
-							},
-							this
-						).then(userModel => {
+						DBModule.runJob("GET_MODEL", { modelName: "user" }, this).then(userModel => {
 							userModel.findOne({ _id: payload.userId }, next);
 						});
 					},
@@ -1065,13 +1055,7 @@ class _StationsModule extends CoreClass {
 			async.waterfall(
 				[
 					next => {
-						DBModule.runJob(
-							"GET_MODEL",
-							{
-								modelName: "user"
-							},
-							this
-						).then(userModel => {
+						DBModule.runJob("GET_MODEL", { modelName: "user" }, this).then(userModel => {
 							userModel.findOne({ _id: payload.userId }, next);
 						});
 					},
@@ -1099,12 +1083,12 @@ class _StationsModule extends CoreClass {
 	 *
 	 * @param {object} payload - the payload object
 	 * @param {object} payload.station - the station object
-	 * @param {string} payload.room - the socket.io room to get the sockets from
+	 * @param {string} payload.room - the websockets room to get the sockets from
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	GET_SOCKETS_THAT_CAN_KNOW_ABOUT_STATION(payload) {
 		return new Promise((resolve, reject) => {
-			IOModule.runJob("GET_ROOM_SOCKETS", { room: payload.room }, this)
+			WSModule.runJob("GET_SOCKETS_FOR_ROOM", { room: payload.room }, this)
 				.then(socketsObject => {
 					const sockets = Object.keys(socketsObject).map(socketKey => socketsObject[socketKey]);
 					let socketsThatCan = [];

+ 8 - 5
backend/logic/tasks.js

@@ -12,7 +12,7 @@ let TasksModule;
 let CacheModule;
 let StationsModule;
 let UtilsModule;
-let IOModule;
+let WSModule;
 let DBModule;
 
 class _TasksModule extends CoreClass {
@@ -37,7 +37,7 @@ class _TasksModule extends CoreClass {
 			CacheModule = this.moduleManager.modules.cache;
 			StationsModule = this.moduleManager.modules.stations;
 			UtilsModule = this.moduleManager.modules.utils;
-			IOModule = this.moduleManager.modules.io;
+			WSModule = this.moduleManager.modules.ws;
 			DBModule = this.moduleManager.modules.db;
 
 			// this.createTask("testTask", testTask, 5000, true);
@@ -245,7 +245,7 @@ class _TasksModule extends CoreClass {
 									}).finally(() => next2());
 								}
 								if (Date.now() - session.refreshDate > 60 * 60 * 24 * 30 * 1000) {
-									return IOModule.runJob("SOCKETS_FROM_SESSION_ID", {
+									return WSModule.runJob("SOCKETS_FROM_SESSION_ID", {
 										sessionId: session.sessionId
 									}).then(response => {
 										if (response.sockets.length > 0) {
@@ -360,10 +360,13 @@ class _TasksModule extends CoreClass {
 			async.each(
 				Object.keys(StationsModule.userList),
 				(socketId, next) => {
-					IOModule.runJob("SOCKET_FROM_SESSION", { socketId }).then(socket => {
+					WSModule.runJob("SOCKET_FROM_SOCKET_ID", { socketId }).then(async socket => {
 						const stationId = StationsModule.userList[socketId];
+						const room = await WSModule.runJob("GET_SOCKETS_FOR_ROOM", {
+							room: `station.${stationId}`
+						});
 
-						if (!socket || Object.keys(socket.rooms).indexOf(`station.${stationId}`) === -1) {
+						if (!socket || !room.includes(socketId)) {
 							if (stationsCountUpdated.indexOf(stationId) === -1) stationsCountUpdated.push(stationId);
 							if (stationsUpdated.indexOf(stationId) === -1) stationsUpdated.push(String(stationId));
 

+ 1 - 20
backend/logic/utils.js

@@ -2,7 +2,6 @@ import crypto from "crypto";
 import CoreClass from "../core";
 
 let UtilsModule;
-let IOModule;
 
 class _UtilsModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
@@ -18,11 +17,7 @@ class _UtilsModule extends CoreClass {
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	initialize() {
-		return new Promise(resolve => {
-			IOModule = this.moduleManager.modules.io;
-
-			resolve();
-		});
+		return new Promise(resolve => resolve());
 	}
 
 	/**
@@ -143,20 +138,6 @@ class _UtilsModule extends CoreClass {
 		return new Promise(resolve => resolve(randomChars.join("")));
 	}
 
-	/**
-	 * Returns a socket object from a socket identifier
-	 *
-	 * @param {object} payload - object that contains the payload
-	 * @param {string} payload.socketId - the socket id
-	 * @returns {Promise} - returns promise (reject, resolve)
-	 */
-	async GET_SOCKET_FROM_ID(payload) {
-		// socketId
-		const io = await IOModule.runJob("IO", {}, this);
-
-		return new Promise(resolve => resolve(io.sockets.sockets[payload.socketId]));
-	}
-
 	/**
 	 * Creates a random number within a range
 	 *

+ 271 - 356
backend/logic/io.js → backend/logic/ws.js

@@ -4,27 +4,28 @@
 
 import config from "config";
 import async from "async";
-import socketio from "socket.io";
+import WebSocket from "ws";
+import { EventEmitter } from "events";
 
 import CoreClass from "../core";
 
-let IOModule;
+let WSModule;
 let AppModule;
 let CacheModule;
 let UtilsModule;
 let DBModule;
 let PunishmentsModule;
 
-class _IOModule extends CoreClass {
+class _WSModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
 	constructor() {
-		super("io");
+		super("ws");
 
-		IOModule = this;
+		WSModule = this;
 	}
 
 	/**
-	 * Initialises the io module
+	 * Initialises the ws module
 	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
@@ -48,55 +49,61 @@ class _IOModule extends CoreClass {
 		// 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 AppModule.runJob("SERVER");
 
-		this._io = socketio(server);
 		// this._io.origins(config.get("cors.origin"));
 
+		this._io = new WebSocket.Server({ server, path: "/ws" });
+
+		this.rooms = {};
+
 		return new Promise(resolve => {
 			this.setStage(3);
 
-			this._io.use(async (socket, cb) => {
-				IOModule.runJob("HANDLE_IO_USE", { socket, cb });
-			});
+			this._io.on("connection", async (socket, req) => {
+				socket.dispatch = (...args) => socket.send(JSON.stringify(args));
 
-			this.setStage(4);
+				socket.actions = new EventEmitter();
+				socket.actions.setMaxListeners(0);
+				socket.listen = (target, cb) => socket.actions.addListener(target, args => cb(args));
 
-			this._io.on("connection", async socket => {
-				IOModule.runJob("HANDLE_IO_CONNECTION", { socket });
+				WSModule.runJob("HANDLE_WS_USE", { socket, req }).then(socket =>
+					WSModule.runJob("HANDLE_WS_CONNECTION", { socket })
+				);
 			});
 
-			this.setStage(5);
+			this.setStage(4);
 
 			return resolve();
 		});
 	}
 
 	/**
-	 * Returns the socket io variable
+	 * Returns the websockets variable
 	 *
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
-	IO() {
-		return new Promise(resolve => {
-			resolve(IOModule._io);
-		});
+	WS() {
+		return new Promise(resolve => resolve(WSModule._io));
 	}
 
 	/**
-	 * Returns whether there is a socket for a session id or not
+	 * Obtains socket object for a specified socket id
 	 *
 	 * @param {object} payload - object containing the payload
-	 * @param {string} payload.sessionId - user session id
+	 * @param {string} payload.socketId - the id of the socket
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
-	async SOCKET_FROM_SESSION(payload) {
-		// socketId
-		return new Promise((resolve, reject) => {
-			const ns = IOModule._io.of("/");
-			if (ns) {
-				return resolve(ns.connected[payload.socketId]);
-			}
+	async SOCKET_FROM_SOCKET_ID(payload) {
+		return new Promise(resolve => {
+			const { clients } = WSModule._io;
+
+			if (clients)
+				// eslint-disable-next-line consistent-return
+				clients.forEach(socket => {
+					if (socket.session.socketId === payload.socketId) return resolve(socket);
+				});
 
-			return reject();
+			// socket doesn't exist
+			return resolve();
 		});
 	}
 
@@ -109,20 +116,18 @@ class _IOModule extends CoreClass {
 	 */
 	async SOCKETS_FROM_SESSION_ID(payload) {
 		return new Promise(resolve => {
-			const ns = IOModule._io.of("/");
+			const { clients } = WSModule._io;
 			const sockets = [];
 
-			if (ns) {
+			if (clients) {
 				return async.each(
-					Object.keys(ns.connected),
+					Object.keys(clients),
 					(id, next) => {
-						const { session } = ns.connected[id];
+						const { session } = clients[id];
 						if (session.sessionId === payload.sessionId) sockets.push(session.sessionId);
 						next();
 					},
-					() => {
-						resolve({ sockets });
-					}
+					() => resolve(sockets)
 				);
 			}
 
@@ -139,33 +144,30 @@ class _IOModule extends CoreClass {
 	 */
 	async SOCKETS_FROM_USER(payload) {
 		return new Promise((resolve, reject) => {
-			const ns = IOModule._io.of("/");
 			const sockets = [];
 
-			if (ns) {
-				return async.eachLimit(
-					Object.keys(ns.connected),
-					1,
-					(id, next) => {
-						const { session } = ns.connected[id];
-
-						if (session.sessionId) {
-							CacheModule.runJob("HGET", { table: "sessions", key: session.sessionId }, this)
-								.then(session => {
-									if (session && session.userId === payload.userId) sockets.push(ns.connected[id]);
-									next();
-								})
-								.catch(err => next(err));
-						} else next();
-					},
-					err => {
-						if (err) return reject(err);
-						return resolve({ sockets });
+			return async.eachLimit(
+				WSModule._io.clients,
+				1,
+				(socket, next) => {
+					const { sessionId } = socket.session;
+
+					if (sessionId) {
+						return CacheModule.runJob("HGET", { table: "sessions", key: sessionId }, this)
+							.then(session => {
+								if (session && session.userId === payload.userId) sockets.push(socket);
+								next();
+							})
+							.catch(err => next(err));
 					}
-				);
-			}
 
-			return resolve();
+					return next();
+				},
+				err => {
+					if (err) return reject(err);
+					return resolve(sockets);
+				}
+			);
 		});
 	}
 
@@ -178,35 +180,24 @@ class _IOModule extends CoreClass {
 	 */
 	async SOCKETS_FROM_IP(payload) {
 		return new Promise(resolve => {
-			const ns = IOModule._io.of("/");
+			const { clients } = WSModule._io;
+
 			const sockets = [];
-			if (ns) {
-				return async.each(
-					Object.keys(ns.connected),
-					(id, next) => {
-						const { session } = ns.connected[id];
-
-						CacheModule.runJob(
-							"HGET",
-							{
-								table: "sessions",
-								key: session.sessionId
-							},
-							this
-						)
-							.then(session => {
-								if (session && ns.connected[id].ip === payload.ip) sockets.push(ns.connected[id]);
-								next();
-							})
-							.catch(() => next());
-					},
-					() => {
-						resolve(sockets);
-					}
-				);
-			}
 
-			return resolve();
+			return async.each(
+				Object.keys(clients),
+				(id, next) => {
+					const { session } = clients[id];
+
+					CacheModule.runJob("HGET", { table: "sessions", key: session.sessionId }, this)
+						.then(session => {
+							if (session && clients[id].ip === payload.ip) sockets.push(clients[id]);
+							next();
+						})
+						.catch(() => next());
+				},
+				() => resolve(sockets)
+			);
 		});
 	}
 
@@ -219,20 +210,18 @@ class _IOModule extends CoreClass {
 	 */
 	async SOCKETS_FROM_USER_WITHOUT_CACHE(payload) {
 		return new Promise(resolve => {
-			const ns = IOModule._io.of("/");
+			const { clients } = WSModule._io;
 			const sockets = [];
 
-			if (ns) {
+			if (clients) {
 				return async.each(
-					Object.keys(ns.connected),
+					Object.keys(clients),
 					(id, next) => {
-						const { session } = ns.connected[id];
-						if (session.userId === payload.userId) sockets.push(ns.connected[id]);
+						const { session } = clients[id];
+						if (session.userId === payload.userId) sockets.push(clients[id]);
 						next();
 					},
-					() => {
-						resolve({ sockets });
-					}
+					() => resolve(sockets)
 				);
 			}
 
@@ -248,20 +237,10 @@ class _IOModule extends CoreClass {
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async SOCKET_LEAVE_ROOMS(payload) {
-		const socket = await IOModule.runJob(
-			"SOCKET_FROM_SESSION",
-			{
-				socketId: payload.socketId
-			},
-			this
-		);
-
 		return new Promise(resolve => {
-			const { rooms } = socket;
-
-			Object.keys(rooms).forEach(roomKey => {
-				const room = rooms[roomKey];
-				socket.leave(room);
+			// filter out rooms that the user is in
+			Object.keys(WSModule.rooms).forEach(room => {
+				WSModule.rooms[room] = WSModule.rooms[room].filter(participant => participant !== payload.socketId);
 			});
 
 			return resolve();
@@ -269,118 +248,114 @@ class _IOModule extends CoreClass {
 	}
 
 	/**
-	 * Allows a socket to join a specified room
+	 * Allows a socket to join a specified room (this will remove them from any rooms they are currently in)
 	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.socketId - the id of the socket which should join the room
-	 * @param {object} payload.room - the object representing the room the socket should join
+	 * @param {string} payload.room - the name of the room
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async SOCKET_JOIN_ROOM(payload) {
-		const socket = await IOModule.runJob(
-			"SOCKET_FROM_SESSION",
-			{
-				socketId: payload.socketId
-			},
-			this
-		);
+		const { room, socketId } = payload;
 
-		return new Promise(resolve => {
-			const { rooms } = socket;
-			Object.keys(rooms).forEach(roomKey => {
-				const room = rooms[roomKey];
-				socket.leave(room);
-			});
+		// leave all other rooms
+		await WSModule.runJob("SOCKET_LEAVE_ROOMS", { socketId }, this);
 
-			socket.join(payload.room);
+		return new Promise(resolve => {
+			// create room if it doesn't exist, and add socketId to array
+			if (WSModule.rooms[room]) WSModule.rooms[room].push(socketId);
+			else WSModule.rooms[room] = [socketId];
 
 			return resolve();
 		});
 	}
 
-	// UNKNOWN
-	// eslint-disable-next-line require-jsdoc
-	async SOCKET_JOIN_SONG_ROOM(payload) {
-		// socketId, room
-		const socket = await IOModule.runJob(
-			"SOCKET_FROM_SESSION",
-			{
-				socketId: payload.socketId
-			},
-			this
-		);
-
+	/**
+	 * Emits arguments to any sockets that are in a specified a room
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.room - the name of the room to emit arguments
+	 * @param {object} payload.args - any arguments to be emitted to the sockets in the specific room
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	async EMIT_TO_ROOM(payload) {
 		return new Promise(resolve => {
-			const { rooms } = socket;
-			Object.keys(rooms).forEach(roomKey => {
-				const room = rooms[roomKey];
-				if (room.indexOf("song.") !== -1) socket.leave(room);
-			});
-
-			socket.join(payload.room);
+			// if the room exists
+			if (WSModule.rooms[payload.room])
+				return WSModule.rooms[payload.room].forEach(async socketId => {
+					// get every socketId (and thus every socket) in the room, and dispatch to each
+					const socket = await WSModule.runJob("SOCKET_FROM_SOCKET_ID", { socketId }, this);
+					socket.dispatch(...payload.args);
+					return resolve();
+				});
 
 			return resolve();
 		});
 	}
 
-	// UNKNOWN
-	// eslint-disable-next-line require-jsdoc
-	SOCKETS_JOIN_SONG_ROOM(payload) {
-		// sockets, room
-		return new Promise(resolve => {
-			Object.keys(payload.sockets).forEach(socketKey => {
-				const socket = payload.sockets[socketKey];
+	/**
+	 * Allows a socket to join a 'song' room
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.socketId - the id of the socket which should join the room
+	 * @param {string} payload.room - the name of the room
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	async SOCKET_JOIN_SONG_ROOM(payload) {
+		const { room, socketId } = payload;
 
-				const { rooms } = socket;
-				Object.keys(rooms).forEach(roomKey => {
-					const room = rooms[roomKey];
-					if (room.indexOf("song.") !== -1) socket.leave(room);
-				});
+		// leave any other song rooms the user is in
+		await WSModule.runJob("SOCKETS_LEAVE_SONG_ROOMS", { sockets: [socketId] }, this);
 
-				socket.join(payload.room);
-			});
+		return new Promise(resolve => {
+			// join the room
+			if (WSModule.rooms[room]) WSModule.rooms[room].push(socketId);
+			else WSModule.rooms[room] = [socketId];
 
 			return resolve();
 		});
 	}
 
-	// UNKNOWN
-	// eslint-disable-next-line require-jsdoc
-	SOCKETS_LEAVE_SONG_ROOMS(payload) {
-		// sockets
+	/**
+	 * Allows multiple sockets to join a 'song' room
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {Array} payload.sockets - array of socketIds
+	 * @param {object} payload.room - the name of the room
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	SOCKETS_JOIN_SONG_ROOM(payload) {
 		return new Promise(resolve => {
-			Object.keys(payload.sockets).forEach(socketKey => {
-				const socket = payload.sockets[socketKey];
-				const { rooms } = socket;
-				Object.keys(rooms).forEach(roomKey => {
-					const room = rooms[roomKey];
-					if (room.indexOf("song.") !== -1) socket.leave(room);
-				});
-			});
-			resolve();
+			Promise.allSettled(
+				payload.sockets.map(async socketId => {
+					await WSModule.runJob("SOCKET_JOIN_SONG_ROOM", { socketId }, this);
+				})
+			).then(() => resolve());
 		});
 	}
 
 	/**
-	 * Emits arguments to any sockets that are in a specified a room
+	 * Allows multiple sockets to leave any 'song' rooms they are in
 	 *
 	 * @param {object} payload - object that contains the payload
-	 * @param {string} payload.room - the name of the room to emit arguments
-	 * @param {object} payload.args - any arguments to be emitted to the sockets in the specific room
+	 * @param {Array} payload.sockets - array of socketIds
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
-	async EMIT_TO_ROOM(payload) {
-		return new Promise(resolve => {
-			const { sockets } = IOModule._io.sockets;
-			Object.keys(sockets).forEach(socketKey => {
-				const socket = sockets[socketKey];
-				if (socket.rooms[payload.room]) {
-					socket.emit(...payload.args);
-				}
-			});
-
-			return resolve();
-		});
+	SOCKETS_LEAVE_SONG_ROOMS(payload) {
+		return new Promise(resolve =>
+			Promise.allSettled(
+				payload.sockets.map(async socketId => {
+					const rooms = await WSModule.runJob("GET_ROOMS_FOR_SOCKET", { socketId }, this);
+
+					rooms.forEach(room => {
+						if (room.indexOf("song.") !== -1)
+							WSModule.rooms[room] = WSModule.rooms[room].filter(
+								participant => participant !== payload.socketId
+							);
+					});
+				})
+			).then(() => resolve())
+		);
 	}
 
 	/**
@@ -390,47 +365,55 @@ class _IOModule extends CoreClass {
 	 * @param {string} payload.room - the name of the room
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
-	async GET_ROOM_SOCKETS(payload) {
+	async GET_SOCKETS_FOR_ROOM(payload) {
 		return new Promise(resolve => {
-			const { sockets } = IOModule._io.sockets;
-			const roomSockets = [];
-			Object.keys(sockets).forEach(socketKey => {
-				const socket = sockets[socketKey];
-				if (socket.rooms[payload.room]) roomSockets.push(socket);
+			if (WSModule.rooms[payload.room]) return resolve(WSModule.rooms[payload.room]);
+			return resolve([]);
+		});
+	}
+
+	/**
+	 * Gets any rooms a socket is connected to
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.socketId - the id of the socket to check the rooms for
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	async GET_ROOMS_FOR_SOCKET(payload) {
+		return new Promise(resolve => {
+			const rooms = [];
+
+			Object.keys(WSModule.rooms).forEach(room => {
+				if (WSModule.rooms[room].includes(payload.socketId)) rooms.push(room);
 			});
 
-			return resolve(roomSockets);
+			return resolve(rooms);
 		});
 	}
 
 	/**
-	 * Handles io.use
+	 * Handles use of websockets
 	 *
 	 * @param {object} payload - object that contains the payload
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
-	async HANDLE_IO_USE(payload) {
+	async HANDLE_WS_USE(payload) {
 		return new Promise(resolve => {
-			const { socket, cb } = payload;
+			const { socket, req } = payload;
+			let SID = "";
 
-			let SID;
-
-			socket.ip = socket.request.headers["x-forwarded-for"] || "0.0.0.0";
+			socket.ip = req.headers["x-forwarded-for"] || "0..0.0";
 
 			return async.waterfall(
 				[
 					next => {
-						if (!socket.request.headers.cookie) return next("No cookie exists yet.");
-						return UtilsModule.runJob(
-							"PARSE_COOKIES",
-							{
-								cookieString: socket.request.headers.cookie
-							},
-							this
-						).then(res => {
-							SID = res[IOModule.SIDname];
-							next(null);
-						});
+						if (!req.headers.cookie) return next("No cookie exists yet.");
+						return UtilsModule.runJob("PARSE_COOKIES", { cookieString: req.headers.cookie }, this).then(
+							res => {
+								SID = res[WSModule.SIDname];
+								next(null);
+							}
+						);
 					},
 
 					next => {
@@ -438,11 +421,10 @@ class _IOModule extends CoreClass {
 						return next();
 					},
 
+					// see if session exists for cookie
 					next => {
 						CacheModule.runJob("HGET", { table: "sessions", key: SID }, this)
-							.then(session => {
-								next(null, session);
-							})
+							.then(session => next(null, session))
 							.catch(next);
 					},
 
@@ -455,15 +437,9 @@ class _IOModule extends CoreClass {
 
 						return CacheModule.runJob(
 							"HSET",
-							{
-								table: "sessions",
-								key: SID,
-								value: session
-							},
+							{ table: "sessions", key: SID, value: session },
 							this
-						).then(session => {
-							next(null, session);
-						});
+						).then(session => next(null, session));
 					},
 
 					(res, next) => {
@@ -490,81 +466,61 @@ class _IOModule extends CoreClass {
 
 								next();
 							})
-							.catch(() => {
-								next();
-							});
+							.catch(() => next());
 					}
 				],
 				() => {
-					if (!socket.session) socket.session = { socketId: socket.id };
-					else socket.session.socketId = socket.id;
+					if (!socket.session) socket.session = { socketId: req.headers["sec-websocket-key"] };
+					else socket.session.socketId = req.headers["sec-websocket-key"];
 
-					cb();
-					resolve();
+					resolve(socket);
 				}
 			);
 		});
 	}
 
 	/**
-	 * Handles io.connection
+	 * Handles a websocket connection
 	 *
 	 * @param {object} payload - object that contains the payload
+	 * @param {object} payload.socket - socket itself
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
-	async HANDLE_IO_CONNECTION(payload) {
+	async HANDLE_WS_CONNECTION(payload) {
 		return new Promise(resolve => {
 			const { socket } = payload;
 
 			let sessionInfo = "";
-
 			if (socket.session.sessionId) sessionInfo = ` UserID: ${socket.session.userId}.`;
 
 			// if session is banned
 			if (socket.banishment && socket.banishment.banned) {
-				IOModule.log(
+				WSModule.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.dispatch("keep.event:banned", socket.banishment.ban);
 
-				return socket.disconnect(true);
+				return socket.close(); // close socket connection
 			}
 
-			IOModule.log("INFO", "IO_CONNECTION", `User connected. IP: ${socket.ip}.${sessionInfo}`);
+			WSModule.log("INFO", "IO_CONNECTION", `User connected. IP: ${socket.ip}.${sessionInfo}`);
 
 			// catch when the socket has been disconnected
-			socket.on("disconnect", () => {
+			socket.on("close", async () => {
 				if (socket.session.sessionId) sessionInfo = ` UserID: ${socket.session.userId}.`;
-				IOModule.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];
+				WSModule.log("INFO", "IO_DISCONNECTION", `User disconnected. IP: ${socket.ip}.${sessionInfo}`);
 
-				if (!namespace) return next(new Error("Invalid namespace."));
-				if (!action) return next(new Error("Invalid action."));
-				if (!IOModule.actions[namespace]) return next(new Error("Namespace not found."));
-				if (!IOModule.actions[namespace][action]) return next(new Error("Action not found."));
-
-				return next();
+				// leave all rooms when a socket connection is closed (to prevent rooms object building up)
+				await WSModule.runJob("SOCKET_LEAVE_ROOMS", { socketId: socket.session.socketId });
 			});
 
-			// catch errors on the socket (internal to socket.io)
-			socket.on("error", console.error);
+			// catch errors on the socket
+			socket.onerror = error => {
+				console.error("SOCKET ERROR: ", error);
+			};
 
 			if (socket.session.sessionId) {
 				CacheModule.runJob("HGET", {
@@ -573,112 +529,68 @@ class _IOModule extends CoreClass {
 				})
 					.then(session => {
 						if (session && session.userId) {
-							IOModule.userModel.findOne({ _id: session.userId }, (err, user) => {
-								if (err || !user) return socket.emit("ready", false);
+							WSModule.userModel.findOne({ _id: session.userId }, (err, user) => {
+								if (err || !user) return socket.dispatch("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);
+								return socket.dispatch("ready", true, role, username, userId);
 							});
-						} else socket.emit("ready", false);
+						} else socket.dispatch("ready", false);
 					})
-					.catch(() => {
-						socket.emit("ready", false);
-					});
-			} else socket.emit("ready", false);
+					.catch(() => socket.dispatch("ready", false));
+			} else socket.dispatch("ready", false);
+
+			socket.onmessage = message => {
+				const data = JSON.parse(message.data);
+
+				if (data.length === 0) return socket.dispatch("ERROR", "Not enough arguments specified.");
+				if (typeof data[0] !== "string") return socket.dispatch("ERROR", "First argument must be a string.");
+
+				const namespaceAction = data[0];
+				if (
+					!namespaceAction ||
+					namespaceAction.indexOf(".") === -1 ||
+					namespaceAction.indexOf(".") !== namespaceAction.lastIndexOf(".")
+				)
+					return socket.dispatch("ERROR", "Invalid first argument");
+
+				const namespace = data[0].split(".")[0];
+				const action = data[0].split(".")[1];
+
+				if (!namespace) return socket.dispatch("ERROR", "Invalid namespace.");
+				if (!action) return socket.dispatch("ERROR", "Invalid action.");
+				if (!WSModule.actions[namespace]) return socket.dispatch("ERROR", "Namespace not found.");
+				if (!WSModule.actions[namespace][action]) return socket.dispatch("ERROR", "Action not found.");
+
+				if (data[data.length - 1].CB_REF) {
+					const { CB_REF } = data[data.length - 1];
+					data.pop();
+
+					return socket.actions.emit(data.shift(0), [...data, res => socket.dispatch("CB_REF", CB_REF, res)]);
+				}
+
+				return socket.actions.emit(data.shift(0), data);
+			};
 
 			// have the socket listen for each action
-			Object.keys(IOModule.actions).forEach(namespace => {
-				Object.keys(IOModule.actions[namespace]).forEach(action => {
+			Object.keys(WSModule.actions).forEach(namespace => {
+				Object.keys(WSModule.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) => {
-						IOModule.runJob("RUN_ACTION", { socket, namespace, action, args });
-
-						/* let cb = args[args.length - 1];
-
-						if (typeof cb !== "function")
-							cb = () => {
-								IOModule.log("INFO", "IO_MODULE", `There was no callback provided for ${name}.`);
-							};
-						else args.pop();
-
-						if (this.getStatus() !== "READY") {
-							IOModule.log(
-								"INFO",
-								"IO_REJECTED_ACTION",
-								`A user tried to execute an action, but the IO module is currently not ready. Action: ${namespace}.${action}.`
-							);
-							return;
-						}
-						IOModule.log("INFO", "IO_ACTION", `A user executed an action. Action: ${namespace}.${action}.`);
-
-						let failedGettingSession = false;
-						// load the session from the cache
-						if (socket.session.sessionId)
-							await CacheModule.runJob("HGET", {
-								table: "sessions",
-								key: socket.session.sessionId
-							})
-								.then(session => {
-									// make sure the sockets sessionId isn't set if there is no session
-									if (socket.session.sessionId && session === null) delete socket.session.sessionId;
-								})
-								.catch(() => {
-									failedGettingSession = true;
-									if (typeof cb === "function")
-										cb({
-											status: "error",
-											message: "An error occurred while obtaining your session"
-										});
-								});
-						if (!failedGettingSession)
-							try {
-								// call the action, passing it the session, and the arguments socket.io passed us
-								this.runJob("RUN_ACTION", { namespace, action, session: socket.session, args })
-									.then(response => {
-										if (typeof cb === "function") cb(response);
-									})
-									.catch(err => {
-										if (typeof cb === "function") cb(err);
-									});
-								// actions[namespace][action].apply(
-								// 	null,
-								// 	[socket.session].concat(args).concat([
-								// 		result => {
-								// 			IOModule.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."
-									});
-
-								IOModule.log(
-									"ERROR",
-									"IO_ACTION_ERROR",
-									`Some type of exception occurred in the action ${namespace}.${action}. Error message: ${err.message}`
-								);
-							} */
-					});
+					socket.listen(name, async args =>
+						WSModule.runJob("RUN_ACTION", { socket, namespace, action, args })
+					);
 				});
 			});
 
@@ -700,13 +612,14 @@ class _IOModule extends CoreClass {
 			const name = `${namespace}.${action}`;
 
 			let cb = args[args.length - 1];
+
 			if (typeof cb !== "function")
 				cb = () => {
-					IOModule.log("INFO", "IO_MODULE", `There was no callback provided for ${name}.`);
+					WSModule.log("INFO", "IO_MODULE", `There was no callback provided for ${name}.`);
 				};
 			else args.pop();
 
-			IOModule.log("INFO", "IO_ACTION", `A user executed an action. Action: ${namespace}.${action}.`);
+			WSModule.log("INFO", "IO_ACTION", `A user executed an action. Action: ${namespace}.${action}.`);
 
 			// load the session from the cache
 			new Promise(resolve => {
@@ -731,9 +644,9 @@ class _IOModule extends CoreClass {
 				else resolve();
 			})
 				.then(() => {
-					// call the job that calls the action, passing it the session, and the arguments socket.io passed us
+					// call the job that calls the action, passing it the session, and the arguments the websocket passed us
 
-					IOModule.runJob("RUN_ACTION2", { session: socket.session, namespace, action, args }, this)
+					WSModule.runJob("RUN_ACTION2", { session: socket.session, namespace, action, args }, this)
 						.then(response => {
 							cb(response);
 							resolve();
@@ -744,9 +657,10 @@ class _IOModule extends CoreClass {
 									status: "error",
 									message: "An error occurred while executing the specified action."
 								});
+
 							reject(err);
 
-							IOModule.log(
+							WSModule.log(
 								"ERROR",
 								"IO_ACTION_ERROR",
 								`Some type of exception occurred in the action ${namespace}.${action}. Error message: ${err.message}`
@@ -768,16 +682,17 @@ class _IOModule extends CoreClass {
 			const { session, namespace, action, args } = payload;
 
 			try {
-				// call the the action, passing it the session, and the arguments socket.io passed us
-				IOModule.actions[namespace][action].apply(
+				// call the the action, passing it the session, and the arguments the websocket passed us
+				WSModule.actions[namespace][action].apply(
 					this,
 					[session].concat(args).concat([
 						result => {
-							IOModule.log(
+							WSModule.log(
 								"INFO",
 								"RUN_ACTION2",
 								`Response to action. Action: ${namespace}.${action}. Response status: ${result.status}`
 							);
+
 							resolve(result);
 						}
 					])
@@ -785,7 +700,7 @@ class _IOModule extends CoreClass {
 			} catch (err) {
 				reject(err);
 
-				IOModule.log(
+				WSModule.log(
 					"ERROR",
 					"IO_ACTION_ERROR",
 					`Some type of exception occurred in the action ${namespace}.${action}. Error message: ${err.message}`
@@ -795,4 +710,4 @@ class _IOModule extends CoreClass {
 	}
 }
 
-export default new _IOModule();
+export default new _WSModule();

+ 24 - 575
backend/package-lock.json

@@ -24,8 +24,8 @@
         "oauth": "^0.9.15",
         "redis": "^2.8.0",
         "sha256": "^0.2.0",
-        "socket.io": "2.4.1",
-        "underscore": "^1.10.2"
+        "underscore": "^1.10.2",
+        "ws": "^7.4.3"
       },
       "devDependencies": {
         "eslint": "^7.16.0",
@@ -231,11 +231,6 @@
       "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
       "dev": true
     },
-    "node_modules/after": {
-      "version": "0.8.2",
-      "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
-      "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
-    },
     "node_modules/ajv": {
       "version": "6.12.6",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -335,11 +330,6 @@
         "node": ">= 0.4"
       }
     },
-    "node_modules/arraybuffer.slice": {
-      "version": "0.0.7",
-      "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
-      "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog=="
-    },
     "node_modules/astral-regex": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
@@ -362,32 +352,11 @@
         "follow-redirects": "^1.10.0"
       }
     },
-    "node_modules/backo2": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
-      "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
-    },
     "node_modules/balanced-match": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
       "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
     },
-    "node_modules/base64-arraybuffer": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
-      "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=",
-      "engines": {
-        "node": ">= 0.6.0"
-      }
-    },
-    "node_modules/base64id": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
-      "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
-      "engines": {
-        "node": "^4.5.0 || >= 5.9"
-      }
-    },
     "node_modules/bcrypt": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.0.tgz",
@@ -410,11 +379,6 @@
         "safe-buffer": "^5.1.1"
       }
     },
-    "node_modules/blob": {
-      "version": "0.0.5",
-      "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
-      "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig=="
-    },
     "node_modules/bluebird": {
       "version": "3.7.2",
       "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@@ -550,21 +514,6 @@
         "node": ">= 6.0.0"
       }
     },
-    "node_modules/component-bind": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
-      "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E="
-    },
-    "node_modules/component-emitter": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
-      "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
-    },
-    "node_modules/component-inherit": {
-      "version": "0.0.3",
-      "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
-      "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM="
-    },
     "node_modules/concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -630,14 +579,6 @@
       "resolved": "https://registry.npmjs.org/convert-string/-/convert-string-0.1.0.tgz",
       "integrity": "sha1-ec5BqbsNA7z3LNxqjzxW+7xkQQo="
     },
-    "node_modules/cookie": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
-      "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
     "node_modules/cookie-parser": {
       "version": "1.4.5",
       "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz",
@@ -801,89 +742,6 @@
         "node": ">= 0.8"
       }
     },
-    "node_modules/engine.io": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.5.0.tgz",
-      "integrity": "sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==",
-      "dependencies": {
-        "accepts": "~1.3.4",
-        "base64id": "2.0.0",
-        "cookie": "~0.4.1",
-        "debug": "~4.1.0",
-        "engine.io-parser": "~2.2.0",
-        "ws": "~7.4.2"
-      },
-      "engines": {
-        "node": ">=8.0.0"
-      }
-    },
-    "node_modules/engine.io-client": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.0.tgz",
-      "integrity": "sha512-12wPRfMrugVw/DNyJk34GQ5vIVArEcVMXWugQGGuw2XxUSztFNmJggZmv8IZlLyEdnpO1QB9LkcjeWewO2vxtA==",
-      "dependencies": {
-        "component-emitter": "~1.3.0",
-        "component-inherit": "0.0.3",
-        "debug": "~3.1.0",
-        "engine.io-parser": "~2.2.0",
-        "has-cors": "1.1.0",
-        "indexof": "0.0.1",
-        "parseqs": "0.0.6",
-        "parseuri": "0.0.6",
-        "ws": "~7.4.2",
-        "xmlhttprequest-ssl": "~1.5.4",
-        "yeast": "0.1.2"
-      }
-    },
-    "node_modules/engine.io-client/node_modules/debug": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
-      "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
-      "dependencies": {
-        "ms": "2.0.0"
-      }
-    },
-    "node_modules/engine.io-client/node_modules/ms": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
-    },
-    "node_modules/engine.io-client/node_modules/ws": {
-      "version": "7.4.2",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz",
-      "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==",
-      "engines": {
-        "node": ">=8.3.0"
-      }
-    },
-    "node_modules/engine.io-parser": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz",
-      "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==",
-      "dependencies": {
-        "after": "0.8.2",
-        "arraybuffer.slice": "~0.0.7",
-        "base64-arraybuffer": "0.1.4",
-        "blob": "0.0.5",
-        "has-binary2": "~1.0.2"
-      }
-    },
-    "node_modules/engine.io/node_modules/debug": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
-      "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
-      "dependencies": {
-        "ms": "^2.1.1"
-      }
-    },
-    "node_modules/engine.io/node_modules/ws": {
-      "version": "7.4.2",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz",
-      "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==",
-      "engines": {
-        "node": ">=8.3.0"
-      }
-    },
     "node_modules/enquirer": {
       "version": "2.3.6",
       "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
@@ -1797,24 +1655,6 @@
         "node": ">= 0.4.0"
       }
     },
-    "node_modules/has-binary2": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
-      "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
-      "dependencies": {
-        "isarray": "2.0.1"
-      }
-    },
-    "node_modules/has-binary2/node_modules/isarray": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
-      "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
-    },
-    "node_modules/has-cors": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
-      "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
-    },
     "node_modules/has-flag": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -1936,11 +1776,6 @@
         "node": ">=0.8.19"
       }
     },
-    "node_modules/indexof": {
-      "version": "0.0.1",
-      "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
-      "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
-    },
     "node_modules/inflight": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -2734,16 +2569,6 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/parseqs": {
-      "version": "0.0.6",
-      "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
-      "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w=="
-    },
-    "node_modules/parseuri": {
-      "version": "0.0.6",
-      "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
-      "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow=="
-    },
     "node_modules/parseurl": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -3221,106 +3046,6 @@
       "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
       "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E="
     },
-    "node_modules/socket.io": {
-      "version": "2.4.1",
-      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.4.1.tgz",
-      "integrity": "sha512-Si18v0mMXGAqLqCVpTxBa8MGqriHGQh8ccEOhmsmNS3thNCGBwO8WGrwMibANsWtQQ5NStdZwHqZR3naJVFc3w==",
-      "dependencies": {
-        "debug": "~4.1.0",
-        "engine.io": "~3.5.0",
-        "has-binary2": "~1.0.2",
-        "socket.io-adapter": "~1.1.0",
-        "socket.io-client": "2.4.0",
-        "socket.io-parser": "~3.4.0"
-      }
-    },
-    "node_modules/socket.io-adapter": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz",
-      "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g=="
-    },
-    "node_modules/socket.io-client": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz",
-      "integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==",
-      "dependencies": {
-        "backo2": "1.0.2",
-        "component-bind": "1.0.0",
-        "component-emitter": "~1.3.0",
-        "debug": "~3.1.0",
-        "engine.io-client": "~3.5.0",
-        "has-binary2": "~1.0.2",
-        "indexof": "0.0.1",
-        "parseqs": "0.0.6",
-        "parseuri": "0.0.6",
-        "socket.io-parser": "~3.3.0",
-        "to-array": "0.1.4"
-      }
-    },
-    "node_modules/socket.io-client/node_modules/debug": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
-      "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
-      "dependencies": {
-        "ms": "2.0.0"
-      }
-    },
-    "node_modules/socket.io-client/node_modules/isarray": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
-      "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
-    },
-    "node_modules/socket.io-client/node_modules/ms": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
-    },
-    "node_modules/socket.io-client/node_modules/socket.io-parser": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz",
-      "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==",
-      "dependencies": {
-        "component-emitter": "~1.3.0",
-        "debug": "~3.1.0",
-        "isarray": "2.0.1"
-      }
-    },
-    "node_modules/socket.io-parser": {
-      "version": "3.4.1",
-      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz",
-      "integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==",
-      "dependencies": {
-        "component-emitter": "1.2.1",
-        "debug": "~4.1.0",
-        "isarray": "2.0.1"
-      }
-    },
-    "node_modules/socket.io-parser/node_modules/component-emitter": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
-      "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
-    },
-    "node_modules/socket.io-parser/node_modules/debug": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
-      "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
-      "dependencies": {
-        "ms": "^2.1.1"
-      }
-    },
-    "node_modules/socket.io-parser/node_modules/isarray": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
-      "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
-    },
-    "node_modules/socket.io/node_modules/debug": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
-      "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
-      "dependencies": {
-        "ms": "^2.1.1"
-      }
-    },
     "node_modules/sparse-bitfield": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
@@ -3568,11 +3293,6 @@
       "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
       "dev": true
     },
-    "node_modules/to-array": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
-      "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA="
-    },
     "node_modules/toidentifier": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
@@ -3737,23 +3457,30 @@
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
       "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
     },
-    "node_modules/xmlhttprequest-ssl": {
-      "version": "1.5.5",
-      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
-      "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=",
+    "node_modules/ws": {
+      "version": "7.4.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.3.tgz",
+      "integrity": "sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA==",
       "engines": {
-        "node": ">=0.4.0"
+        "node": ">=8.3.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": "^5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
       }
     },
     "node_modules/yallist": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
       "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
-    },
-    "node_modules/yeast": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
-      "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
     }
   },
   "dependencies": {
@@ -3924,11 +3651,6 @@
       "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
       "dev": true
     },
-    "after": {
-      "version": "0.8.2",
-      "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
-      "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
-    },
     "ajv": {
       "version": "6.12.6",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -4013,11 +3735,6 @@
         "es-abstract": "^1.18.0-next.1"
       }
     },
-    "arraybuffer.slice": {
-      "version": "0.0.7",
-      "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
-      "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog=="
-    },
     "astral-regex": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
@@ -4037,26 +3754,11 @@
         "follow-redirects": "^1.10.0"
       }
     },
-    "backo2": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
-      "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
-    },
     "balanced-match": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
       "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
     },
-    "base64-arraybuffer": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
-      "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI="
-    },
-    "base64id": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
-      "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
-    },
     "bcrypt": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.0.tgz",
@@ -4075,11 +3777,6 @@
         "safe-buffer": "^5.1.1"
       }
     },
-    "blob": {
-      "version": "0.0.5",
-      "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
-      "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig=="
-    },
     "bluebird": {
       "version": "3.7.2",
       "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@@ -4193,21 +3890,6 @@
       "integrity": "sha512-GKNxVA7/iuTnAqGADlTWX4tkhzxZKXp5fLJqKTlQLHkE65XDUKutZ3BHaJC5IGcper2tT3QRD1xr4o3jNpgXXg==",
       "dev": true
     },
-    "component-bind": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
-      "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E="
-    },
-    "component-emitter": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
-      "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
-    },
-    "component-inherit": {
-      "version": "0.0.3",
-      "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
-      "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM="
-    },
     "concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -4261,11 +3943,6 @@
       "resolved": "https://registry.npmjs.org/convert-string/-/convert-string-0.1.0.tgz",
       "integrity": "sha1-ec5BqbsNA7z3LNxqjzxW+7xkQQo="
     },
-    "cookie": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
-      "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
-    },
     "cookie-parser": {
       "version": "1.4.5",
       "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz",
@@ -4395,84 +4072,6 @@
       "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
       "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
     },
-    "engine.io": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.5.0.tgz",
-      "integrity": "sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==",
-      "requires": {
-        "accepts": "~1.3.4",
-        "base64id": "2.0.0",
-        "cookie": "~0.4.1",
-        "debug": "~4.1.0",
-        "engine.io-parser": "~2.2.0",
-        "ws": "~7.4.2"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
-          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
-          "requires": {
-            "ms": "^2.1.1"
-          }
-        },
-        "ws": {
-          "version": "7.4.2",
-          "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz",
-          "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA=="
-        }
-      }
-    },
-    "engine.io-client": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.0.tgz",
-      "integrity": "sha512-12wPRfMrugVw/DNyJk34GQ5vIVArEcVMXWugQGGuw2XxUSztFNmJggZmv8IZlLyEdnpO1QB9LkcjeWewO2vxtA==",
-      "requires": {
-        "component-emitter": "~1.3.0",
-        "component-inherit": "0.0.3",
-        "debug": "~3.1.0",
-        "engine.io-parser": "~2.2.0",
-        "has-cors": "1.1.0",
-        "indexof": "0.0.1",
-        "parseqs": "0.0.6",
-        "parseuri": "0.0.6",
-        "ws": "~7.4.2",
-        "xmlhttprequest-ssl": "~1.5.4",
-        "yeast": "0.1.2"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
-          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
-          "requires": {
-            "ms": "2.0.0"
-          }
-        },
-        "ms": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
-        },
-        "ws": {
-          "version": "7.4.2",
-          "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz",
-          "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA=="
-        }
-      }
-    },
-    "engine.io-parser": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz",
-      "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==",
-      "requires": {
-        "after": "0.8.2",
-        "arraybuffer.slice": "~0.0.7",
-        "base64-arraybuffer": "0.1.4",
-        "blob": "0.0.5",
-        "has-binary2": "~1.0.2"
-      }
-    },
     "enquirer": {
       "version": "2.3.6",
       "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
@@ -5231,26 +4830,6 @@
         "function-bind": "^1.1.1"
       }
     },
-    "has-binary2": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
-      "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
-      "requires": {
-        "isarray": "2.0.1"
-      },
-      "dependencies": {
-        "isarray": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
-          "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
-        }
-      }
-    },
-    "has-cors": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
-      "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
-    },
     "has-flag": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -5349,11 +4928,6 @@
       "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
       "dev": true
     },
-    "indexof": {
-      "version": "0.0.1",
-      "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
-      "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
-    },
     "inflight": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -5986,16 +5560,6 @@
         "error-ex": "^1.2.0"
       }
     },
-    "parseqs": {
-      "version": "0.0.6",
-      "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
-      "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w=="
-    },
-    "parseuri": {
-      "version": "0.0.6",
-      "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
-      "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow=="
-    },
     "parseurl": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -6380,112 +5944,6 @@
       "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
       "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E="
     },
-    "socket.io": {
-      "version": "2.4.1",
-      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.4.1.tgz",
-      "integrity": "sha512-Si18v0mMXGAqLqCVpTxBa8MGqriHGQh8ccEOhmsmNS3thNCGBwO8WGrwMibANsWtQQ5NStdZwHqZR3naJVFc3w==",
-      "requires": {
-        "debug": "~4.1.0",
-        "engine.io": "~3.5.0",
-        "has-binary2": "~1.0.2",
-        "socket.io-adapter": "~1.1.0",
-        "socket.io-client": "2.4.0",
-        "socket.io-parser": "~3.4.0"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
-          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
-          "requires": {
-            "ms": "^2.1.1"
-          }
-        }
-      }
-    },
-    "socket.io-adapter": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz",
-      "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g=="
-    },
-    "socket.io-client": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz",
-      "integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==",
-      "requires": {
-        "backo2": "1.0.2",
-        "component-bind": "1.0.0",
-        "component-emitter": "~1.3.0",
-        "debug": "~3.1.0",
-        "engine.io-client": "~3.5.0",
-        "has-binary2": "~1.0.2",
-        "indexof": "0.0.1",
-        "parseqs": "0.0.6",
-        "parseuri": "0.0.6",
-        "socket.io-parser": "~3.3.0",
-        "to-array": "0.1.4"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
-          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
-          "requires": {
-            "ms": "2.0.0"
-          }
-        },
-        "isarray": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
-          "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
-        },
-        "ms": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
-        },
-        "socket.io-parser": {
-          "version": "3.3.2",
-          "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz",
-          "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==",
-          "requires": {
-            "component-emitter": "~1.3.0",
-            "debug": "~3.1.0",
-            "isarray": "2.0.1"
-          }
-        }
-      }
-    },
-    "socket.io-parser": {
-      "version": "3.4.1",
-      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz",
-      "integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==",
-      "requires": {
-        "component-emitter": "1.2.1",
-        "debug": "~4.1.0",
-        "isarray": "2.0.1"
-      },
-      "dependencies": {
-        "component-emitter": {
-          "version": "1.2.1",
-          "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
-          "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
-        },
-        "debug": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
-          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
-          "requires": {
-            "ms": "^2.1.1"
-          }
-        },
-        "isarray": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
-          "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
-        }
-      }
-    },
     "sparse-bitfield": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
@@ -6695,11 +6153,6 @@
       "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
       "dev": true
     },
-    "to-array": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
-      "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA="
-    },
     "toidentifier": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
@@ -6830,20 +6283,16 @@
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
       "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
     },
-    "xmlhttprequest-ssl": {
-      "version": "1.5.5",
-      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
-      "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4="
+    "ws": {
+      "version": "7.4.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.3.tgz",
+      "integrity": "sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA==",
+      "requires": {}
     },
     "yallist": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
       "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
-    },
-    "yeast": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
-      "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
     }
   }
 }

+ 2 - 2
backend/package.json

@@ -30,8 +30,8 @@
     "oauth": "^0.9.15",
     "redis": "^2.8.0",
     "sha256": "^0.2.0",
-    "socket.io": "2.4.1",
-    "underscore": "^1.10.2"
+    "underscore": "^1.10.2",
+    "ws": "^7.4.3"
   },
   "devDependencies": {
     "eslint": "^7.16.0",

+ 3 - 2
frontend/dist/config/template.json

@@ -3,7 +3,8 @@
 		"key": "",
 		"enabled": false
 	},
-	"serverDomain": "http://localhost:8080",
+	"apiDomain": "http://localhost:8080",
+	"websocketsDomain": "ws://localhost:8080/ws",
 	"frontendDomain": "http://localhost",
 	"frontendPort": "81",
   	"cookie": {
@@ -18,5 +19,5 @@
 		"github": "https://github.com/Musare/MusareNode"
 	},
 	"skipConfigVersionCheck": false,
-	"configVersion": 1
+	"configVersion": 2
 }

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

@@ -36,7 +36,6 @@
 	<link rel='stylesheet' href='/index.css'>
 	<script src='https://www.youtube.com/iframe_api'></script>
 	<script type='text/javascript' src='/vendor/can-autoplay.min.js'></script>
-	<script src="/vendor/socket.io.2.2.0.js"></script>
 	<script type='text/javascript' src='/vendor/lofig.1.3.3.min.js'></script>
 </head>
 <body>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 5
frontend/dist/vendor/socket.io.2.2.0.js


+ 36 - 37
frontend/src/App.vue

@@ -11,14 +11,14 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapActions, mapGetters } from "vuex";
 import Toast from "toasters";
 
 import Banned from "./pages/Banned.vue";
 import WhatIsNew from "./components/modals/WhatIsNew.vue";
 import LoginModal from "./components/modals/Login.vue";
 import RegisterModal from "./components/modals/Register.vue";
-import io from "./io";
+import ws from "./ws";
 import keyboardShortcuts from "./keyboardShortcuts";
 
 export default {
@@ -31,24 +31,28 @@ export default {
 	replace: false,
 	data() {
 		return {
-			serverDomain: "",
+			apiDomain: "",
 			socketConnected: true,
 			keyIsDown: false
 		};
 	},
-	computed: mapState({
-		loggedIn: state => state.user.auth.loggedIn,
-		role: state => state.user.auth.role,
-		username: state => state.user.auth.username,
-		userId: state => state.user.auth.userId,
-		banned: state => state.user.auth.banned,
-		modals: state => state.modalVisibility.modals,
-		currentlyActive: state => state.modalVisibility.currentlyActive,
-		nightmode: state => state.user.preferences.nightmode
-	}),
+	computed: {
+		...mapState({
+			loggedIn: state => state.user.auth.loggedIn,
+			role: state => state.user.auth.role,
+			username: state => state.user.auth.username,
+			userId: state => state.user.auth.userId,
+			banned: state => state.user.auth.banned,
+			modals: state => state.modalVisibility.modals,
+			currentlyActive: state => state.modalVisibility.currentlyActive,
+			nightmode: state => state.user.preferences.nightmode
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
+		})
+	},
 	watch: {
 		socketConnected(connected) {
-			console.log(connected);
 			if (!connected)
 				new Toast({
 					content: "Could not connect to the server.",
@@ -108,17 +112,15 @@ export default {
 			localStorage.removeItem("github_redirect");
 		}
 
-		io.onConnect(true, () => {
+		ws.onConnect(true, () => {
 			this.socketConnected = true;
 		});
-		io.onConnectError(true, () => {
-			this.socketConnected = false;
-		});
-		io.onDisconnect(true, () => {
+
+		ws.onDisconnect(true, () => {
 			this.socketConnected = false;
 		});
 
-		this.serverDomain = await lofig.get("serverDomain");
+		this.apiDomain = await lofig.get("apiDomain");
 
 		this.$router.onReady(() => {
 			if (this.$route.query.err) {
@@ -138,24 +140,21 @@ export default {
 				new Toast({ content: msg, timeout: 20000 });
 			}
 		});
-		io.getSocket(true, socket => {
-			this.socket = socket;
-
-			this.socket.emit("users.getPreferences", res => {
-				if (res.status === "success") {
-					this.changeAutoSkipDisliked(res.data.autoSkipDisliked);
-					this.changeNightmode(res.data.nightmode);
-					this.changeActivityLogPublic(res.data.activityLogPublic);
-
-					if (this.nightmode) this.enableNightMode();
-					else this.disableNightMode();
-				}
-			});
-
-			this.socket.on("keep.event:user.session.removed", () =>
-				window.location.reload()
-			);
+
+		this.socket.dispatch("users.getPreferences", res => {
+			if (res.status === "success") {
+				this.changeAutoSkipDisliked(res.data.autoSkipDisliked);
+				this.changeNightmode(res.data.nightmode);
+				this.changeActivityLogPublic(res.data.activityLogPublic);
+
+				if (this.nightmode) this.enableNightMode();
+				else this.disableNightMode();
+			}
 		});
+
+		this.socket.on("keep.event:user.session.removed", () =>
+			window.location.reload()
+		);
 	},
 	methods: {
 		submitOnEnter: (cb, event) => {

+ 3 - 1
frontend/src/api/admin/index.js

@@ -1,6 +1,8 @@
+/* eslint-disable import/no-cycle */
+
 import reports from "./reports";
 
-// when Vuex needs to interact with socket.io
+// when Vuex needs to interact with websockets
 
 export default {
 	reports

+ 11 - 11
frontend/src/api/admin/reports.js

@@ -1,17 +1,17 @@
+/* eslint-disable import/no-cycle */
+
 import Toast from "toasters";
-import io from "../../io";
+import ws from "../../ws";
 
 export default {
 	resolve(reportId) {
-		return new Promise((resolve, reject) => {
-			io.getSocket(socket => {
-				socket.emit("reports.resolve", reportId, res => {
-					new Toast({ content: res.message, timeout: 3000 });
-					if (res.status === "success")
-						return resolve({ status: "success" });
-					return reject(new Error(res.message));
-				});
-			});
-		});
+		return new Promise((resolve, reject) =>
+			ws.socket.dispatch("reports.resolve", reportId, res => {
+				new Toast({ content: res.message, timeout: 3000 });
+				if (res.status === "success")
+					return resolve({ status: "success" });
+				return reject(new Error(res.message));
+			})
+		);
 	}
 };

+ 64 - 69
frontend/src/api/auth.js

@@ -1,96 +1,91 @@
+/* eslint-disable import/no-cycle */
+
 import Toast from "toasters";
-import io from "../io";
+import ws from "../ws";
 
-// when Vuex needs to interact with socket.io
+// when Vuex needs to interact with websockets
 
 export default {
 	register(user) {
 		return new Promise((resolve, reject) => {
 			const { username, email, password, recaptchaToken } = user;
 
-			io.getSocket(socket => {
-				socket.emit(
-					"users.register",
-					username,
-					email,
-					password,
-					recaptchaToken,
-					res => {
-						if (res.status === "success") {
-							if (res.SID) {
-								return lofig.get("cookie").then(cookie => {
-									const date = new Date();
-									date.setTime(
-										new Date().getTime() +
-											2 * 365 * 24 * 60 * 60 * 1000
-									);
-									const secure = cookie.secure
-										? "secure=true; "
-										: "";
-									document.cookie = `SID=${
-										res.SID
-									}; expires=${date.toGMTString()}; domain=${
-										cookie.domain
-									}; ${secure}path=/`;
+			ws.socket.dispatch(
+				"users.register",
+				username,
+				email,
+				password,
+				recaptchaToken,
+				res => {
+					if (res.status === "success") {
+						if (res.SID) {
+							return lofig.get("cookie").then(cookie => {
+								const date = new Date();
+								date.setTime(
+									new Date().getTime() +
+										2 * 365 * 24 * 60 * 60 * 1000
+								);
+								const secure = cookie.secure
+									? "secure=true; "
+									: "";
+								document.cookie = `SID=${
+									res.SID
+								}; expires=${date.toGMTString()}; domain=${
+									cookie.domain
+								}; ${secure}path=/`;
 
-									return resolve({
-										status: "success",
-										message: "Account registered!"
-									});
+								return resolve({
+									status: "success",
+									message: "Account registered!"
 								});
-							}
-							return reject(new Error("You must login"));
+							});
 						}
-
-						return reject(new Error(res.message));
+						return reject(new Error("You must login"));
 					}
-				);
-			});
+
+					return reject(new Error(res.message));
+				}
+			);
 		});
 	},
 	login(user) {
 		return new Promise((resolve, reject) => {
 			const { email, password } = user;
 
-			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();
-							date.setTime(
-								new Date().getTime() +
-									2 * 365 * 24 * 60 * 60 * 1000
-							);
-							const secure = cookie.secure ? "secure=true; " : "";
-							let domain = "";
-							if (cookie.domain !== "localhost")
-								domain = ` domain=${cookie.domain};`;
-							document.cookie = `${cookie.SIDname}=${
-								res.SID
-							}; expires=${date.toGMTString()}; ${domain}${secure}path=/`;
-							return resolve({ status: "success" });
-						});
-					}
+			ws.socket.dispatch("users.login", email, password, res => {
+				console.log(123, res);
+				if (res.status === "success") {
+					return lofig.get("cookie").then(cookie => {
+						const date = new Date();
+						date.setTime(
+							new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000
+						);
+						const secure = cookie.secure ? "secure=true; " : "";
+						let domain = "";
+						if (cookie.domain !== "localhost")
+							domain = ` domain=${cookie.domain};`;
+						document.cookie = `${cookie.SIDname}=${
+							res.SID
+						}; expires=${date.toGMTString()}; ${domain}${secure}path=/`;
+						return resolve({ status: "success" });
+					});
+				}
 
-					return reject(new Error(res.message));
-				});
+				return reject(new Error(res.message));
 			});
 		});
 	},
 	logout() {
 		return new Promise((resolve, reject) => {
-			io.getSocket(socket => {
-				socket.emit("users.logout", res => {
-					if (res.status === "success") {
-						return lofig.get("cookie").then(cookie => {
-							document.cookie = `${cookie.SIDname}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
-							return window.location.reload();
-						});
-					}
-					new Toast({ content: res.message, timeout: 4000 });
-					return reject(new Error(res.message));
-				});
+			ws.socket.dispatch("users.logout", res => {
+				if (res.status === "success") {
+					return lofig.get("cookie").then(cookie => {
+						document.cookie = `${cookie.SIDname}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
+						return window.location.reload();
+					});
+				}
+				new Toast({ content: res.message, timeout: 4000 });
+				return reject(new Error(res.message));
 			});
 		});
 	}

+ 17 - 17
frontend/src/components/modals/AddSongToQueue.vue

@@ -206,7 +206,7 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapGetters, mapActions } from "vuex";
 
 import Toast from "toasters";
 
@@ -216,8 +216,6 @@ import PlaylistItem from "../ui/PlaylistItem.vue";
 import SearchQueryItem from "../ui/SearchQueryItem.vue";
 import Modal from "../Modal.vue";
 
-import io from "../../io";
-
 export default {
 	components: { Modal, PlaylistItem, SearchQueryItem },
 	mixins: [SearchYoutube],
@@ -226,18 +224,20 @@ export default {
 			playlists: []
 		};
 	},
-	computed: mapState({
-		loggedIn: state => state.user.auth.loggedIn,
-		station: state => state.station.station,
-		privatePlaylistQueueSelected: state =>
-			state.station.privatePlaylistQueueSelected
-	}),
+	computed: {
+		...mapState({
+			loggedIn: state => state.user.auth.loggedIn,
+			station: state => state.station.station,
+			privatePlaylistQueueSelected: state =>
+				state.station.privatePlaylistQueueSelected
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
+		})
+	},
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-			this.socket.emit("playlists.indexMyPlaylists", true, res => {
-				if (res.status === "success") this.playlists = res.data;
-			});
+		this.socket.dispatch("playlists.indexMyPlaylists", true, res => {
+			if (res.status === "success") this.playlists = res.data;
 		});
 	},
 	methods: {
@@ -257,7 +257,7 @@ export default {
 		},
 		addSongToQueue(songId, index) {
 			if (this.station.type === "community") {
-				this.socket.emit(
+				this.socket.dispatch(
 					"stations.addToQueue",
 					this.station._id,
 					songId,
@@ -280,7 +280,7 @@ export default {
 					}
 				);
 			} else {
-				this.socket.emit("queueSongs.add", songId, data => {
+				this.socket.dispatch("queueSongs.add", songId, data => {
 					if (data.status !== "success")
 						new Toast({
 							content: `Error: ${data.message}`,
@@ -318,7 +318,7 @@ export default {
 				}
 			}, 750);
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				"queueSongs.addSetToQueue",
 				this.search.playlist.query,
 				this.search.playlist.isImportingOnlyMusic,

+ 5 - 8
frontend/src/components/modals/CreateCommunityStation.vue

@@ -39,11 +39,10 @@
 </template>
 
 <script>
-import { mapActions } from "vuex";
+import { mapGetters, mapActions } from "vuex";
 
 import Toast from "toasters";
 import Modal from "../Modal.vue";
-import io from "../../io";
 import validation from "../../validation";
 
 export default {
@@ -57,11 +56,9 @@ export default {
 			}
 		};
 	},
-	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-		});
-	},
+	computed: mapGetters({
+		socket: "websockets/getSocket"
+	}),
 	methods: {
 		submitModal() {
 			this.newCommunity.name = this.newCommunity.name.toLowerCase();
@@ -118,7 +115,7 @@ export default {
 					timeout: 8000
 				});
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				"stations.create",
 				{
 					name,

+ 21 - 20
frontend/src/components/modals/CreatePlaylist.vue

@@ -33,11 +33,10 @@
 </template>
 
 <script>
-import { mapActions } from "vuex";
+import { mapActions, mapGetters } from "vuex";
 
 import Toast from "toasters";
 import Modal from "../Modal.vue";
-import io from "../../io";
 import validation from "../../validation";
 
 export default {
@@ -51,11 +50,9 @@ export default {
 			}
 		};
 	},
-	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-		});
-	},
+	computed: mapGetters({
+		socket: "websockets/getSocket"
+	}),
 	methods: {
 		createPlaylist() {
 			const { displayName } = this.playlist;
@@ -73,21 +70,25 @@ export default {
 					timeout: 8000
 				});
 
-			return this.socket.emit("playlists.create", this.playlist, res => {
-				new Toast({ content: res.message, timeout: 3000 });
+			return this.socket.dispatch(
+				"playlists.create",
+				this.playlist,
+				res => {
+					new Toast({ content: res.message, timeout: 3000 });
 
-				if (res.status === "success") {
-					this.closeModal({
-						sector: "station",
-						modal: "createPlaylist"
-					});
-					this.editPlaylist(res.data._id);
-					this.openModal({
-						sector: "station",
-						modal: "editPlaylist"
-					});
+					if (res.status === "success") {
+						this.closeModal({
+							sector: "station",
+							modal: "createPlaylist"
+						});
+						this.editPlaylist(res.data._id);
+						this.openModal({
+							sector: "station",
+							modal: "editPlaylist"
+						});
+					}
 				}
-			});
+			);
 		},
 		...mapActions("modalVisibility", ["closeModal", "openModal"]),
 		...mapActions("user/playlists", ["editPlaylist"])

+ 32 - 29
frontend/src/components/modals/EditNews.vue

@@ -164,10 +164,9 @@
 </template>
 
 <script>
-import { mapActions, mapState } from "vuex";
+import { mapActions, mapGetters, mapState } from "vuex";
 
 import Toast from "toasters";
-import io from "../../io";
 
 import Modal from "../Modal.vue";
 
@@ -180,27 +179,26 @@ export default {
 	computed: {
 		...mapState("modals/editNews", {
 			news: state => state.news
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
 		})
 	},
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-
-			this.socket.emit(`news.getNewsFromId`, this.newsId, res => {
-				if (res.status === "success") {
-					const news = res.data;
-					this.editNews(news);
-				} else {
-					new Toast({
-						content: "News with that ID not found",
-						timeout: 3000
-					});
-					this.closeModal({
-						sector: this.sector,
-						modal: "editNews"
-					});
-				}
-			});
+		this.socket.dispatch(`news.getNewsFromId`, this.newsId, res => {
+			if (res.status === "success") {
+				const news = res.data;
+				this.editNews(news);
+			} else {
+				new Toast({
+					content: "News with that ID not found",
+					timeout: 3000
+				});
+				this.closeModal({
+					sector: this.sector,
+					modal: "editNews"
+				});
+			}
 		});
 	},
 	methods: {
@@ -227,16 +225,21 @@ export default {
 			this.removeChange({ type, index });
 		},
 		updateNews(close) {
-			this.socket.emit("news.update", this.news._id, this.news, res => {
-				new Toast({ content: res.message, timeout: 4000 });
-				if (res.status === "success") {
-					if (close)
-						this.closeModal({
-							sector: this.sector,
-							modal: "editNews"
-						});
+			this.socket.dispatch(
+				"news.update",
+				this.news._id,
+				this.news,
+				res => {
+					new Toast({ content: res.message, timeout: 4000 });
+					if (res.status === "success") {
+						if (close)
+							this.closeModal({
+								sector: this.sector,
+								modal: "editNews"
+							});
+					}
 				}
-			});
+			);
 		},
 		...mapActions("modalVisibility", ["closeModal"]),
 		...mapActions("modals/editNews", [

+ 80 - 80
frontend/src/components/modals/EditPlaylist/index.vue

@@ -358,7 +358,7 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapGetters, mapActions } from "vuex";
 import draggable from "vuedraggable";
 import Toast from "toasters";
 
@@ -368,7 +368,6 @@ import Modal from "../../Modal.vue";
 import SearchQueryItem from "../../ui/SearchQueryItem.vue";
 import PlaylistSongItem from "./components/PlaylistSongItem.vue";
 
-import io from "../../../io";
 import validation from "../../../validation";
 import utils from "../../../../js/utils";
 
@@ -379,7 +378,7 @@ export default {
 		return {
 			utils,
 			drag: false,
-			serverDomain: "",
+			apiDomain: "",
 			playlist: { songs: [] }
 		};
 	},
@@ -396,7 +395,10 @@ export default {
 				disabled: !this.isEditable(),
 				ghostClass: "draggable-list-ghost"
 			};
-		}
+		},
+		...mapGetters({
+			socket: "websockets/getSocket"
+		})
 	},
 	watch: {
 		"search.songs.results": function checkIfSongInPlaylist(songs) {
@@ -411,70 +413,64 @@ export default {
 		}
 	},
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-
-			this.socket.emit("playlists.getPlaylist", this.editing, res => {
-				if (res.status === "success") {
-					this.playlist = res.data;
-					this.playlist.songs.sort((a, b) => a.position - b.position);
-				}
+		this.socket.dispatch("playlists.getPlaylist", this.editing, res => {
+			if (res.status === "success") {
+				this.playlist = res.data;
+				this.playlist.songs.sort((a, b) => a.position - b.position);
+			}
 
-				this.playlist.oldId = res.data._id;
-			});
+			this.playlist.oldId = res.data._id;
+		});
 
-			this.socket.on("event:playlist.addSong", data => {
-				if (this.playlist._id === data.playlistId)
-					this.playlist.songs.push(data.song);
-			});
+		this.socket.on("event:playlist.addSong", data => {
+			if (this.playlist._id === data.playlistId)
+				this.playlist.songs.push(data.song);
+		});
 
-			this.socket.on("event:playlist.removeSong", data => {
-				if (this.playlist._id === data.playlistId) {
-					// remove song from array of playlists
-					this.playlist.songs.forEach((song, index) => {
-						if (song.songId === data.songId)
-							this.playlist.songs.splice(index, 1);
-					});
+		this.socket.on("event:playlist.removeSong", data => {
+			if (this.playlist._id === data.playlistId) {
+				// remove song from array of playlists
+				this.playlist.songs.forEach((song, index) => {
+					if (song.songId === data.songId)
+						this.playlist.songs.splice(index, 1);
+				});
 
-					// if this song is in search results, mark it available to add to the playlist again
-					this.search.songs.results.forEach((searchItem, index) => {
-						if (data.songId === searchItem.id) {
-							this.search.songs.results[
-								index
-							].isAddedToQueue = false;
-						}
-					});
-				}
-			});
+				// if this song is in search results, mark it available to add to the playlist again
+				this.search.songs.results.forEach((searchItem, index) => {
+					if (data.songId === searchItem.id) {
+						this.search.songs.results[index].isAddedToQueue = false;
+					}
+				});
+			}
+		});
 
-			this.socket.on("event:playlist.updateDisplayName", data => {
-				if (this.playlist._id === data.playlistId)
-					this.playlist.displayName = data.displayName;
-			});
+		this.socket.on("event:playlist.updateDisplayName", data => {
+			if (this.playlist._id === data.playlistId)
+				this.playlist.displayName = data.displayName;
+		});
 
-			this.socket.on("event:playlist.repositionSongs", data => {
-				if (this.playlist._id === data.playlistId) {
-					// for each song that has a new position
-					data.songsBeingChanged.forEach(changedSong => {
-						this.playlist.songs.forEach((song, index) => {
-							// find song locally
-							if (song.songId === changedSong.songId) {
-								// change song position attribute
-								this.playlist.songs[index].position =
-									changedSong.position;
-
-								// reposition in array if needed
-								if (index !== changedSong.position - 1)
-									this.playlist.songs.splice(
-										changedSong.position - 1,
-										0,
-										this.playlist.songs.splice(index, 1)[0]
-									);
-							}
-						});
+		this.socket.on("event:playlist.repositionSongs", data => {
+			if (this.playlist._id === data.playlistId) {
+				// for each song that has a new position
+				data.songsBeingChanged.forEach(changedSong => {
+					this.playlist.songs.forEach((song, index) => {
+						// find song locally
+						if (song.songId === changedSong.songId) {
+							// change song position attribute
+							this.playlist.songs[index].position =
+								changedSong.position;
+
+							// reposition in array if needed
+							if (index !== changedSong.position - 1)
+								this.playlist.songs.splice(
+									changedSong.position - 1,
+									0,
+									this.playlist.songs.splice(index, 1)[0]
+								);
+						}
 					});
-				}
-			});
+				});
+			}
 		});
 	},
 	methods: {
@@ -507,7 +503,7 @@ export default {
 				}
 			}, 750);
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				"playlists.addSetToPlaylist",
 				this.search.playlist.query,
 				this.playlist._id,
@@ -545,7 +541,7 @@ export default {
 					});
 			});
 
-			this.socket.emit(
+			this.socket.dispatch(
 				"playlists.repositionSongs",
 				this.playlist._id,
 				songsBeingChanged,
@@ -562,17 +558,21 @@ export default {
 			return this.utils.formatTimeLong(length);
 		},
 		shuffle() {
-			this.socket.emit("playlists.shuffle", this.playlist._id, res => {
-				new Toast({ content: res.message, timeout: 4000 });
-				if (res.status === "success") {
-					this.playlist.songs = res.data.songs.sort(
-						(a, b) => a.position - b.position
-					);
+			this.socket.dispatch(
+				"playlists.shuffle",
+				this.playlist._id,
+				res => {
+					new Toast({ content: res.message, timeout: 4000 });
+					if (res.status === "success") {
+						this.playlist.songs = res.data.songs.sort(
+							(a, b) => a.position - b.position
+						);
+					}
 				}
-			});
+			);
 		},
 		addSongToPlaylist(id, index) {
-			this.socket.emit(
+			this.socket.dispatch(
 				"playlists.addSongToPlaylist",
 				false,
 				id,
@@ -586,16 +586,16 @@ export default {
 		},
 		removeSongFromPlaylist(id) {
 			if (this.playlist.displayName === "Liked Songs") {
-				this.socket.emit("songs.unlike", id, res => {
+				this.socket.dispatch("songs.unlike", id, res => {
 					new Toast({ content: res.message, timeout: 4000 });
 				});
 			}
 			if (this.playlist.displayName === "Disliked Songs") {
-				this.socket.emit("songs.undislike", id, res => {
+				this.socket.dispatch("songs.undislike", id, res => {
 					new Toast({ content: res.message, timeout: 4000 });
 				});
 			} else {
-				this.socket.emit(
+				this.socket.dispatch(
 					"playlists.removeSongFromPlaylist",
 					id,
 					this.playlist._id,
@@ -620,7 +620,7 @@ export default {
 					timeout: 8000
 				});
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				"playlists.updateDisplayName",
 				this.playlist._id,
 				this.playlist.displayName,
@@ -630,7 +630,7 @@ export default {
 			);
 		},
 		removePlaylist() {
-			this.socket.emit("playlists.remove", this.playlist._id, res => {
+			this.socket.dispatch("playlists.remove", this.playlist._id, res => {
 				new Toast({ content: res.message, timeout: 3000 });
 				if (res.status === "success") {
 					this.closeModal({
@@ -641,11 +641,11 @@ export default {
 			});
 		},
 		async downloadPlaylist() {
-			if (this.serverDomain === "")
-				this.serverDomain = await lofig.get("serverDomain");
+			if (this.apiDomain === "")
+				this.apiDomain = await lofig.get("apiDomain");
 
 			fetch(
-				`${this.serverDomain}/export/privatePlaylist/${this.playlist._id}`,
+				`${this.apiDomain}/export/privatePlaylist/${this.playlist._id}`,
 				{ credentials: "include" }
 			)
 				.then(res => res.blob())
@@ -698,7 +698,7 @@ export default {
 		updatePrivacy() {
 			const { privacy } = this.playlist;
 			if (privacy === "public" || privacy === "private") {
-				this.socket.emit(
+				this.socket.dispatch(
 					"playlists.updatePrivacy",
 					this.playlist._id,
 					privacy,

+ 142 - 151
frontend/src/components/modals/EditSong.vue

@@ -506,10 +506,9 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapGetters, mapActions } from "vuex";
 import Toast from "toasters";
 
-import io from "../../io";
 import keyboardShortcuts from "../../keyboardShortcuts";
 import validation from "../../validation";
 import Modal from "../Modal.vue";
@@ -590,6 +589,9 @@ export default {
 		}),
 		...mapState("modalVisibility", {
 			modals: state => state.modals.admin
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
 		})
 	},
 	watch: {
@@ -612,162 +614,151 @@ export default {
 
 		this.useHTTPS = await lofig.get("cookie.secure");
 
-		io.getSocket(socket => {
-			this.socket = socket;
+		this.socket.dispatch(
+			`${this.songType}.getSongFromMusareId`,
+			this.songId,
+			res => {
+				if (res.status === "success") {
+					const { song } = res.data;
+					// this.song = { ...song };
+					// if (this.song.discogs === undefined)
+					// 	this.song.discogs = null;
+					this.editSong(song);
+
+					this.songDataLoaded = true;
+
+					// this.edit(res.data.song);
+
+					this.discogsQuery = this.song.title;
+
+					this.interval = setInterval(() => {
+						if (
+							this.song.duration !== -1 &&
+							this.video.paused === false &&
+							this.playerReady &&
+							this.video.player.getCurrentTime() -
+								this.song.skipDuration >
+								this.song.duration
+						) {
+							this.video.paused = false;
+							this.video.player.stopVideo();
+							this.drawCanvas();
+						}
+						if (this.playerReady) {
+							this.youtubeVideoCurrentTime = this.video.player
+								.getCurrentTime()
+								.toFixed(3);
+						}
+
+						if (this.video.paused === false) this.drawCanvas();
+					}, 200);
+
+					this.video.player = new window.YT.Player("editSongPlayer", {
+						height: 298,
+						width: 530,
+						videoId: this.song.songId,
+						host: "https://www.youtube-nocookie.com",
+						playerVars: {
+							controls: 0,
+							iv_load_policy: 3,
+							rel: 0,
+							showinfo: 0,
+							autoplay: 1
+						},
+						startSeconds: this.song.skipDuration,
+						events: {
+							onReady: () => {
+								let volume = parseInt(
+									localStorage.getItem("volume")
+								);
+								volume =
+									typeof volume === "number" ? volume : 20;
+								console.log(
+									`Seekto: ${this.song.skipDuration}`
+								);
+								this.video.player.seekTo(
+									this.song.skipDuration
+								);
+								this.video.player.setVolume(volume);
+								if (volume > 0) this.video.player.unMute();
+								this.youtubeVideoDuration = this.video.player
+									.getDuration()
+									.toFixed(3);
+								this.youtubeVideoNote = "(~)";
+								this.playerReady = true;
 
-			this.socket.emit(
-				`${this.songType}.getSongFromMusareId`,
-				this.songId,
-				res => {
-					if (res.status === "success") {
-						const { song } = res.data;
-						// this.song = { ...song };
-						// if (this.song.discogs === undefined)
-						// 	this.song.discogs = null;
-						this.editSong(song);
-
-						this.songDataLoaded = true;
-
-						// this.edit(res.data.song);
-
-						this.discogsQuery = this.song.title;
-
-						this.interval = setInterval(() => {
-							if (
-								this.song.duration !== -1 &&
-								this.video.paused === false &&
-								this.playerReady &&
-								this.video.player.getCurrentTime() -
-									this.song.skipDuration >
-									this.song.duration
-							) {
-								this.video.paused = false;
-								this.video.player.stopVideo();
 								this.drawCanvas();
-							}
-							if (this.playerReady) {
-								this.youtubeVideoCurrentTime = this.video.player
-									.getCurrentTime()
-									.toFixed(3);
-							}
+							},
+							onStateChange: event => {
+								this.drawCanvas();
 
-							if (this.video.paused === false) this.drawCanvas();
-						}, 200);
-
-						this.video.player = new window.YT.Player(
-							"editSongPlayer",
-							{
-								height: 298,
-								width: 530,
-								videoId: this.song.songId,
-								host: "https://www.youtube-nocookie.com",
-								playerVars: {
-									controls: 0,
-									iv_load_policy: 3,
-									rel: 0,
-									showinfo: 0,
-									autoplay: 1
-								},
-								startSeconds: this.song.skipDuration,
-								events: {
-									onReady: () => {
-										let volume = parseInt(
-											localStorage.getItem("volume")
-										);
-										volume =
-											typeof volume === "number"
-												? volume
-												: 20;
-										console.log(
-											`Seekto: ${this.song.skipDuration}`
-										);
-										this.video.player.seekTo(
+								if (event.data === 1) {
+									if (!this.video.autoPlayed) {
+										this.video.autoPlayed = true;
+										return this.video.player.stopVideo();
+									}
+
+									this.video.paused = false;
+									let youtubeDuration = this.video.player.getDuration();
+									this.youtubeVideoDuration = youtubeDuration.toFixed(
+										3
+									);
+									this.youtubeVideoNote = "";
+
+									if (this.song.duration === -1)
+										this.song.duration = youtubeDuration;
+
+									youtubeDuration -= this.song.skipDuration;
+									if (
+										this.song.duration >
+										youtubeDuration + 1
+									) {
+										this.video.player.stopVideo();
+										this.video.paused = true;
+										return new Toast({
+											content:
+												"Video can't play. Specified duration is bigger than the YouTube song duration.",
+											timeout: 4000
+										});
+									}
+									if (this.song.duration <= 0) {
+										this.video.player.stopVideo();
+										this.video.paused = true;
+										return new Toast({
+											content:
+												"Video can't play. Specified duration has to be more than 0 seconds.",
+											timeout: 4000
+										});
+									}
+
+									if (
+										this.video.player.getCurrentTime() <
+										this.song.skipDuration
+									) {
+										return this.video.player.seekTo(
 											this.song.skipDuration
 										);
-										this.video.player.setVolume(volume);
-										if (volume > 0)
-											this.video.player.unMute();
-										this.youtubeVideoDuration = this.video.player
-											.getDuration()
-											.toFixed(3);
-										this.youtubeVideoNote = "(~)";
-										this.playerReady = true;
-
-										this.drawCanvas();
-									},
-									onStateChange: event => {
-										this.drawCanvas();
-
-										if (event.data === 1) {
-											if (!this.video.autoPlayed) {
-												this.video.autoPlayed = true;
-												return this.video.player.stopVideo();
-											}
-
-											this.video.paused = false;
-											let youtubeDuration = this.video.player.getDuration();
-											this.youtubeVideoDuration = youtubeDuration.toFixed(
-												3
-											);
-											this.youtubeVideoNote = "";
-
-											if (this.song.duration === -1)
-												this.song.duration = youtubeDuration;
-
-											youtubeDuration -= this.song
-												.skipDuration;
-											if (
-												this.song.duration >
-												youtubeDuration + 1
-											) {
-												this.video.player.stopVideo();
-												this.video.paused = true;
-												return new Toast({
-													content:
-														"Video can't play. Specified duration is bigger than the YouTube song duration.",
-													timeout: 4000
-												});
-											}
-											if (this.song.duration <= 0) {
-												this.video.player.stopVideo();
-												this.video.paused = true;
-												return new Toast({
-													content:
-														"Video can't play. Specified duration has to be more than 0 seconds.",
-													timeout: 4000
-												});
-											}
-
-											if (
-												this.video.player.getCurrentTime() <
-												this.song.skipDuration
-											) {
-												return this.video.player.seekTo(
-													this.song.skipDuration
-												);
-											}
-										} else if (event.data === 2) {
-											this.video.paused = true;
-										}
-
-										return false;
 									}
+								} else if (event.data === 2) {
+									this.video.paused = true;
 								}
+
+								return false;
 							}
-						);
-					} else {
-						new Toast({
-							content: "Song with that ID not found",
-							timeout: 3000
-						});
-						this.closeModal({
-							sector: this.sector,
-							modal: "editSong"
-						});
-					}
+						}
+					});
+				} else {
+					new Toast({
+						content: "Song with that ID not found",
+						timeout: 3000
+					});
+					this.closeModal({
+						sector: this.sector,
+						modal: "editSong"
+					});
 				}
-			);
-		});
+			}
+		);
 
 		let volume = parseFloat(localStorage.getItem("volume"));
 		volume =
@@ -1092,7 +1083,7 @@ export default {
 
 			saveButtonRef.status = "disabled";
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				`${this.songType}.update`,
 				song._id,
 				song,
@@ -1177,7 +1168,7 @@ export default {
 		searchDiscogsForPage(page) {
 			const query = this.discogsQuery;
 
-			this.socket.emit("apis.searchDiscogs", query, page, res => {
+			this.socket.dispatch("apis.searchDiscogs", query, page, res => {
 				if (res.status === "success") {
 					if (page === 1)
 						new Toast({

+ 98 - 100
frontend/src/components/modals/EditStation.vue

@@ -421,12 +421,11 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapGetters, mapActions } from "vuex";
 
 import Toast from "toasters";
 
 import Modal from "../Modal.vue";
-import io from "../../io";
 import validation from "../../validation";
 import SaveButton from "../ui/SaveButton.vue";
 
@@ -507,87 +506,82 @@ export default {
 		...mapState("modals/editStation", {
 			station: state => state.station,
 			originalStation: state => state.originalStation
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
 		})
 	},
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-
-			this.socket.emit(`stations.getStationById`, this.stationId, res => {
-				if (res.status === "success") {
-					const { station } = res;
-					// this.song = { ...song };
-					// if (this.song.discogs === undefined)
-					// 	this.song.discogs = null;
-					this.editStation(station);
-
-					// this.songDataLoaded = true;
-
-					this.socket.emit(
-						`stations.getStationIncludedPlaylistsById`,
-						this.stationId,
-						res => {
-							if (res.status === "success") {
-								this.station.genres = res.playlists.map(
-									playlist => {
-										if (playlist) {
-											if (playlist.type === "genre")
-												return playlist.createdFor;
-											return `Playlist: ${playlist.name}`;
-										}
-										return "Unknown/Error";
+		this.socket.dispatch(`stations.getStationById`, this.stationId, res => {
+			if (res.status === "success") {
+				const { station } = res;
+				// this.song = { ...song };
+				// if (this.song.discogs === undefined)
+				// 	this.song.discogs = null;
+				this.editStation(station);
+
+				// this.songDataLoaded = true;
+
+				this.socket.dispatch(
+					`stations.getStationIncludedPlaylistsById`,
+					this.stationId,
+					res => {
+						if (res.status === "success") {
+							this.station.genres = res.playlists.map(
+								playlist => {
+									if (playlist) {
+										if (playlist.type === "genre")
+											return playlist.createdFor;
+										return `Playlist: ${playlist.name}`;
 									}
-								);
-								this.originalStation.genres = JSON.parse(
-									JSON.stringify(this.station.genres)
-								);
-							}
+									return "Unknown/Error";
+								}
+							);
+							this.originalStation.genres = JSON.parse(
+								JSON.stringify(this.station.genres)
+							);
 						}
-					);
-
-					this.socket.emit(
-						`stations.getStationExcludedPlaylistsById`,
-						this.stationId,
-						res => {
-							if (res.status === "success") {
-								this.station.blacklistedGenres = res.playlists.map(
-									playlist => {
-										if (playlist) {
-											if (playlist.type === "genre")
-												return playlist.createdFor;
-											return `Playlist: ${playlist.name}`;
-										}
-										return "Unknown/Error";
+					}
+				);
+
+				this.socket.dispatch(
+					`stations.getStationExcludedPlaylistsById`,
+					this.stationId,
+					res => {
+						if (res.status === "success") {
+							this.station.blacklistedGenres = res.playlists.map(
+								playlist => {
+									if (playlist) {
+										if (playlist.type === "genre")
+											return playlist.createdFor;
+										return `Playlist: ${playlist.name}`;
 									}
-								);
-								this.originalStation.blacklistedGenres = JSON.parse(
-									JSON.stringify(
-										this.station.blacklistedGenres
-									)
-								);
-							}
+									return "Unknown/Error";
+								}
+							);
+							this.originalStation.blacklistedGenres = JSON.parse(
+								JSON.stringify(this.station.blacklistedGenres)
+							);
 						}
-					);
-
-					// this.station.genres = JSON.parse(
-					// 	JSON.stringify(this.station.genres)
-					// );
-					// this.station.blacklistedGenres = JSON.parse(
-					// 	JSON.stringify(this.station.blacklistedGenres)
-					// );
-				} else {
-					new Toast({
-						content: "Station with that ID not found",
-						timeout: 3000
-					});
-					this.closeModal({
-						sector: this.sector,
-						modal: "editStation"
-					});
-				}
-			});
-
-			return socket;
+					}
+				);
+
+				// this.station.genres = JSON.parse(
+				// 	JSON.stringify(this.station.genres)
+				// );
+				// this.station.blacklistedGenres = JSON.parse(
+				// 	JSON.stringify(this.station.blacklistedGenres)
+				// );
+			} else {
+				new Toast({
+					content: "Station with that ID not found",
+					timeout: 3000
+				});
+				this.closeModal({
+					sector: this.sector,
+					modal: "editStation"
+				});
+			}
 		});
 	},
 	methods: {
@@ -660,7 +654,7 @@ export default {
 
 			this.$refs.saveButton.status = "disabled";
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				"stations.updateName",
 				this.station._id,
 				name,
@@ -695,7 +689,7 @@ export default {
 
 			this.$refs.saveButton.status = "disabled";
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				"stations.updateDisplayName",
 				this.station._id,
 				displayName,
@@ -732,7 +726,7 @@ export default {
 
 			this.$refs.saveButton.status = "disabled";
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				"stations.updateDescription",
 				this.station._id,
 				description,
@@ -756,7 +750,7 @@ export default {
 		updatePrivacy() {
 			this.$refs.saveButton.status = "disabled";
 
-			this.socket.emit(
+			this.socket.dispatch(
 				"stations.updatePrivacy",
 				this.station._id,
 				this.station.privacy,
@@ -775,7 +769,7 @@ export default {
 		updateGenres() {
 			this.$refs.saveButton.status = "disabled";
 
-			this.socket.emit(
+			this.socket.dispatch(
 				"stations.updateGenres",
 				this.station._id,
 				this.station.genres,
@@ -800,7 +794,7 @@ export default {
 		updateBlacklistedGenres() {
 			this.$refs.saveButton.status = "disabled";
 
-			this.socket.emit(
+			this.socket.dispatch(
 				"stations.updateBlacklistedGenres",
 				this.station._id,
 				this.station.blacklistedGenres,
@@ -829,7 +823,7 @@ export default {
 		updatePartyMode() {
 			this.$refs.saveButton.status = "disabled";
 
-			this.socket.emit(
+			this.socket.dispatch(
 				"stations.updatePartyMode",
 				this.station._id,
 				this.station.partyMode,
@@ -853,26 +847,30 @@ export default {
 		updateQueueLock() {
 			this.$refs.saveButton.status = "disabled";
 
-			this.socket.emit("stations.toggleLock", this.station._id, res => {
-				if (res.status === "success") {
-					if (this.originalStation)
-						this.originalStation.locked = res.data;
+			this.socket.dispatch(
+				"stations.toggleLock",
+				this.station._id,
+				res => {
+					if (res.status === "success") {
+						if (this.originalStation)
+							this.originalStation.locked = res.data;
+
+						new Toast({
+							content: `Toggled queue lock successfully to ${res.data}`,
+							timeout: 4000
+						});
+
+						return this.$refs.saveButton.handleSuccessfulSave();
+					}
 
 					new Toast({
-						content: `Toggled queue lock successfully to ${res.data}`,
-						timeout: 4000
+						content: "Failed to toggle queue lock.",
+						timeout: 8000
 					});
 
-					return this.$refs.saveButton.handleSuccessfulSave();
+					return this.$refs.saveButton.handleFailedSave();
 				}
-
-				new Toast({
-					content: "Failed to toggle queue lock.",
-					timeout: 8000
-				});
-
-				return this.$refs.saveButton.handleFailedSave();
-			});
+			);
 		},
 		updateThemeLocal(theme) {
 			if (this.station.theme === theme) return;
@@ -882,7 +880,7 @@ export default {
 		updateTheme() {
 			this.$refs.saveButton.status = "disabled";
 
-			this.socket.emit(
+			this.socket.dispatch(
 				"stations.updateTheme",
 				this.station._id,
 				this.station.theme,
@@ -899,7 +897,7 @@ export default {
 			);
 		},
 		deleteStation() {
-			this.socket.emit("stations.remove", this.station._id, res => {
+			this.socket.dispatch("stations.remove", this.station._id, res => {
 				if (res.status === "success")
 					this.closeModal({
 						sector: "station",

+ 23 - 27
frontend/src/components/modals/EditUser.vue

@@ -88,10 +88,9 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapGetters, mapActions } from "vuex";
 
 import Toast from "toasters";
-import io from "../../io";
 import Modal from "../Modal.vue";
 import validation from "../../validation";
 
@@ -111,29 +110,26 @@ export default {
 	computed: {
 		...mapState("modals/editUser", {
 			user: state => state.user
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
 		})
 	},
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-
-			this.socket.emit(`users.getUserFromId`, this.userId, res => {
-				if (res.status === "success") {
-					const user = res.data;
-					this.editUser(user);
-				} else {
-					new Toast({
-						content: "User with that ID not found",
-						timeout: 3000
-					});
-					this.closeModal({
-						sector: this.sector,
-						modal: "editUser"
-					});
-				}
-			});
-
-			return socket;
+		this.socket.dispatch(`users.getUserFromId`, this.userId, res => {
+			if (res.status === "success") {
+				const user = res.data;
+				this.editUser(user);
+			} else {
+				new Toast({
+					content: "User with that ID not found",
+					timeout: 3000
+				});
+				this.closeModal({
+					sector: this.sector,
+					modal: "editUser"
+				});
+			}
 		});
 	},
 	methods: {
@@ -151,7 +147,7 @@ export default {
 					timeout: 8000
 				});
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				`users.updateUsername`,
 				this.user._id,
 				username,
@@ -177,7 +173,7 @@ export default {
 					timeout: 8000
 				});
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				`users.updateEmail`,
 				this.user._id,
 				email,
@@ -187,7 +183,7 @@ export default {
 			);
 		},
 		updateRole() {
-			this.socket.emit(
+			this.socket.dispatch(
 				`users.updateRole`,
 				this.user._id,
 				this.user.role,
@@ -210,7 +206,7 @@ export default {
 					timeout: 8000
 				});
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				`users.banUserById`,
 				this.user._id,
 				this.ban.reason,
@@ -221,7 +217,7 @@ export default {
 			);
 		},
 		removeSessions() {
-			this.socket.emit(`users.removeSessions`, this.user._id, res => {
+			this.socket.dispatch(`users.removeSessions`, this.user._id, res => {
 				new Toast({ content: res.message, timeout: 4000 });
 			});
 		},

+ 3 - 3
frontend/src/components/modals/Login.vue

@@ -62,7 +62,7 @@
 				>
 				<a
 					class="button is-github"
-					:href="serverDomain + '/auth/github/authorize'"
+					:href="apiDomain + '/auth/github/authorize'"
 					@click="githubRedirect()"
 				>
 					<div class="icon">
@@ -88,11 +88,11 @@ export default {
 		return {
 			email: "",
 			password: "",
-			serverDomain: ""
+			apiDomain: ""
 		};
 	},
 	async mounted() {
-		this.serverDomain = await lofig.get("serverDomain");
+		this.apiDomain = await lofig.get("apiDomain");
 	},
 	methods: {
 		submitModal() {

+ 3 - 3
frontend/src/components/modals/Register.vue

@@ -94,7 +94,7 @@
 				>
 				<a
 					class="button is-github"
-					:href="serverDomain + '/auth/github/authorize'"
+					:href="apiDomain + '/auth/github/authorize'"
 					@click="githubRedirect()"
 				>
 					<div class="icon">
@@ -142,7 +142,7 @@ export default {
 				token: "",
 				enabled: false
 			},
-			serverDomain: ""
+			apiDomain: ""
 		};
 	},
 	watch: {
@@ -195,7 +195,7 @@ export default {
 		}
 	},
 	async mounted() {
-		this.serverDomain = await lofig.get("serverDomain");
+		this.apiDomain = await lofig.get("apiDomain");
 
 		lofig.get("recaptcha").then(obj => {
 			this.recaptcha.enabled = obj.enabled;

+ 5 - 7
frontend/src/components/modals/Report.vue

@@ -182,11 +182,10 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapGetters, mapActions } from "vuex";
 
 import Toast from "toasters";
 import Modal from "../Modal.vue";
-import io from "../../io";
 
 export default {
 	components: { Modal },
@@ -249,13 +248,12 @@ export default {
 			currentSong: state => state.station.currentSong,
 			previousSong: state => state.station.previousSong,
 			song: state => state.modals.report.song
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
 		})
 	},
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-		});
-
 		this.report.songId = this.currentSong.songId;
 
 		if (this.song !== null) {
@@ -267,7 +265,7 @@ export default {
 	methods: {
 		create() {
 			console.log(this.report);
-			this.socket.emit("reports.create", this.report, res => {
+			this.socket.dispatch("reports.create", this.report, res => {
 				new Toast({ content: res.message, timeout: 4000 });
 				if (res.status === "success")
 					this.closeModal({

+ 22 - 26
frontend/src/components/modals/ViewPunishment.vue

@@ -73,11 +73,10 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapGetters, mapActions } from "vuex";
 import { format, formatDistance, parseISO } from "date-fns"; // eslint-disable-line no-unused-vars
 
 import Toast from "toasters";
-import io from "../../io";
 import Modal from "../Modal.vue";
 import UserIdToUsername from "../common/UserIdToUsername.vue";
 
@@ -95,34 +94,31 @@ export default {
 	computed: {
 		...mapState("modals/viewPunishment", {
 			punishment: state => state.punishment
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
 		})
 	},
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-
-			this.socket.emit(
-				`punishments.getPunishmentById`,
-				this.punishmentId,
-				res => {
-					if (res.status === "success") {
-						const punishment = res.data;
-						this.viewPunishment(punishment);
-					} else {
-						new Toast({
-							content: "Punishment with that ID not found",
-							timeout: 3000
-						});
-						this.closeModal({
-							sector: this.sector,
-							modal: "viewPunishment"
-						});
-					}
+		this.socket.dispatch(
+			`punishments.getPunishmentById`,
+			this.punishmentId,
+			res => {
+				if (res.status === "success") {
+					const punishment = res.data;
+					this.viewPunishment(punishment);
+				} else {
+					new Toast({
+						content: "Punishment with that ID not found",
+						timeout: 3000
+					});
+					this.closeModal({
+						sector: this.sector,
+						modal: "viewPunishment"
+					});
 				}
-			);
-
-			return socket;
-		});
+			}
+		);
 	},
 	methods: {
 		...mapActions("modalVisibility", ["closeModal"]),

+ 18 - 23
frontend/src/components/modals/ViewReport.vue

@@ -89,12 +89,10 @@
 </template>
 
 <script>
-import { mapActions, mapState } from "vuex";
+import { mapActions, mapGetters, mapState } from "vuex";
 import { formatDistance } from "date-fns";
 import Toast from "toasters";
 
-import io from "../../io";
-
 import UserIdToUsername from "../common/UserIdToUsername.vue";
 import Modal from "../Modal.vue";
 
@@ -107,6 +105,9 @@ export default {
 	computed: {
 		...mapState("modals/viewReport", {
 			report: state => state.report
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
 		})
 	},
 	mounted() {
@@ -114,26 +115,20 @@ export default {
 			this.closeModal({ sector: this.sector, modal: "editSong" });
 		}
 
-		io.getSocket(socket => {
-			this.socket = socket;
-
-			this.socket.emit(`reports.findOne`, this.reportId, res => {
-				if (res.status === "success") {
-					const report = res.data;
-					this.viewReport(report);
-				} else {
-					new Toast({
-						content: "Report with that ID not found",
-						timeout: 3000
-					});
-					this.closeModal({
-						sector: this.sector,
-						modal: "viewReport"
-					});
-				}
-			});
-
-			return socket;
+		this.socket.dispatch(`reports.findOne`, this.reportId, res => {
+			if (res.status === "success") {
+				const report = res.data;
+				this.viewReport(report);
+			} else {
+				new Toast({
+					content: "Report with that ID not found",
+					timeout: 3000
+				});
+				this.closeModal({
+					sector: this.sector,
+					modal: "viewReport"
+				});
+			}
 		});
 	},
 	methods: {

+ 24 - 28
frontend/src/components/modals/WhatIsNew.vue

@@ -69,8 +69,7 @@
 
 <script>
 import { format } from "date-fns";
-
-import io from "../../io";
+import { mapGetters } from "vuex";
 
 export default {
 	data() {
@@ -79,35 +78,32 @@ export default {
 			news: null
 		};
 	},
+	computed: mapGetters({
+		socket: "websockets/getSocket"
+	}),
 	mounted() {
-		io.getSocket(true, socket => {
-			this.socket = socket;
-			this.socket.emit("news.newest", res => {
-				this.news = res.data;
-				if (this.news && localStorage.getItem("firstVisited")) {
-					if (localStorage.getItem("whatIsNew")) {
-						if (
-							parseInt(localStorage.getItem("whatIsNew")) <
-							res.data.createdAt
-						) {
-							this.toggleModal();
-							localStorage.setItem(
-								"whatIsNew",
-								res.data.createdAt
-							);
-						}
-					} else {
-						if (
-							parseInt(localStorage.getItem("firstVisited")) <
-							res.data.createdAt
-						) {
-							this.toggleModal();
-						}
+		this.socket.dispatch("news.newest", res => {
+			this.news = res.data;
+			if (this.news && localStorage.getItem("firstVisited")) {
+				if (localStorage.getItem("whatIsNew")) {
+					if (
+						parseInt(localStorage.getItem("whatIsNew")) <
+						res.data.createdAt
+					) {
+						this.toggleModal();
 						localStorage.setItem("whatIsNew", res.data.createdAt);
 					}
-				} else if (!localStorage.getItem("firstVisited"))
-					localStorage.setItem("firstVisited", Date.now());
-			});
+				} else {
+					if (
+						parseInt(localStorage.getItem("firstVisited")) <
+						res.data.createdAt
+					) {
+						this.toggleModal();
+					}
+					localStorage.setItem("whatIsNew", res.data.createdAt);
+				}
+			} else if (!localStorage.getItem("firstVisited"))
+				localStorage.setItem("firstVisited", Date.now());
 		});
 	},
 	methods: {

+ 29 - 30
frontend/src/components/ui/AddToPlaylistDropdown.vue

@@ -41,8 +41,8 @@
 </template>
 
 <script>
+import { mapGetters } from "vuex";
 import Toast from "toasters";
-import io from "../../io";
 
 export default {
 	props: {
@@ -56,46 +56,45 @@ export default {
 			playlists: []
 		};
 	},
+	computed: mapGetters({
+		socket: "websockets/getSocket"
+	}),
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-
-			this.socket.emit("playlists.indexMyPlaylists", false, res => {
-				if (res.status === "success") {
-					this.playlists = res.data;
-					this.checkIfPlaylistsHaveSong();
-				}
-			});
-
-			this.socket.on("event:songs.next", () => {
+		this.socket.dispatch("playlists.indexMyPlaylists", false, res => {
+			if (res.status === "success") {
+				this.playlists = res.data;
 				this.checkIfPlaylistsHaveSong();
-			});
+			}
+		});
 
-			this.socket.on("event:playlist.create", playlist => {
-				this.playlists.push(playlist);
-			});
+		this.socket.on("event:songs.next", () => {
+			this.checkIfPlaylistsHaveSong();
+		});
 
-			this.socket.on("event:playlist.delete", playlistId => {
-				this.playlists.forEach((playlist, index) => {
-					if (playlist._id === playlistId) {
-						this.playlists.splice(index, 1);
-					}
-				});
+		this.socket.on("event:playlist.create", playlist => {
+			this.playlists.push(playlist);
+		});
+
+		this.socket.on("event:playlist.delete", playlistId => {
+			this.playlists.forEach((playlist, index) => {
+				if (playlist._id === playlistId) {
+					this.playlists.splice(index, 1);
+				}
 			});
+		});
 
-			this.socket.on("event:playlist.updateDisplayName", data => {
-				this.playlists.forEach((playlist, index) => {
-					if (playlist._id === data.playlistId) {
-						this.playlists[index].displayName = data.displayName;
-					}
-				});
+		this.socket.on("event:playlist.updateDisplayName", data => {
+			this.playlists.forEach((playlist, index) => {
+				if (playlist._id === data.playlistId) {
+					this.playlists[index].displayName = data.displayName;
+				}
 			});
 		});
 	},
 	methods: {
 		toggleSongInPlaylist(index, playlistId) {
 			if (!this.playlists[index].hasSong) {
-				this.socket.emit(
+				this.socket.dispatch(
 					"playlists.addSongToPlaylist",
 					false,
 					this.song.songId,
@@ -110,7 +109,7 @@ export default {
 					}
 				);
 			} else {
-				this.socket.emit(
+				this.socket.dispatch(
 					"playlists.removeSongFromPlaylist",
 					this.song.songId,
 					playlistId,

+ 0 - 103
frontend/src/io.js

@@ -1,103 +0,0 @@
-import Toast from "toasters";
-
-const callbacks = {
-	general: {
-		temp: [],
-		persist: []
-	},
-	onConnect: {
-		temp: [],
-		persist: []
-	},
-	onDisconnect: {
-		temp: [],
-		persist: []
-	},
-	onConnectError: {
-		temp: [],
-		persist: []
-	}
-};
-
-export default {
-	ready: false,
-
-	socket: null,
-
-	getSocket(...args) {
-		if (args[0] === true) {
-			if (this.ready) args[1](this.socket);
-			else callbacks.general.persist.push(args[1]);
-		} else if (this.ready) args[0](this.socket);
-		else callbacks.general.temp.push(args[0]);
-	},
-
-	onConnect(...args) {
-		if (args[0] === true) callbacks.onConnect.persist.push(args[1]);
-		else callbacks.onConnect.temp.push(args[0]);
-	},
-
-	onDisconnect(...args) {
-		if (args[0] === true) callbacks.onDisconnect.persist.push(args[1]);
-		else callbacks.onDisconnect.temp.push(args[0]);
-	},
-
-	onConnectError(...args) {
-		if (args[0] === true) callbacks.onDisconnect.persist.push(args[1]);
-		else callbacks.onConnectError.temp.push(args[0]);
-	},
-
-	clear: () => {
-		Object.keys(callbacks).forEach(type => {
-			callbacks[type].temp = [];
-		});
-	},
-
-	removeAllListeners() {
-		Object.keys(this.socket._callbacks).forEach(id => {
-			if (
-				id.indexOf("$event:") !== -1 &&
-				id.indexOf("$event:keep.") === -1
-			)
-				delete this.socket._callbacks[id];
-		});
-	},
-
-	init(url) {
-		/* eslint-disable-next-line no-undef */
-		this.socket = window.socket = io(url);
-
-		this.socket.on("connect", () => {
-			callbacks.onConnect.temp.forEach(cb => cb());
-			callbacks.onConnect.persist.forEach(cb => cb());
-		});
-
-		this.socket.on("disconnect", () => {
-			console.log("IO: SOCKET DISCONNECTED");
-			callbacks.onDisconnect.temp.forEach(cb => cb());
-			callbacks.onDisconnect.persist.forEach(cb => cb());
-		});
-
-		this.socket.on("connect_error", () => {
-			console.log("IO: SOCKET CONNECT ERROR");
-			callbacks.onConnectError.temp.forEach(cb => cb());
-			callbacks.onConnectError.persist.forEach(cb => cb());
-		});
-
-		this.socket.on("error", err => {
-			console.log("IO: SOCKET ERROR", err);
-			new Toast({
-				content: err,
-				timeout: 8000
-			});
-		});
-
-		this.ready = true;
-
-		callbacks.general.temp.forEach(callback => callback(this.socket));
-		callbacks.general.persist.forEach(callback => callback(this.socket));
-
-		callbacks.general.temp = [];
-		callbacks.general.persist = [];
-	}
-};

+ 104 - 91
frontend/src/main.js

@@ -4,9 +4,9 @@ import VueRouter from "vue-router";
 import store from "./store";
 
 import App from "./App.vue";
-import io from "./io";
+import ws from "./ws";
 
-const REQUIRED_CONFIG_VERSION = 1;
+const REQUIRED_CONFIG_VERSION = 2;
 
 const handleMetadata = attrs => {
 	document.title = `Musare | ${attrs.title}`;
@@ -138,109 +138,122 @@ const router = new VueRouter({
 });
 
 lofig.folder = "../config/default.json";
-lofig.fetchConfig().then(config => {
-	const { configVersion, skipConfigVersionCheck } = config;
-	if (configVersion !== REQUIRED_CONFIG_VERSION && !skipConfigVersionCheck) {
-		// eslint-disable-next-line no-alert
-		alert(
-			"CONFIG VERSION IS WRONG. PLEASE UPDATE YOUR CONFIG WITH THE HELP OF THE TEMPLATE FILE AND THE README FILE."
-		);
-		window.stop();
-	}
 
-	const { serverDomain } = config;
-	io.init(serverDomain);
-	io.getSocket(socket => {
-		socket.on("ready", (loggedIn, role, username, userId) => {
-			store.dispatch("user/auth/authData", {
-				loggedIn,
-				role,
-				username,
-				userId
-			});
-		});
-
-		socket.on("keep.event:banned", ban =>
-			store.dispatch("user/auth/banUser", ban)
-		);
+(async () => {
+	const websocketsDomain = await lofig.get("websocketsDomain");
+	ws.init(websocketsDomain);
+
+	ws.socket.on("ready", (loggedIn, role, username, userId) =>
+		store.dispatch("user/auth/authData", {
+			loggedIn,
+			role,
+			username,
+			userId
+		})
+	);
 
-		socket.on("event:user.username.changed", username =>
-			store.dispatch("user/auth/updateUsername", username)
+	ws.socket.on("keep.event:banned", ban =>
+		store.dispatch("user/auth/banUser", ban)
+	);
+
+	ws.socket.on("event:user.username.changed", username =>
+		store.dispatch("user/auth/updateUsername", username)
+	);
+
+	ws.socket.on("keep.event:user.preferences.changed", preferences => {
+		store.dispatch(
+			"user/preferences/changeAutoSkipDisliked",
+			preferences.autoSkipDisliked
 		);
 
-		socket.on("keep.event:user.preferences.changed", preferences => {
-			store.dispatch(
-				"user/preferences/changeAutoSkipDisliked",
-				preferences.autoSkipDisliked
-			);
+		store.dispatch(
+			"user/preferences/changeNightmode",
+			preferences.nightmode
+		);
 
-			store.dispatch(
-				"user/preferences/changeNightmode",
-				preferences.nightmode
-			);
+		store.dispatch(
+			"user/preferences/changeActivityLogPublic",
+			preferences.activityLogPublic
+		);
+	});
 
-			store.dispatch(
-				"user/preferences/changeActivityLogPublic",
-				preferences.activityLogPublic
+	lofig.fetchConfig().then(config => {
+		const { configVersion, skipConfigVersionCheck } = config;
+		if (
+			configVersion !== REQUIRED_CONFIG_VERSION &&
+			!skipConfigVersionCheck
+		) {
+			// eslint-disable-next-line no-alert
+			alert(
+				"CONFIG VERSION IS WRONG. PLEASE UPDATE YOUR CONFIG WITH THE HELP OF THE TEMPLATE FILE AND THE README FILE."
 			);
-		});
+			window.stop();
+		}
 	});
-});
 
-router.beforeEach((to, from, next) => {
-	if (window.stationInterval) {
-		clearInterval(window.stationInterval);
-		window.stationInterval = 0;
-	}
+	router.beforeEach((to, from, next) => {
+		if (window.stationInterval) {
+			clearInterval(window.stationInterval);
+			window.stationInterval = 0;
+		}
 
-	if (window.socket) io.removeAllListeners();
+		if (window.socket) ws.removeAllListeners();
 
-	io.clear();
+		ws.clear();
 
-	if (to.meta.loginRequired || to.meta.adminRequired) {
-		const gotData = () => {
-			if (to.meta.loginRequired && !store.state.user.auth.loggedIn)
-				next({ path: "/login" });
-			else if (
-				to.meta.adminRequired &&
-				store.state.user.auth.role !== "admin"
-			)
-				next({ path: "/" });
-			else next();
-		};
+		if (to.meta.loginRequired || to.meta.adminRequired) {
+			const gotData = () => {
+				if (to.meta.loginRequired && !store.state.user.auth.loggedIn)
+					next({ path: "/login" });
+				else if (
+					to.meta.adminRequired &&
+					store.state.user.auth.role !== "admin"
+				)
+					next({ path: "/" });
+				else next();
+			};
 
-		if (store.state.user.auth.gotData) gotData();
-		else {
-			const watcher = store.watch(
-				state => state.user.auth.gotData,
-				() => {
-					watcher();
-					gotData();
+			if (store.state.user.auth.gotData) gotData();
+			else {
+				const watcher = store.watch(
+					state => state.user.auth.gotData,
+					() => {
+						watcher();
+						gotData();
+					}
+				);
+			}
+		} else next();
+	});
+
+	Vue.directive("click-outside", {
+		bind(element, binding) {
+			window.handleOutsideClick = event => {
+				if (
+					!(
+						element === event.target ||
+						element.contains(event.target)
+					)
+				) {
+					binding.value();
 				}
+			};
+
+			document.body.addEventListener("click", window.handleOutsideClick);
+		},
+		unbind() {
+			document.body.removeEventListener(
+				"click",
+				window.handleOutsideClick
 			);
 		}
-	} else next();
-});
-
-Vue.directive("click-outside", {
-	bind(element, binding) {
-		window.handleOutsideClick = event => {
-			if (!(element === event.target || element.contains(event.target))) {
-				binding.value();
-			}
-		};
-
-		document.body.addEventListener("click", window.handleOutsideClick);
-	},
-	unbind() {
-		document.body.removeEventListener("click", window.handleOutsideClick);
-	}
-});
+	});
 
-// eslint-disable-next-line no-new
-new Vue({
-	router,
-	store,
-	el: "#root",
-	render: wrapper => wrapper(App)
-});
+	// eslint-disable-next-line no-new
+	new Vue({
+		router,
+		store,
+		el: "#root",
+		render: wrapper => wrapper(App)
+	});
+})();

+ 2 - 2
frontend/src/mixins/SearchYoutube.vue

@@ -33,7 +33,7 @@ export default {
 				query = query.join("");
 			}
 
-			this.socket.emit("apis.searchYoutube", query, res => {
+			this.socket.dispatch("apis.searchYoutube", query, res => {
 				if (res.status === "success") {
 					this.search.songs.nextPageToken = res.data.nextPageToken;
 					this.search.songs.results = [];
@@ -52,7 +52,7 @@ export default {
 			});
 		},
 		loadMoreSongs() {
-			this.socket.emit(
+			this.socket.dispatch(
 				"apis.searchYoutubeForPage",
 				this.search.songs.query,
 				this.search.songs.nextPageToken,

+ 2 - 2
frontend/src/mixins/SortablePlaylists.vue

@@ -19,7 +19,7 @@ export default {
 			return {
 				animation: 200,
 				group: "description",
-				disabled: false,
+				disabled: this.myUserId !== this.userId,
 				ghostClass: "draggable-list-ghost"
 			};
 		}
@@ -41,7 +41,7 @@ export default {
 			)
 				return; // nothing has changed
 
-			this.socket.emit(
+			this.socket.dispatch(
 				"users.updateOrderOfPlaylists",
 				recalculatedOrder,
 				res => {

+ 10 - 8
frontend/src/pages/Admin/tabs/NewStatistics.vue

@@ -191,7 +191,9 @@
 </template>
 
 <script>
-import io from "../../../io";
+import { mapGetters } from "vuex";
+
+import ws from "../../../ws";
 
 export default {
 	components: {},
@@ -201,16 +203,16 @@ export default {
 			module: null
 		};
 	},
+	computed: mapGetters({
+		socket: "websockets/getSocket"
+	}),
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-			if (this.socket.connected) this.init();
-			io.onConnect(() => this.init());
-		});
+		if (this.socket.readyState === 1) this.init();
+		ws.onConnect(() => this.init());
 	},
 	methods: {
 		init() {
-			this.socket.emit("utils.getModules", data => {
+			this.socket.dispatch("utils.getModules", data => {
 				console.log(data);
 				if (data.status === "success") {
 					this.modules = data.modules;
@@ -218,7 +220,7 @@ export default {
 			});
 
 			if (this.$route.query.moduleName) {
-				this.socket.emit(
+				this.socket.dispatch(
 					"utils.getModule",
 					this.$route.query.moduleName,
 					data => {

+ 39 - 35
frontend/src/pages/Admin/tabs/News.vue

@@ -215,10 +215,10 @@
 </template>
 
 <script>
-import { mapActions, mapState } from "vuex";
+import { mapActions, mapState, mapGetters } from "vuex";
 
 import Toast from "toasters";
-import io from "../../../io";
+import ws from "../../../ws";
 
 import EditNews from "../../../components/modals/EditNews.vue";
 
@@ -243,28 +243,28 @@ export default {
 		}),
 		...mapState("admin/news", {
 			news: state => state.news
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
 		})
 	},
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-			this.socket.emit("news.index", res => {
-				res.data.forEach(news => {
-					this.addNews(news);
-				});
-			});
-			this.socket.on("event:admin.news.created", news => {
-				this.addNews(news);
-			});
-			this.socket.on("event:admin.news.updated", updatedNews => {
-				this.updateNews(updatedNews);
-			});
-			this.socket.on("event:admin.news.removed", news => {
-				this.removeNews(news._id);
-			});
-			if (this.socket.connected) this.init();
-			io.onConnect(() => this.init());
-		});
+		this.socket.dispatch("news.index", res =>
+			res.data.forEach(news => this.addNews(news))
+		);
+
+		this.socket.on("event:admin.news.created", news => this.addNews(news));
+
+		this.socket.on("event:admin.news.updated", updatedNews =>
+			this.updateNews(updatedNews)
+		);
+
+		this.socket.on("event:admin.news.removed", news =>
+			this.removeNews(news._id)
+		);
+
+		if (this.socket.readyState === 1) this.init();
+		ws.onConnect(() => this.init());
 	},
 	methods: {
 		createNews() {
@@ -293,21 +293,25 @@ export default {
 					timeout: 3000
 				});
 
-			return this.socket.emit("news.create", this.creating, result => {
-				new Toast(result.message, 4000);
-				if (result.status === "success")
-					this.creating = {
-						title: "",
-						description: "",
-						bugs: [],
-						features: [],
-						improvements: [],
-						upcoming: []
-					};
-			});
+			return this.socket.dispatch(
+				"news.create",
+				this.creating,
+				result => {
+					new Toast(result.message, 4000);
+					if (result.status === "success")
+						this.creating = {
+							title: "",
+							description: "",
+							bugs: [],
+							features: [],
+							improvements: [],
+							upcoming: []
+						};
+				}
+			);
 		},
 		remove(news) {
-			this.socket.emit(
+			this.socket.dispatch(
 				"news.remove",
 				news,
 				res => new Toast({ content: res.message, timeout: 8000 })
@@ -340,7 +344,7 @@ export default {
 			this.creating[type].splice(index, 1);
 		},
 		init() {
-			this.socket.emit("apis.joinAdminRoom", "news", () => {});
+			this.socket.dispatch("apis.joinAdminRoom", "news", () => {});
 		},
 		...mapActions("modalVisibility", ["openModal", "closeModal"]),
 		...mapActions("admin/news", [

+ 9 - 9
frontend/src/pages/Admin/tabs/Playlists.vue

@@ -65,14 +65,14 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapActions, mapGetters } from "vuex";
 
 import Toast from "toasters";
 
 // import EditPlaylist from "../../../components/modals/EditPlaylist/index.vue";
 import UserIdToUsername from "../../../components/common/UserIdToUsername.vue";
 
-import io from "../../../io";
+import ws from "../../../ws";
 import utils from "../../../../js/utils";
 
 export default {
@@ -87,14 +87,14 @@ export default {
 	computed: {
 		...mapState("modalVisibility", {
 			modals: state => state.modals.admin
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
 		})
 	},
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-			if (this.socket.connected) this.init();
-			io.onConnect(() => this.init());
-		});
+		if (this.socket.readyState === 1) this.init();
+		ws.onConnect(() => this.init());
 	},
 	methods: {
 		// edit(playlist) {
@@ -102,7 +102,7 @@ export default {
 		// 	this.openModal({ sector: "admin", modal: "editPlaylist" });
 		// },
 		init() {
-			this.socket.emit("playlists.index", res => {
+			this.socket.dispatch("playlists.index", res => {
 				console.log(res);
 				if (res.status === "success") {
 					this.playlists = res.data;
@@ -114,7 +114,7 @@ export default {
 					// }
 				}
 			});
-			this.socket.emit("apis.joinAdminRoom", "playlists", () => {});
+			this.socket.dispatch("apis.joinAdminRoom", "playlists", () => {});
 		},
 		getDateFormatted(createdAt) {
 			const date = new Date(createdAt);

+ 14 - 13
frontend/src/pages/Admin/tabs/Punishments.vue

@@ -94,11 +94,11 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapGetters, mapActions } from "vuex";
 import Toast from "toasters";
 
 import ViewPunishment from "../../../components/modals/ViewPunishment.vue";
-import io from "../../../io";
+import ws from "../../../ws";
 
 export default {
 	components: { ViewPunishment },
@@ -118,17 +118,18 @@ export default {
 		},
 		...mapState("modalVisibility", {
 			modals: state => state.modals.admin
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
 		})
 	},
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-			if (this.socket.connected) this.init();
-			io.onConnect(() => this.init());
-			socket.on("event:admin.punishment.added", punishment => {
-				this.punishments.push(punishment);
-			});
-		});
+		if (this.socket.readyState === 1) this.init();
+		ws.onConnect(() => this.init());
+
+		this.socket.on("event:admin.punishment.added", punishment =>
+			this.punishments.push(punishment)
+		);
 	},
 	methods: {
 		view(punishment) {
@@ -137,7 +138,7 @@ export default {
 			this.openModal({ sector: "admin", modal: "viewPunishment" });
 		},
 		banIP() {
-			this.socket.emit(
+			this.socket.dispatch(
 				"punishments.banIP",
 				this.ipBan.ip,
 				this.ipBan.reason,
@@ -148,10 +149,10 @@ export default {
 			);
 		},
 		init() {
-			this.socket.emit("punishments.index", res => {
+			this.socket.dispatch("punishments.index", res => {
 				if (res.status === "success") this.punishments = res.data;
 			});
-			this.socket.emit("apis.joinAdminRoom", "punishments", () => {});
+			this.socket.dispatch("apis.joinAdminRoom", "punishments", () => {});
 		},
 		...mapActions("modalVisibility", ["openModal"]),
 		...mapActions("admin/punishments", ["viewPunishment"])

+ 26 - 30
frontend/src/pages/Admin/tabs/QueueSongs.vue

@@ -177,8 +177,7 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
-import Vue from "vue";
+import { mapState, mapActions, mapGetters } from "vuex";
 
 import Toast from "toasters";
 
@@ -189,7 +188,7 @@ import FloatingBox from "../../../components/ui/FloatingBox.vue";
 
 import ScrollAndFetchHandler from "../../../mixins/ScrollAndFetchHandler.vue";
 
-import io from "../../../io";
+import ws from "../../../ws";
 
 export default {
 	components: { EditSong, UserIdToUsername, FloatingBox },
@@ -212,6 +211,9 @@ export default {
 		},
 		...mapState("modalVisibility", {
 			modals: state => state.modals.admin
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
 		})
 	},
 	watch: {
@@ -221,33 +223,27 @@ export default {
 		}
 	},
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-
-			this.socket.on("event:admin.queueSong.added", queueSong => {
-				this.songs.push(queueSong);
-			});
+		this.socket.on("event:admin.queueSong.added", queueSong => {
+			this.songs.push(queueSong);
+		});
 
-			this.socket.on("event:admin.queueSong.removed", songId => {
-				this.songs = this.songs.filter(song => {
-					return song._id !== songId;
-				});
+		this.socket.on("event:admin.queueSong.removed", songId => {
+			this.songs = this.songs.filter(song => {
+				return song._id !== songId;
 			});
+		});
 
-			this.socket.on("event:admin.queueSong.updated", updatedSong => {
-				for (let i = 0; i < this.songs.length; i += 1) {
-					const song = this.songs[i];
-					if (song._id === updatedSong._id) {
-						Vue.set(this.songs, i, updatedSong);
-					}
+		this.socket.on("event:admin.queueSong.updated", updatedSong => {
+			for (let i = 0; i < this.songs.length; i += 1) {
+				const song = this.songs[i];
+				if (song._id === updatedSong._id) {
+					this.$set(this.songs, i, updatedSong);
 				}
-			});
-
-			if (this.socket.connected) this.init();
-			io.onConnect(() => {
-				this.init();
-			});
+			}
 		});
+
+		if (this.socket.readyState === 1) this.init();
+		ws.onConnect(() => this.init());
 	},
 	methods: {
 		edit(song) {
@@ -261,7 +257,7 @@ export default {
 			this.openModal({ sector: "admin", modal: "editSong" });
 		},
 		add(song) {
-			this.socket.emit("songs.add", song, res => {
+			this.socket.dispatch("songs.add", song, res => {
 				if (res.status === "success")
 					new Toast({ content: res.message, timeout: 2000 });
 				else new Toast({ content: res.message, timeout: 4000 });
@@ -273,7 +269,7 @@ export default {
 				"Are you sure you want to delete this song?"
 			);
 			if (dialogResult !== true) return;
-			this.socket.emit("queueSongs.remove", id, res => {
+			this.socket.dispatch("queueSongs.remove", id, res => {
 				if (res.status === "success")
 					new Toast({ content: res.message, timeout: 2000 });
 				else new Toast({ content: res.message, timeout: 4000 });
@@ -284,7 +280,7 @@ export default {
 			if (this.position >= this.maxPosition) return;
 			this.isGettingSet = true;
 
-			this.socket.emit("queueSongs.getSet", this.position, data => {
+			this.socket.dispatch("queueSongs.getSet", this.position, data => {
 				data.forEach(song => this.songs.push(song));
 
 				this.position += 1;
@@ -309,13 +305,13 @@ export default {
 			if (this.songs.length > 0)
 				this.position = Math.ceil(this.songs.length / 15) + 1;
 
-			this.socket.emit("queueSongs.length", length => {
+			this.socket.dispatch("queueSongs.length", length => {
 				this.maxPosition = Math.ceil(length / 15) + 1;
 
 				this.getSet();
 			});
 
-			this.socket.emit("apis.joinAdminRoom", "queue", () => {});
+			this.socket.dispatch("apis.joinAdminRoom", "queue", () => {});
 		},
 		// ...mapActions("admin/songs", ["editSong"]),
 		...mapActions("modals/editSong", ["stopVideo"]),

+ 30 - 29
frontend/src/pages/Admin/tabs/Reports.vue

@@ -67,11 +67,11 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapActions, mapGetters } from "vuex";
 import { formatDistance } from "date-fns";
 
 import Toast from "toasters";
-import io from "../../../io";
+import ws from "../../../ws";
 
 import ViewReport from "../../../components/modals/ViewReport.vue";
 import UserIdToUsername from "../../../components/common/UserIdToUsername.vue";
@@ -87,47 +87,48 @@ export default {
 	computed: {
 		...mapState("modalVisibility", {
 			modals: state => state.modals.admin
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
 		})
 	},
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-			if (this.socket.connected) this.init();
+		if (this.socket.readyState === 1) this.init();
+		ws.onConnect(() => this.init());
 
-			this.socket.emit("reports.index", res => {
-				this.reports = res.data;
-			});
-
-			this.socket.on("event:admin.report.resolved", reportId => {
-				this.reports = this.reports.filter(report => {
-					return report._id !== reportId;
-				});
-			});
-
-			this.socket.on("event:admin.report.created", report => {
-				this.reports.push(report);
-			});
+		this.socket.dispatch("reports.index", res => {
+			this.reports = res.data;
+		});
 
-			io.onConnect(() => {
-				this.init();
+		this.socket.on("event:admin.report.resolved", reportId => {
+			this.reports = this.reports.filter(report => {
+				return report._id !== reportId;
 			});
 		});
 
+		this.socket.on("event:admin.report.created", report =>
+			this.reports.push(report)
+		);
+
 		if (this.$route.query.id) {
-			this.socket.emit("reports.findOne", this.$route.query.id, res => {
-				if (res.status === "success") this.view(res.data);
-				else
-					new Toast({
-						content: "Report with that ID not found",
-						timeout: 3000
-					});
-			});
+			this.socket.dispatch(
+				"reports.findOne",
+				this.$route.query.id,
+				res => {
+					if (res.status === "success") this.view(res.data);
+					else
+						new Toast({
+							content: "Report with that ID not found",
+							timeout: 3000
+						});
+				}
+			);
 		}
 	},
 	methods: {
 		formatDistance,
 		init() {
-			this.socket.emit("apis.joinAdminRoom", "reports", () => {});
+			this.socket.dispatch("apis.joinAdminRoom", "reports", () => {});
 		},
 		view(report) {
 			// this.viewReport(report);

+ 19 - 24
frontend/src/pages/Admin/tabs/Songs.vue

@@ -200,7 +200,7 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapActions, mapGetters } from "vuex";
 
 import Toast from "toasters";
 
@@ -211,7 +211,7 @@ import FloatingBox from "../../../components/ui/FloatingBox.vue";
 
 import ScrollAndFetchHandler from "../../../mixins/ScrollAndFetchHandler.vue";
 
-import io from "../../../io";
+import ws from "../../../ws";
 
 export default {
 	components: { EditSong, UserIdToUsername, FloatingBox },
@@ -298,6 +298,9 @@ export default {
 		}),
 		...mapState("admin/songs", {
 			songs: state => state.songs
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
 		})
 	},
 	watch: {
@@ -307,29 +310,21 @@ export default {
 		}
 	},
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-
-			this.socket.on("event:admin.song.added", song => {
-				this.addSong(song);
-			});
+		this.socket.on("event:admin.song.added", song => this.addSong(song));
 
-			this.socket.on("event:admin.song.removed", songId => {
-				this.removeSong(songId);
-			});
+		this.socket.on("event:admin.song.removed", songId =>
+			this.removeSong(songId)
+		);
 
-			this.socket.on("event:admin.song.updated", updatedSong => {
-				this.updateSong(updatedSong);
-			});
+		this.socket.on("event:admin.song.updated", updatedSong =>
+			this.updateSong(updatedSong)
+		);
 
-			if (this.socket.connected) this.init();
-			io.onConnect(() => {
-				this.init();
-			});
-		});
+		if (this.socket.readyState === 1) this.init();
+		ws.onConnect(() => this.init());
 
 		if (this.$route.query.songId) {
-			this.socket.emit(
+			this.socket.dispatch(
 				"songs.getSongFromMusareId",
 				this.$route.query.songId,
 				res => {
@@ -356,7 +351,7 @@ export default {
 				"Are you sure you want to delete this song?"
 			);
 			if (dialogResult !== true) return;
-			this.socket.emit("songs.remove", id, res => {
+			this.socket.dispatch("songs.remove", id, res => {
 				if (res.status === "success")
 					new Toast({ content: res.message, timeout: 4000 });
 				else new Toast({ content: res.message, timeout: 8000 });
@@ -367,7 +362,7 @@ export default {
 			if (this.position >= this.maxPosition) return;
 			this.isGettingSet = true;
 
-			this.socket.emit("songs.getSet", this.position, data => {
+			this.socket.dispatch("songs.getSet", this.position, data => {
 				data.forEach(song => {
 					this.addSong(song);
 				});
@@ -404,13 +399,13 @@ export default {
 			if (this.songs.length > 0)
 				this.position = Math.ceil(this.songs.length / 15) + 1;
 
-			this.socket.emit("songs.length", length => {
+			this.socket.dispatch("songs.length", length => {
 				this.maxPosition = Math.ceil(length / 15) + 1;
 
 				this.getSet();
 			});
 
-			this.socket.emit("apis.joinAdminRoom", "songs", () => {});
+			this.socket.dispatch("apis.joinAdminRoom", "songs", () => {});
 		},
 		...mapActions("admin/songs", [
 			// "stopVideo",

+ 18 - 19
frontend/src/pages/Admin/tabs/Stations.vue

@@ -184,10 +184,10 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapActions, mapGetters } from "vuex";
 
 import Toast from "toasters";
-import io from "../../../io";
+import ws from "../../../ws";
 
 import EditStation from "../../../components/modals/EditStation.vue";
 import UserIdToUsername from "../../../components/common/UserIdToUsername.vue";
@@ -209,23 +209,22 @@ export default {
 		}),
 		...mapState("modalVisibility", {
 			modals: state => state.modals.admin
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
 		})
 	},
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-			if (this.socket.connected) this.init();
+		if (this.socket.readyState === 1) this.init();
+		ws.onConnect(() => this.init());
 
-			this.socket.on("event:admin.station.added", station => {
-				this.stationAdded(station);
-			});
-			this.socket.on("event:admin.station.removed", stationId => {
-				this.stationRemoved(stationId);
-			});
-			io.onConnect(() => {
-				this.init();
-			});
-		});
+		this.socket.on("event:admin.station.added", station =>
+			this.stationAdded(station)
+		);
+
+		this.socket.on("event:admin.station.removed", stationId =>
+			this.stationRemoved(stationId)
+		);
 	},
 	methods: {
 		createStation() {
@@ -255,7 +254,7 @@ export default {
 					timeout: 3000
 				});
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				"stations.create",
 				{
 					name,
@@ -276,7 +275,7 @@ export default {
 			);
 		},
 		removeStation(index) {
-			this.socket.emit(
+			this.socket.dispatch(
 				"stations.remove",
 				this.stations[index]._id,
 				res => {
@@ -339,10 +338,10 @@ export default {
 			this.newStation.blacklistedGenres.splice(index, 1);
 		},
 		init() {
-			this.socket.emit("stations.index", data => {
+			this.socket.dispatch("stations.index", data => {
 				this.loadStations(data.stations);
 			});
-			this.socket.emit("apis.joinAdminRoom", "stations", () => {});
+			this.socket.dispatch("apis.joinAdminRoom", "stations", () => {});
 		},
 		...mapActions("modalVisibility", ["openModal"]),
 		...mapActions("admin/stations", [

+ 8 - 7
frontend/src/pages/Admin/tabs/Statistics.vue

@@ -102,10 +102,11 @@
 </template>
 
 <script>
+import { mapGetters } from "vuex";
 import { Line } from "chart.js";
 import "chartjs-adapter-date-fns";
 
-import io from "../../../io";
+import ws from "../../../ws";
 
 export default {
 	components: {},
@@ -143,6 +144,9 @@ export default {
 			}
 		};
 	},
+	computed: mapGetters({
+		socket: "websockets/getSocket"
+	}),
 	mounted() {
 		const minuteCtx = document.getElementById("minuteChart");
 		const hourCtx = document.getElementById("hourChart");
@@ -263,15 +267,12 @@ export default {
 			}
 		});
 
-		io.getSocket(socket => {
-			this.socket = socket;
-			if (this.socket.connected) this.init();
-			io.onConnect(() => this.init());
-		});
+		if (this.socket.readyState === 1) this.init();
+		ws.onConnect(() => this.init());
 	},
 	methods: {
 		init() {
-			this.socket.emit("apis.joinAdminRoom", "statistics", () => {});
+			this.socket.dispatch("apis.joinAdminRoom", "statistics", () => {});
 			this.socket.on(
 				"event:admin.statistics.success.units.minute",
 				units => {

+ 9 - 9
frontend/src/pages/Admin/tabs/Users.vue

@@ -66,11 +66,11 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapActions, mapGetters } from "vuex";
 
 import EditUser from "../../../components/modals/EditUser.vue";
 import ProfilePicture from "../../../components/ui/ProfilePicture.vue";
-import io from "../../../io";
+import ws from "../../../ws";
 
 export default {
 	components: { EditUser, ProfilePicture },
@@ -83,14 +83,14 @@ export default {
 	computed: {
 		...mapState("modalVisibility", {
 			modals: state => state.modals.admin
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
 		})
 	},
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-			if (this.socket.connected) this.init();
-			io.onConnect(() => this.init());
-		});
+		if (this.socket.readyState === 1) this.init();
+		ws.onConnect(() => this.init());
 	},
 	methods: {
 		edit(user) {
@@ -98,7 +98,7 @@ export default {
 			this.openModal({ sector: "admin", modal: "editUser" });
 		},
 		init() {
-			this.socket.emit("users.index", res => {
+			this.socket.dispatch("users.index", res => {
 				console.log(res);
 				if (res.status === "success") {
 					this.users = res.data;
@@ -110,7 +110,7 @@ export default {
 					}
 				}
 			});
-			this.socket.emit("apis.joinAdminRoom", "users", () => {});
+			this.socket.dispatch("apis.joinAdminRoom", "users", () => {});
 		},
 		...mapActions("modalVisibility", ["openModal"])
 	}

+ 153 - 153
frontend/src/pages/Home.vue

@@ -437,7 +437,7 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapGetters, mapActions } from "vuex";
 import Toast from "toasters";
 
 import MainHeader from "../components/layout/MainHeader.vue";
@@ -445,7 +445,7 @@ import MainFooter from "../components/layout/MainFooter.vue";
 import CreateCommunityStation from "../components/modals/CreateCommunityStation.vue";
 import UserIdToUsername from "../components/common/UserIdToUsername.vue";
 
-import io from "../io";
+import ws from "../ws";
 
 export default {
 	components: {
@@ -456,9 +456,7 @@ export default {
 	},
 	data() {
 		return {
-			recaptcha: {
-				key: ""
-			},
+			recaptcha: { key: "" },
 			stations: [],
 			searchQuery: "",
 			siteName: "Musare"
@@ -470,6 +468,9 @@ export default {
 			userId: state => state.user.auth.userId,
 			modals: state => state.modalVisibility.modals.home
 		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
+		}),
 		filteredStations() {
 			const privacyOrder = ["public", "unlisted", "private"];
 			return this.stations
@@ -498,165 +499,156 @@ export default {
 	async mounted() {
 		this.siteName = await lofig.get("siteSettings.siteName");
 
-		io.getSocket(socket => {
-			this.socket = socket;
-
-			if (this.socket.connected) this.init();
-			io.onConnect(() => this.init());
-
-			this.socket.on("event:stations.created", res => {
-				const station = res;
-				if (
-					this.stations.find(_station => _station._id === station._id)
-				) {
-					this.stations.forEach(s => {
-						const _station = s;
-						if (_station._id === station._id) {
-							_station.privacy = station.privacy;
-						}
-					});
-				} else {
-					if (!station.currentSong)
-						station.currentSong = {
-							thumbnail: "/assets/notes-transparent.png"
-						};
-					if (station.currentSong && !station.currentSong.thumbnail)
-						station.currentSong.ytThumbnail = `https://img.youtube.com/vi/${station.currentSong.songId}/mqdefault.jpg`;
-					this.stations.push(station);
-				}
-			});
+		if (this.socket.readyState === 1) this.init();
+		ws.onConnect(() => this.init());
 
-			this.socket.on("event:station.removed", response => {
-				const { stationId } = response;
-				const station = this.stations.find(
-					station => station._id === stationId
-				);
-				if (station) {
-					const stationIndex = this.stations.indexOf(station);
-					this.stations.splice(stationIndex, 1);
+		this.socket.on("event:stations.created", res => {
+			const station = res;
+			if (this.stations.find(_station => _station._id === station._id)) {
+				this.stations.forEach(s => {
+					const _station = s;
+					if (_station._id === station._id) {
+						_station.privacy = station.privacy;
+					}
+				});
+			} else {
+				if (!station.currentSong)
+					station.currentSong = {
+						thumbnail: "/assets/notes-transparent.png"
+					};
+				if (station.currentSong && !station.currentSong.thumbnail)
+					station.currentSong.ytThumbnail = `https://img.youtube.com/vi/${station.currentSong.songId}/mqdefault.jpg`;
+				this.stations.push(station);
+			}
+		});
+
+		this.socket.on("event:station.removed", response => {
+			const { stationId } = response;
+			const station = this.stations.find(
+				station => station._id === stationId
+			);
+			if (station) {
+				const stationIndex = this.stations.indexOf(station);
+				this.stations.splice(stationIndex, 1);
+			}
+		});
+
+		this.socket.on("event:userCount.updated", (stationId, userCount) => {
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.userCount = userCount;
 				}
 			});
+		});
 
-			this.socket.on(
-				"event:userCount.updated",
-				(stationId, userCount) => {
-					this.stations.forEach(s => {
-						const station = s;
-						if (station._id === stationId) {
-							station.userCount = userCount;
-						}
-					});
+		this.socket.on("event:station.updatePrivacy", response => {
+			const { stationId, privacy } = response;
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.privacy = privacy;
 				}
-			);
-
-			this.socket.on("event:station.updatePrivacy", response => {
-				const { stationId, privacy } = response;
-				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						station.privacy = privacy;
-					}
-				});
 			});
+		});
 
-			this.socket.on("event:station.updateName", response => {
-				const { stationId, name } = response;
-				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						station.name = name;
-					}
-				});
+		this.socket.on("event:station.updateName", response => {
+			const { stationId, name } = response;
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.name = name;
+				}
 			});
+		});
 
-			this.socket.on("event:station.updateDisplayName", response => {
-				const { stationId, displayName } = response;
-				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						station.displayName = displayName;
-					}
-				});
+		this.socket.on("event:station.updateDisplayName", response => {
+			const { stationId, displayName } = response;
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.displayName = displayName;
+				}
 			});
+		});
 
-			this.socket.on("event:station.updateDescription", response => {
-				const { stationId, description } = response;
-				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						station.description = description;
-					}
-				});
+		this.socket.on("event:station.updateDescription", response => {
+			const { stationId, description } = response;
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.description = description;
+				}
 			});
+		});
 
-			this.socket.on("event:station.updateTheme", response => {
-				const { stationId, theme } = response;
-				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						station.theme = theme;
-					}
-				});
+		this.socket.on("event:station.updateTheme", response => {
+			const { stationId, theme } = response;
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.theme = theme;
+				}
 			});
+		});
 
-			this.socket.on("event:station.nextSong", (stationId, song) => {
-				let newSong = song;
-				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						if (!newSong)
-							newSong = {
-								thumbnail: "/assets/notes-transparent.png"
-							};
-						if (newSong && !newSong.thumbnail)
-							newSong.ytThumbnail = `https://img.youtube.com/vi/${newSong.songId}/mqdefault.jpg`;
-						station.currentSong = newSong;
-					}
-				});
+		this.socket.on("event:station.nextSong", (stationId, song) => {
+			let newSong = song;
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					if (!newSong)
+						newSong = {
+							thumbnail: "/assets/notes-transparent.png"
+						};
+					if (newSong && !newSong.thumbnail)
+						newSong.ytThumbnail = `https://img.youtube.com/vi/${newSong.songId}/mqdefault.jpg`;
+					station.currentSong = newSong;
+				}
 			});
+		});
 
-			this.socket.on("event:station.pause", response => {
-				const { stationId } = response;
-				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						station.paused = true;
-					}
-				});
+		this.socket.on("event:station.pause", response => {
+			const { stationId } = response;
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.paused = true;
+				}
 			});
+		});
 
-			this.socket.on("event:station.resume", response => {
-				const { stationId } = response;
-				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						station.paused = false;
-					}
-				});
+		this.socket.on("event:station.resume", response => {
+			const { stationId } = response;
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.paused = false;
+				}
 			});
+		});
 
-			this.socket.on("event:user.favoritedStation", stationId => {
-				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						station.isFavorited = true;
-					}
-				});
+		this.socket.on("event:user.favoritedStation", stationId => {
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.isFavorited = true;
+				}
 			});
+		});
 
-			this.socket.on("event:user.unfavoritedStation", stationId => {
-				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						station.isFavorited = false;
-					}
-				});
+		this.socket.on("event:user.unfavoritedStation", stationId => {
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.isFavorited = false;
+				}
 			});
 		});
 	},
 	methods: {
 		init() {
-			this.socket.emit("stations.index", data => {
+			this.socket.dispatch("stations.index", data => {
 				this.stations = [];
 
 				if (data.status === "success")
@@ -678,7 +670,7 @@ export default {
 					});
 			});
 
-			this.socket.emit("apis.joinRoom", "home", () => {});
+			this.socket.dispatch("apis.joinRoom", "home", () => {});
 		},
 		isOwner(station) {
 			return station.owner === this.userId;
@@ -687,24 +679,32 @@ export default {
 			return typeof station.currentSong.title !== "undefined";
 		},
 		favoriteStation(station) {
-			this.socket.emit("stations.favoriteStation", station._id, res => {
-				if (res.status === "success") {
-					new Toast({
-						content: "Successfully favorited station.",
-						timeout: 4000
-					});
-				} else new Toast({ content: res.message, timeout: 8000 });
-			});
+			this.socket.dispatch(
+				"stations.favoriteStation",
+				station._id,
+				res => {
+					if (res.status === "success") {
+						new Toast({
+							content: "Successfully favorited station.",
+							timeout: 4000
+						});
+					} else new Toast({ content: res.message, timeout: 8000 });
+				}
+			);
 		},
 		unfavoriteStation(station) {
-			this.socket.emit("stations.unfavoriteStation", station._id, res => {
-				if (res.status === "success") {
-					new Toast({
-						content: "Successfully unfavorited station.",
-						timeout: 4000
-					});
-				} else new Toast({ content: res.message, timeout: 8000 });
-			});
+			this.socket.dispatch(
+				"stations.unfavoriteStation",
+				station._id,
+				res => {
+					if (res.status === "success") {
+						new Toast({
+							content: "Successfully unfavorited station.",
+							timeout: 4000
+						});
+					} else new Toast({ content: res.message, timeout: 8000 });
+				}
+			);
 		},
 		...mapActions("modalVisibility", ["openModal"]),
 		...mapActions("station", ["updateIfStationIsFavorited"])

+ 21 - 22
frontend/src/pages/News.vue

@@ -84,11 +84,10 @@
 
 <script>
 import { format } from "date-fns";
-import Vue from "vue";
+import { mapGetters } from "vuex";
 
 import MainHeader from "../components/layout/MainHeader.vue";
 import MainFooter from "../components/layout/MainFooter.vue";
-import io from "../io";
 
 export default {
 	components: { MainHeader, MainFooter },
@@ -98,28 +97,28 @@ export default {
 			noFound: false
 		};
 	},
+	computed: mapGetters({
+		socket: "websockets/getSocket"
+	}),
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-			this.socket.emit("news.index", res => {
-				this.news = res.data;
-				if (this.news.length === 0) this.noFound = true;
-			});
-			this.socket.on("event:admin.news.created", news => {
-				this.news.unshift(news);
-				this.noFound = false;
-			});
-			this.socket.on("event:admin.news.updated", news => {
-				for (let n = 0; n < this.news.length; n += 1) {
-					if (this.news[n]._id === news._id) {
-						Vue.set(this.news, n, news);
-					}
+		this.socket.dispatch("news.index", res => {
+			this.news = res.data;
+			if (this.news.length === 0) this.noFound = true;
+		});
+		this.socket.on("event:admin.news.created", news => {
+			this.news.unshift(news);
+			this.noFound = false;
+		});
+		this.socket.on("event:admin.news.updated", news => {
+			for (let n = 0; n < this.news.length; n += 1) {
+				if (this.news[n]._id === news._id) {
+					this.$set(this.news, n, news);
 				}
-			});
-			this.socket.on("event:admin.news.removed", news => {
-				this.news = this.news.filter(item => item._id !== news._id);
-				if (this.news.length === 0) this.noFound = true;
-			});
+			}
+		});
+		this.socket.on("event:admin.news.removed", news => {
+			this.news = this.news.filter(item => item._id !== news._id);
+			if (this.news.length === 0) this.noFound = true;
 		});
 	},
 	methods: {

+ 22 - 24
frontend/src/pages/Profile/index.vue

@@ -84,7 +84,7 @@
 </template>
 
 <script>
-import { mapState } from "vuex";
+import { mapState, mapGetters } from "vuex";
 import { format, parseISO } from "date-fns";
 
 import TabQueryHandler from "../../mixins/TabQueryHandler.vue";
@@ -96,8 +96,6 @@ import ProfilePicture from "../../components/ui/ProfilePicture.vue";
 import MainHeader from "../../components/layout/MainHeader.vue";
 import MainFooter from "../../components/layout/MainFooter.vue";
 
-import io from "../../io";
-
 export default {
 	components: {
 		MainHeader,
@@ -124,6 +122,9 @@ export default {
 			...mapState("modalVisibility", {
 				modals: state => state.modals.station
 			})
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
 		})
 	},
 	mounted() {
@@ -133,28 +134,25 @@ export default {
 		)
 			this.tab = this.$route.query.tab;
 
-		io.getSocket(socket => {
-			this.socket = socket;
-
-			this.socket.emit(
-				"users.findByUsername",
-				this.$route.params.username,
-				res => {
-					if (res.status === "error") this.$router.go("/404");
-					else {
-						this.user = res.data;
-
-						this.user.createdAt = format(
-							parseISO(this.user.createdAt),
-							"MMMM do yyyy"
-						);
-
-						this.isUser = true;
-						this.userId = this.user._id;
-					}
+		this.socket.dispatch(
+			"users.findByUsername",
+			this.$route.params.username,
+			res => {
+				if (res.status === "error" || res.status === "failure")
+					this.$router.push("/404");
+				else {
+					this.user = res.data;
+
+					this.user.createdAt = format(
+						parseISO(this.user.createdAt),
+						"MMMM do yyyy"
+					);
+
+					this.isUser = true;
+					this.userId = this.user._id;
 				}
-			);
-		});
+			}
+		);
 	}
 };
 </script>

+ 70 - 66
frontend/src/pages/Profile/tabs/Playlists.vue

@@ -34,7 +34,10 @@
 					:name="!drag ? 'draggable-list-transition' : null"
 				>
 					<div
-						class="item item-draggable"
+						:class="{
+							item: true,
+							'item-draggable': myUserId === userId
+						}"
 						v-for="playlist in playlists"
 						:key="playlist._id"
 					>
@@ -87,9 +90,9 @@
 
 <script>
 import draggable from "vuedraggable";
-import { mapActions, mapState } from "vuex";
+import { mapActions, mapState, mapGetters } from "vuex";
 
-import io from "../../../io";
+import ws from "../../../ws";
 
 import SortablePlaylists from "../../../mixins/SortablePlaylists.vue";
 import PlaylistItem from "../../../components/ui/PlaylistItem.vue";
@@ -123,7 +126,10 @@ export default {
 			set(playlists) {
 				this.$store.commit("user/playlists/setPlaylists", playlists);
 			}
-		}
+		},
+		...mapGetters({
+			socket: "websockets/getSocket"
+		})
 	},
 	mounted() {
 		if (
@@ -132,86 +138,84 @@ export default {
 		)
 			this.tab = this.$route.query.tab;
 
-		io.getSocket(socket => {
-			this.socket = socket;
-
-			if (this.myUserId !== this.userId) {
-				this.socket.emit(
+		if (this.myUserId !== this.userId) {
+			ws.onConnect(() =>
+				this.socket.dispatch(
 					"apis.joinRoom",
 					`profile-${this.userId}-playlists`,
 					() => {}
-				);
-			}
+				)
+			);
+		}
 
-			this.socket.emit("playlists.indexForUser", this.userId, res => {
-				if (res.status === "success") this.setPlaylists(res.data);
-				this.orderOfPlaylists = this.calculatePlaylistOrder(); // order in regards to the database
-			});
+		this.socket.dispatch("playlists.indexForUser", this.userId, res => {
+			if (res.status === "success") this.setPlaylists(res.data);
+			this.orderOfPlaylists = this.calculatePlaylistOrder(); // order in regards to the database
+		});
 
-			this.socket.on("event:playlist.create", playlist => {
-				this.playlists.push(playlist);
-			});
+		this.socket.on("event:playlist.create", playlist => {
+			this.playlists.push(playlist);
+		});
 
-			this.socket.on("event:playlist.delete", playlistId => {
-				this.playlists.forEach((playlist, index) => {
-					if (playlist._id === playlistId) {
-						this.playlists.splice(index, 1);
-					}
-				});
+		this.socket.on("event:playlist.delete", playlistId => {
+			this.playlists.forEach((playlist, index) => {
+				if (playlist._id === playlistId) {
+					this.playlists.splice(index, 1);
+				}
 			});
+		});
 
-			this.socket.on("event:playlist.addSong", data => {
-				this.playlists.forEach((playlist, index) => {
-					if (playlist._id === data.playlistId) {
-						this.playlists[index].songs.push(data.song);
-					}
-				});
+		this.socket.on("event:playlist.addSong", data => {
+			this.playlists.forEach((playlist, index) => {
+				if (playlist._id === data.playlistId) {
+					this.playlists[index].songs.push(data.song);
+				}
 			});
+		});
 
-			this.socket.on("event:playlist.removeSong", data => {
-				this.playlists.forEach((playlist, index) => {
-					if (playlist._id === data.playlistId) {
-						this.playlists[index].songs.forEach((song, index2) => {
-							if (song.songId === data.songId) {
-								this.playlists[index].songs.splice(index2, 1);
-							}
-						});
-					}
-				});
+		this.socket.on("event:playlist.removeSong", data => {
+			this.playlists.forEach((playlist, index) => {
+				if (playlist._id === data.playlistId) {
+					this.playlists[index].songs.forEach((song, index2) => {
+						if (song.songId === data.songId) {
+							this.playlists[index].songs.splice(index2, 1);
+						}
+					});
+				}
 			});
+		});
 
-			this.socket.on("event:playlist.updateDisplayName", data => {
-				this.playlists.forEach((playlist, index) => {
-					if (playlist._id === data.playlistId) {
-						this.playlists[index].displayName = data.displayName;
-					}
-				});
+		this.socket.on("event:playlist.updateDisplayName", data => {
+			this.playlists.forEach((playlist, index) => {
+				if (playlist._id === data.playlistId) {
+					this.playlists[index].displayName = data.displayName;
+				}
 			});
+		});
 
-			this.socket.on("event:playlist.updatePrivacy", data => {
-				this.playlists.forEach((playlist, index) => {
-					if (playlist._id === data.playlist._id) {
-						this.playlists[index].privacy = data.playlist.privacy;
-					}
-				});
+		this.socket.on("event:playlist.updatePrivacy", data => {
+			this.playlists.forEach((playlist, index) => {
+				if (playlist._id === data.playlist._id) {
+					this.playlists[index].privacy = data.playlist.privacy;
+				}
 			});
+		});
 
-			this.socket.on(
-				"event:user.orderOfPlaylists.changed",
-				orderOfPlaylists => {
-					const sortedPlaylists = [];
+		this.socket.on(
+			"event:user.orderOfPlaylists.changed",
+			orderOfPlaylists => {
+				const sortedPlaylists = [];
 
-					this.playlists.forEach(playlist => {
-						sortedPlaylists[
-							orderOfPlaylists.indexOf(playlist._id)
-						] = playlist;
-					});
+				this.playlists.forEach(playlist => {
+					sortedPlaylists[
+						orderOfPlaylists.indexOf(playlist._id)
+					] = playlist;
+				});
 
-					this.playlists = sortedPlaylists;
-					this.orderOfPlaylists = this.calculatePlaylistOrder();
-				}
-			);
-		});
+				this.playlists = sortedPlaylists;
+				this.orderOfPlaylists = this.calculatePlaylistOrder();
+			}
+		);
 	},
 	methods: {
 		showPlaylist(playlistId) {

+ 42 - 34
frontend/src/pages/Profile/tabs/RecentActivity.vue

@@ -39,10 +39,10 @@
 </template>
 
 <script>
-import { mapState } from "vuex";
+import { mapState, mapGetters, mapActions } from "vuex";
 import Toast from "toasters";
 
-import io from "../../../io";
+import ws from "../../../ws";
 
 import ActivityItem from "../../../components/ui/ActivityItem.vue";
 
@@ -56,6 +56,7 @@ export default {
 	},
 	data() {
 		return {
+			username: "",
 			activities: [],
 			position: 1,
 			maxPosition: 1,
@@ -68,51 +69,57 @@ export default {
 			...mapState("modalVisibility", {
 				modals: state => state.modals.station
 			}),
-			myUserId: state => state.user.auth.userId,
-			username: state => state.user.auth.username
+			myUserId: state => state.user.auth.userId
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
 		})
 	},
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-
-			if (this.myUserId !== this.userId) {
-				this.socket.emit(
+		if (this.myUserId !== this.userId) {
+			ws.onConnect(() =>
+				this.socket.dispatch(
 					"apis.joinRoom",
 					`profile-${this.userId}-activities`,
-					() => {}
-				);
-			}
+					res => {
+						console.log("res of joining activities room", res);
+					}
+				)
+			);
 
-			this.socket.emit("activities.length", this.userId, length => {
-				this.maxPosition = Math.ceil(length / 15) + 1;
-				this.getSet();
+			this.getUsernameFromId(this.userId).then(res => {
+				if (res) this.username = res;
 			});
+		}
 
-			this.socket.on("event:activity.create", activity => {
-				this.activities.unshift(activity);
-				this.offsettedFromNextSet += 1;
-			});
+		this.socket.dispatch("activities.length", this.userId, length => {
+			this.maxPosition = Math.ceil(length / 15) + 1;
+			this.getSet();
+		});
+
+		this.socket.on("event:activity.create", activity => {
+			this.activities.unshift(activity);
+			this.offsettedFromNextSet += 1;
+		});
 
-			this.socket.on("event:activity.hide", activityId => {
-				this.activities = this.activities.filter(
-					activity => activity._id !== activityId
-				);
+		this.socket.on("event:activity.hide", activityId => {
+			this.activities = this.activities.filter(
+				activity => activity._id !== activityId
+			);
 
-				this.offsettedFromNextSet -= 1;
-			});
+			this.offsettedFromNextSet -= 1;
+		});
 
-			this.socket.on("event:activity.removeAllForUser", () => {
-				this.activities = [];
-				this.position = 1;
-				this.maxPosition = 1;
-				this.offsettedFromNextSet = 0;
-			});
+		this.socket.on("event:activity.removeAllForUser", () => {
+			this.activities = [];
+			this.position = 1;
+			this.maxPosition = 1;
+			this.offsettedFromNextSet = 0;
 		});
 	},
 	methods: {
 		hideActivity(activityId) {
-			this.socket.emit("activities.hideActivity", activityId, res => {
+			this.socket.dispatch("activities.hideActivity", activityId, res => {
 				if (res.status !== "success")
 					new Toast({ content: res.message, timeout: 3000 });
 			});
@@ -123,7 +130,7 @@ export default {
 
 			this.isGettingSet = true;
 
-			this.socket.emit(
+			this.socket.dispatch(
 				"activities.getSet",
 				this.userId,
 				this.position,
@@ -148,7 +155,8 @@ export default {
 			if (scrollPosition + 100 >= bottomPosition) this.getSet();
 
 			return this.maxPosition === this.position;
-		}
+		},
+		...mapActions("user/auth", ["getUsernameFromId"])
 	}
 };
 </script>

+ 8 - 11
frontend/src/pages/ResetPassword.vue

@@ -227,12 +227,12 @@
 
 <script>
 import Toast from "toasters";
+import { mapGetters } from "vuex";
 
 import MainHeader from "../components/layout/MainHeader.vue";
 import MainFooter from "../components/layout/MainFooter.vue";
 import InputHelpBox from "../components/ui/InputHelpBox.vue";
 
-import io from "../io";
 import validation from "../validation";
 
 export default {
@@ -271,6 +271,9 @@ export default {
 			step: 1
 		};
 	},
+	computed: mapGetters({
+		socket: "websockets/getSocket"
+	}),
 	watch: {
 		email(value) {
 			if (
@@ -305,12 +308,6 @@ export default {
 			this.checkPasswordMatch(this.newPassword, value);
 		}
 	},
-	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-		});
-	},
-
 	methods: {
 		checkPasswordMatch(newPassword, newPasswordAgain) {
 			if (newPasswordAgain !== newPassword) {
@@ -345,7 +342,7 @@ export default {
 			this.hasEmailBeenSentAlready = false;
 
 			if (this.mode === "set") {
-				return this.socket.emit("users.requestPassword", res => {
+				return this.socket.dispatch("users.requestPassword", res => {
 					new Toast({ content: res.message, timeout: 8000 });
 					if (res.status === "success") {
 						this.step = 2;
@@ -353,7 +350,7 @@ export default {
 				});
 			}
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				"users.requestPasswordReset",
 				this.email,
 				res => {
@@ -372,7 +369,7 @@ export default {
 					timeout: 8000
 				});
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				this.mode === "set"
 					? "users.verifyPasswordCode"
 					: "users.verifyPasswordResetCode",
@@ -401,7 +398,7 @@ export default {
 					timeout: 8000
 				});
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				this.mode === "set"
 					? "users.changePasswordWithCode"
 					: "users.changePasswordWithResetCode",

+ 41 - 44
frontend/src/pages/Settings/index.vue

@@ -46,7 +46,7 @@
 </template>
 
 <script>
-import { mapActions } from "vuex";
+import { mapActions, mapGetters } from "vuex";
 import Toast from "toasters";
 
 import TabQueryHandler from "../../mixins/TabQueryHandler.vue";
@@ -59,8 +59,6 @@ import AccountSettings from "./tabs/Account.vue";
 import ProfileSettings from "./tabs/Profile.vue";
 import PreferencesSettings from "./tabs/Preferences.vue";
 
-import io from "../../io";
-
 export default {
 	components: {
 		MainHeader,
@@ -76,6 +74,9 @@ export default {
 			tab: "profile"
 		};
 	},
+	computed: mapGetters({
+		socket: "websockets/getSocket"
+	}),
 	mounted() {
 		if (
 			this.$route.query.tab === "profile" ||
@@ -87,48 +88,44 @@ export default {
 
 		this.localNightmode = this.nightmode;
 
-		io.getSocket(socket => {
-			this.socket = socket;
-
-			this.socket.emit("users.findBySession", res => {
-				if (res.status === "success") {
-					this.setUser(res.data);
-				} else {
-					new Toast({
-						content: "You're not currently signed in.",
-						timeout: 3000
-					});
-				}
-			});
-
-			this.socket.on("event:user.linkPassword", () =>
-				this.updateOriginalUser({
-					property: "password",
-					value: true
-				})
-			);
-
-			this.socket.on("event:user.unlinkPassword", () =>
-				this.updateOriginalUser({
-					property: "password",
-					value: false
-				})
-			);
-
-			this.socket.on("event:user.linkGithub", () =>
-				this.updateOriginalUser({
-					property: "github",
-					value: true
-				})
-			);
-
-			this.socket.on("event:user.unlinkGithub", () =>
-				this.updateOriginalUser({
-					property: "github",
-					value: false
-				})
-			);
+		this.socket.dispatch("users.findBySession", res => {
+			if (res.status === "success") {
+				this.setUser(res.data);
+			} else {
+				new Toast({
+					content: "You're not currently signed in.",
+					timeout: 3000
+				});
+			}
 		});
+
+		this.socket.on("event:user.linkPassword", () =>
+			this.updateOriginalUser({
+				property: "password",
+				value: true
+			})
+		);
+
+		this.socket.on("event:user.unlinkPassword", () =>
+			this.updateOriginalUser({
+				property: "password",
+				value: false
+			})
+		);
+
+		this.socket.on("event:user.linkGithub", () =>
+			this.updateOriginalUser({
+				property: "github",
+				value: true
+			})
+		);
+
+		this.socket.on("event:user.unlinkGithub", () =>
+			this.updateOriginalUser({
+				property: "github",
+				value: false
+			})
+		);
 	},
 	methods: mapActions("settings", ["updateOriginalUser", "setUser"])
 };

+ 16 - 17
frontend/src/pages/Settings/tabs/Account.vue

@@ -96,11 +96,10 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapActions, mapGetters } from "vuex";
 import Toast from "toasters";
 
 import validation from "../../../validation";
-import io from "../../../io";
 
 import InputHelpBox from "../../../components/ui/InputHelpBox.vue";
 import SaveButton from "../../../components/ui/SaveButton.vue";
@@ -123,11 +122,16 @@ export default {
 			}
 		};
 	},
-	computed: mapState({
-		userId: state => state.user.auth.userId,
-		originalUser: state => state.settings.originalUser,
-		modifiedUser: state => state.settings.modifiedUser
-	}),
+	computed: {
+		...mapState({
+			userId: state => state.user.auth.userId,
+			originalUser: state => state.settings.originalUser,
+			modifiedUser: state => state.settings.modifiedUser
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
+		})
+	},
 	watch: {
 		// prettier-ignore
 		// eslint-disable-next-line func-names
@@ -167,11 +171,6 @@ export default {
 			}
 		}
 	},
-	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-		});
-	},
 	methods: {
 		onInputBlur(inputName) {
 			this.validation[inputName].entered = true;
@@ -214,7 +213,7 @@ export default {
 
 			this.$refs.saveButton.saveStatus = "disabled";
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				"users.updateEmail",
 				this.userId,
 				email,
@@ -256,7 +255,7 @@ export default {
 
 			this.$refs.saveButton.saveStatus = "disabled";
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				"users.updateUsername",
 				this.userId,
 				username,
@@ -281,9 +280,9 @@ export default {
 			);
 		},
 		removeAccount() {
-			return this.socket.emit("users.remove", res => {
+			return this.socket.dispatch("users.remove", res => {
 				if (res.status === "success") {
-					return this.socket.emit("users.logout", () => {
+					return this.socket.dispatch("users.logout", () => {
 						return lofig.get("cookie").then(cookie => {
 							document.cookie = `${cookie.SIDname}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
 							return window.location.reload();
@@ -295,7 +294,7 @@ export default {
 			});
 		},
 		removeActivities() {
-			this.socket.emit("activities.removeAllForUser", res => {
+			this.socket.dispatch("activities.removeAllForUser", res => {
 				new Toast({ content: res.message, timeout: 4000 });
 			});
 		},

+ 23 - 23
frontend/src/pages/Settings/tabs/Preferences.vue

@@ -41,10 +41,9 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapActions, mapGetters } from "vuex";
 import Toast from "toasters";
 
-import io from "../../../io";
 import SaveButton from "../../../components/ui/SaveButton.vue";
 
 export default {
@@ -56,28 +55,29 @@ export default {
 			localActivityLogPublic: false
 		};
 	},
-	computed: mapState({
-		nightmode: state => state.user.preferences.nightmode,
-		autoSkipDisliked: state => state.user.preferences.autoSkipDisliked,
-		activityLogPublic: state => state.user.preferences.activityLogPublic
-	}),
+	computed: {
+		...mapState({
+			nightmode: state => state.user.preferences.nightmode,
+			autoSkipDisliked: state => state.user.preferences.autoSkipDisliked,
+			activityLogPublic: state => state.user.preferences.activityLogPublic
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
+		})
+	},
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-
-			this.socket.emit("users.getPreferences", res => {
-				if (res.status === "success") {
-					this.localNightmode = res.data.nightmode;
-					this.localAutoSkipDisliked = res.data.autoSkipDisliked;
-					this.localActivityLogPublic = res.data.activityLogPublic;
-				}
-			});
+		this.socket.dispatch("users.getPreferences", res => {
+			if (res.status === "success") {
+				this.localNightmode = res.data.nightmode;
+				this.localAutoSkipDisliked = res.data.autoSkipDisliked;
+				this.localActivityLogPublic = res.data.activityLogPublic;
+			}
+		});
 
-			socket.on("keep.event:user.preferences.changed", preferences => {
-				this.localNightmode = preferences.nightmode;
-				this.localAutoSkipDisliked = preferences.autoSkipDisliked;
-				this.localActivityLogPublic = preferences.activityLogPublic;
-			});
+		this.socket.on("keep.event:user.preferences.changed", preferences => {
+			this.localNightmode = preferences.nightmode;
+			this.localAutoSkipDisliked = preferences.autoSkipDisliked;
+			this.localActivityLogPublic = preferences.activityLogPublic;
 		});
 	},
 	methods: {
@@ -97,7 +97,7 @@ export default {
 
 			this.$refs.saveButton.status = "disabled";
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				"users.updatePreferences",
 				{
 					nightmode: this.localNightmode,

+ 15 - 16
frontend/src/pages/Settings/tabs/Profile.vue

@@ -73,22 +73,26 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapActions, mapGetters } from "vuex";
 import Toast from "toasters";
 
 import validation from "../../../validation";
-import io from "../../../io";
 
 import ProfilePicture from "../../../components/ui/ProfilePicture.vue";
 import SaveButton from "../../../components/ui/SaveButton.vue";
 
 export default {
 	components: { ProfilePicture, SaveButton },
-	computed: mapState({
-		userId: state => state.user.auth.userId,
-		originalUser: state => state.settings.originalUser,
-		modifiedUser: state => state.settings.modifiedUser
-	}),
+	computed: {
+		...mapState({
+			userId: state => state.user.auth.userId,
+			originalUser: state => state.settings.originalUser,
+			modifiedUser: state => state.settings.modifiedUser
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
+		})
+	},
 	watch: {
 		"modifiedUser.avatar.type": function watchAvatarType(newType, oldType) {
 			if (
@@ -104,11 +108,6 @@ export default {
 			}
 		}
 	},
-	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-		});
-	},
 	methods: {
 		saveChanges() {
 			const nameChanged =
@@ -149,7 +148,7 @@ export default {
 
 			this.$refs.saveButton.status = "disabled";
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				"users.updateName",
 				this.userId,
 				name,
@@ -184,7 +183,7 @@ export default {
 
 			this.$refs.saveButton.status = "disabled";
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				"users.updateLocation",
 				this.userId,
 				location,
@@ -219,7 +218,7 @@ export default {
 
 			this.$refs.saveButton.status = "disabled";
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				"users.updateBio",
 				this.userId,
 				bio,
@@ -248,7 +247,7 @@ export default {
 
 			this.$refs.saveButton.status = "disabled";
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				"users.updateAvatarType",
 				this.userId,
 				avatar,

+ 9 - 16
frontend/src/pages/Settings/tabs/Security.vue

@@ -78,10 +78,7 @@
 
 			<hr class="section-horizontal-rule" />
 
-			<a
-				class="button is-github"
-				:href="`${serverDomain}/auth/github/link`"
-			>
+			<a class="button is-github" :href="`${apiDomain}/auth/github/link`">
 				<div class="icon">
 					<img class="invert" src="/assets/social/github.svg" />
 				</div>
@@ -147,7 +144,6 @@
 import Toast from "toasters";
 import { mapGetters, mapState } from "vuex";
 
-import io from "../../../io";
 import validation from "../../../validation";
 
 import InputHelpBox from "../../../components/ui/InputHelpBox.vue";
@@ -156,7 +152,7 @@ export default {
 	components: { InputHelpBox },
 	data() {
 		return {
-			serverDomain: "",
+			apiDomain: "",
 			previousPassword: "",
 			validation: {
 				newPassword: {
@@ -171,7 +167,8 @@ export default {
 	computed: {
 		...mapGetters({
 			isPasswordLinked: "settings/isPasswordLinked",
-			isGithubLinked: "settings/isGithubLinked"
+			isGithubLinked: "settings/isGithubLinked",
+			socket: "websockets/getSocket"
 		}),
 		...mapState({
 			userId: state => state.user.auth.userId
@@ -195,11 +192,7 @@ export default {
 		}
 	},
 	async mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-		});
-
-		this.serverDomain = await lofig.get("serverDomain");
+		this.apiDomain = await lofig.get("apiDomain");
 	},
 	methods: {
 		onInputBlur(inputName) {
@@ -220,7 +213,7 @@ export default {
 					timeout: 8000
 				});
 
-			return this.socket.emit(
+			return this.socket.dispatch(
 				"users.updatePassword",
 				this.previousPassword,
 				newPassword,
@@ -240,17 +233,17 @@ export default {
 			);
 		},
 		unlinkPassword() {
-			this.socket.emit("users.unlinkPassword", res => {
+			this.socket.dispatch("users.unlinkPassword", res => {
 				new Toast({ content: res.message, timeout: 8000 });
 			});
 		},
 		unlinkGitHub() {
-			this.socket.emit("users.unlinkGitHub", res => {
+			this.socket.dispatch("users.unlinkGitHub", res => {
 				new Toast({ content: res.message, timeout: 8000 });
 			});
 		},
 		removeSessions() {
-			this.socket.emit(`users.removeSessions`, this.userId, res => {
+			this.socket.dispatch(`users.removeSessions`, this.userId, res => {
 				new Toast({ content: res.message, timeout: 4000 });
 			});
 		}

+ 66 - 66
frontend/src/pages/Station/components/Sidebar/MyPlaylists.vue

@@ -64,11 +64,10 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapActions, mapGetters } from "vuex";
 import Toast from "toasters";
 import draggable from "vuedraggable";
 
-import io from "../../../../io";
 import PlaylistItem from "../../../../components/ui/PlaylistItem.vue";
 import SortablePlaylists from "../../../../mixins/SortablePlaylists.vue";
 
@@ -80,83 +79,84 @@ export default {
 			playlists: []
 		};
 	},
-	computed: mapState({
-		station: state => state.station.station
-	}),
+	computed: {
+		...mapState({
+			station: state => state.station.station
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
+		})
+	},
 	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-
-			/** Get playlists for user */
-			this.socket.emit("playlists.indexMyPlaylists", true, res => {
-				if (res.status === "success") this.playlists = res.data;
-				this.orderOfPlaylists = this.calculatePlaylistOrder(); // order in regards to the database
-			});
+		/** Get playlists for user */
+		this.socket.dispatch("playlists.indexMyPlaylists", true, res => {
+			if (res.status === "success") this.playlists = res.data;
+			this.orderOfPlaylists = this.calculatePlaylistOrder(); // order in regards to the database
+		});
 
-			this.socket.on("event:playlist.create", playlist => {
-				this.playlists.push(playlist);
-			});
+		this.socket.on("event:playlist.create", playlist => {
+			this.playlists.push(playlist);
+		});
 
-			this.socket.on("event:playlist.delete", playlistId => {
-				this.playlists.forEach((playlist, index) => {
-					if (playlist._id === playlistId) {
-						this.playlists.splice(index, 1);
-					}
-				});
+		this.socket.on("event:playlist.delete", playlistId => {
+			this.playlists.forEach((playlist, index) => {
+				if (playlist._id === playlistId) {
+					this.playlists.splice(index, 1);
+				}
 			});
+		});
 
-			this.socket.on("event:playlist.addSong", data => {
-				this.playlists.forEach((playlist, index) => {
-					if (playlist._id === data.playlistId) {
-						this.playlists[index].songs.push(data.song);
-					}
-				});
+		this.socket.on("event:playlist.addSong", data => {
+			this.playlists.forEach((playlist, index) => {
+				if (playlist._id === data.playlistId) {
+					this.playlists[index].songs.push(data.song);
+				}
 			});
+		});
 
-			this.socket.on("event:playlist.removeSong", data => {
-				this.playlists.forEach((playlist, index) => {
-					if (playlist._id === data.playlistId) {
-						this.playlists[index].songs.forEach((song, index2) => {
-							if (song.songId === data.songId) {
-								this.playlists[index].songs.splice(index2, 1);
-							}
-						});
-					}
-				});
+		this.socket.on("event:playlist.removeSong", data => {
+			this.playlists.forEach((playlist, index) => {
+				if (playlist._id === data.playlistId) {
+					this.playlists[index].songs.forEach((song, index2) => {
+						if (song.songId === data.songId) {
+							this.playlists[index].songs.splice(index2, 1);
+						}
+					});
+				}
 			});
+		});
 
-			this.socket.on("event:playlist.updateDisplayName", data => {
-				this.playlists.forEach((playlist, index) => {
-					if (playlist._id === data.playlistId) {
-						this.playlists[index].displayName = data.displayName;
-					}
-				});
+		this.socket.on("event:playlist.updateDisplayName", data => {
+			this.playlists.forEach((playlist, index) => {
+				if (playlist._id === data.playlistId) {
+					this.playlists[index].displayName = data.displayName;
+				}
 			});
+		});
 
-			this.socket.on("event:playlist.updatePrivacy", data => {
-				this.playlists.forEach((playlist, index) => {
-					if (playlist._id === data.playlist._id) {
-						this.playlists[index].privacy = data.playlist.privacy;
-					}
-				});
+		this.socket.on("event:playlist.updatePrivacy", data => {
+			this.playlists.forEach((playlist, index) => {
+				if (playlist._id === data.playlist._id) {
+					this.playlists[index].privacy = data.playlist.privacy;
+				}
 			});
+		});
 
-			this.socket.on(
-				"event:user.orderOfPlaylists.changed",
-				orderOfPlaylists => {
-					const sortedPlaylists = [];
+		this.socket.on(
+			"event:user.orderOfPlaylists.changed",
+			orderOfPlaylists => {
+				const sortedPlaylists = [];
 
-					this.playlists.forEach(playlist => {
-						sortedPlaylists[
-							orderOfPlaylists.indexOf(playlist._id)
-						] = playlist;
-					});
+				this.playlists.forEach(playlist => {
+					sortedPlaylists[
+						orderOfPlaylists.indexOf(playlist._id)
+					] = playlist;
+				});
 
-					this.playlists = sortedPlaylists;
-					this.orderOfPlaylists = this.calculatePlaylistOrder();
-				}
-			);
-		});
+				this.playlists = sortedPlaylists;
+				this.orderOfPlaylists = this.calculatePlaylistOrder();
+			}
+		);
 	},
 	methods: {
 		edit(id) {
@@ -164,7 +164,7 @@ export default {
 			this.openModal({ sector: "station", modal: "editPlaylist" });
 		},
 		selectPlaylist(id) {
-			this.socket.emit(
+			this.socket.dispatch(
 				"stations.selectPrivatePlaylist",
 				this.station._id,
 				id,
@@ -179,7 +179,7 @@ export default {
 			);
 		},
 		deselectPlaylist() {
-			this.socket.emit(
+			this.socket.dispatch(
 				"stations.deselectPrivatePlaylist",
 				this.station._id,
 				res => {

+ 1 - 1
frontend/src/pages/Station/components/Sidebar/Queue/index.vue

@@ -118,7 +118,7 @@ export default {
 			return this.loggedIn && this.userRole === "admin";
 		},
 		removeFromQueue(songId) {
-			window.socket.emit(
+			window.socket.dispatch(
 				"stations.removeFromQueue",
 				this.station._id,
 				songId,

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 488 - 446
frontend/src/pages/Station/index.vue


+ 4 - 0
frontend/src/store/index.js

@@ -1,6 +1,9 @@
+/* eslint-disable import/no-cycle */
 import Vue from "vue";
 import Vuex from "vuex";
 
+import websockets from "./modules/websockets";
+
 import user from "./modules/user";
 import settings from "./modules/settings";
 import modalVisibility from "./modules/modalVisibility";
@@ -19,6 +22,7 @@ Vue.use(Vuex);
 
 export default new Vuex.Store({
 	modules: {
+		websockets,
 		user,
 		settings,
 		station,

+ 5 - 9
frontend/src/store/modules/admin.js

@@ -1,6 +1,6 @@
 /* eslint no-param-reassign: 0 */
+/* eslint-disable import/no-cycle */
 
-import Vue from "vue";
 import admin from "../../api/admin/index";
 
 const state = {};
@@ -33,7 +33,7 @@ const modules = {
 			updateSong(state, updatedSong) {
 				state.songs.forEach((song, index) => {
 					if (song._id === updatedSong._id)
-						Vue.set(state.songs, index, updatedSong);
+						this.$set(state.songs, index, updatedSong);
 				});
 			}
 		}
@@ -76,12 +76,8 @@ const modules = {
 				return new Promise((resolve, reject) => {
 					return admin.reports
 						.resolve(reportId)
-						.then(res => {
-							return resolve(res);
-						})
-						.catch(err => {
-							return reject(new Error(err.message));
-						});
+						.then(res => resolve(res))
+						.catch(err => reject(new Error(err.message)));
 				});
 			}
 		},
@@ -118,7 +114,7 @@ const modules = {
 			updateNews(state, updatedNews) {
 				state.news.forEach((news, index) => {
 					if (news._id === updatedNews._id)
-						Vue.set(state.news, index, updatedNews);
+						this.$set(state.news, index, updatedNews);
 				});
 			}
 		}

+ 1 - 0
frontend/src/store/modules/modals/viewReport.js

@@ -1,4 +1,5 @@
 /* eslint no-param-reassign: 0 */
+/* eslint-disable import/no-cycle */
 
 import admin from "../../../api/admin/index";
 

+ 19 - 23
frontend/src/store/modules/user.js

@@ -1,7 +1,8 @@
 /* eslint no-param-reassign: 0 */
+/* eslint-disable import/no-cycle */
 
 import auth from "../../api/auth";
-import io from "../../io";
+import ws from "../../ws";
 import validation from "../../validation";
 
 const state = {};
@@ -116,32 +117,27 @@ const modules = {
 					if (typeof state.userIdMap[`Z${userId}`] !== "string") {
 						if (state.userIdRequested[`Z${userId}`] !== true) {
 							commit("requestingUserId", userId);
-							io.getSocket(socket => {
-								socket.emit(
-									"users.getUsernameFromId",
-									userId,
-									res => {
-										if (res.status === "success") {
-											commit("mapUserId", {
-												userId,
-												username: res.data
-											});
+							ws.socket.dispatch(
+								"users.getUsernameFromId",
+								userId,
+								res => {
+									if (res.status === "success") {
+										commit("mapUserId", {
+											userId,
+											username: res.data
+										});
 
-											state.pendingUserIdCallbacks[
-												`Z${userId}`
-											].forEach(cb => cb(res.data));
+										state.pendingUserIdCallbacks[
+											`Z${userId}`
+										].forEach(cb => cb(res.data));
 
-											commit(
-												"clearPendingCallbacks",
-												userId
-											);
+										commit("clearPendingCallbacks", userId);
 
-											return resolve(res.data);
-										}
-										return resolve();
+										return resolve(res.data);
 									}
-								);
-							});
+									return resolve();
+								}
+							);
 						} else {
 							commit("pendingUsername", {
 								userId,

+ 42 - 0
frontend/src/store/modules/websockets.js

@@ -0,0 +1,42 @@
+/* eslint no-param-reassign: 0 */
+
+const state = {
+	socket: {
+		dispatcher: {}
+	}
+};
+
+const getters = {
+	getSocket: state => state.socket
+};
+
+const actions = {
+	createSocket: ({ commit }, socket) => commit("createSocket", socket)
+};
+
+const mutations = {
+	createSocket(state, socket) {
+		const { listeners } = state.socket.dispatcher;
+		state.socket = socket;
+
+		// only executes if the websocket object is being replaced
+		if (listeners) {
+			// for each listener type
+			Object.keys(listeners).forEach(listenerType =>
+				// for each callback previously present for the listener type
+				listeners[listenerType].forEach(cb =>
+					// add the listener back after the websocket object is reset
+					state.socket.dispatcher.addEventListener(listenerType, cb)
+				)
+			);
+		}
+	}
+};
+
+export default {
+	namespaced: true,
+	state,
+	getters,
+	actions,
+	mutations
+};

+ 156 - 0
frontend/src/ws.js

@@ -0,0 +1,156 @@
+import Toast from "toasters";
+
+// eslint-disable-next-line import/no-cycle
+import store from "./store";
+
+const onConnect = {
+	temp: [],
+	persist: []
+};
+
+const onDisconnect = {
+	temp: [],
+	persist: []
+};
+
+// references for when a dispatch event is ready to callback from server to client
+const CB_REFS = {};
+let CB_REF = 0;
+
+export default {
+	socket: null,
+	dispatcher: null,
+
+	onConnect(...args) {
+		if (args[0] === true) onConnect.persist.push(args[1]);
+		else onConnect.temp.push(args[0]);
+	},
+
+	onDisconnect(...args) {
+		if (args[0] === true) onDisconnect.persist.push(args[1]);
+		else onDisconnect.temp.push(args[0]);
+	},
+
+	clear: () => {
+		onConnect.temp = [];
+		onDisconnect.temp = [];
+	},
+
+	removeAllListeners: () =>
+		Object.keys(CB_REFS).forEach(id => {
+			if (
+				id.indexOf("$event:") !== -1 &&
+				id.indexOf("$event:keep.") === -1
+			)
+				delete CB_REFS[id];
+		}),
+
+	init(url) {
+		class ListenerHandler extends EventTarget {
+			constructor() {
+				super();
+				this.listeners = {};
+			}
+
+			addEventListener(type, cb) {
+				if (!(type in this.listeners)) this.listeners[type] = []; // add the listener type to listeners object
+				this.listeners[type].push(cb); // push the callback
+			}
+
+			// eslint-disable-next-line consistent-return
+			removeEventListener(type, cb) {
+				if (!(type in this.listeners)) return true; // event type doesn't exist
+
+				const stack = this.listeners[type];
+
+				stack.forEach((element, index) => {
+					if (element === cb) stack.splice(index, 1);
+				});
+			}
+
+			dispatchEvent(event) {
+				if (!(event.type in this.listeners)) return true; // event type doesn't exist
+
+				const stack = this.listeners[event.type].slice();
+				stack.forEach(element => element.call(this, event));
+
+				return !event.defaultPrevented;
+			}
+		}
+
+		class CustomWebSocket extends WebSocket {
+			constructor() {
+				super(url);
+				this.dispatcher = new ListenerHandler();
+			}
+
+			on(target, cb) {
+				this.dispatcher.addEventListener(target, event =>
+					cb(...event.detail)
+				);
+			}
+
+			dispatch(...args) {
+				CB_REF += 1;
+
+				if (this.readyState !== 1)
+					return onConnect.temp.push(() => this.dispatch(...args));
+
+				const cb = args[args.length - 1];
+				if (typeof cb === "function") CB_REFS[CB_REF] = cb;
+
+				return this.send(
+					JSON.stringify([...args.slice(0, -1), { CB_REF }])
+				);
+			}
+		}
+
+		this.socket = new CustomWebSocket();
+		store.dispatch("websockets/createSocket", this.socket);
+
+		this.socket.onopen = () => {
+			console.log("IO: SOCKET CONNECTED");
+
+			onConnect.temp.forEach(cb => cb());
+			onConnect.persist.forEach(cb => cb());
+		};
+
+		this.socket.onmessage = message => {
+			const data = JSON.parse(message.data);
+			const name = data.shift(0);
+
+			if (name === "CB_REF") {
+				const CB_REF = data.shift(0);
+				CB_REFS[CB_REF](...data);
+				return delete CB_REFS[CB_REF];
+			}
+
+			if (name === "ERROR") console.log("WS: SOCKET ERROR:", data[0]);
+
+			return this.socket.dispatcher.dispatchEvent(
+				new CustomEvent(name, {
+					detail: data
+				})
+			);
+		};
+
+		this.socket.onclose = () => {
+			console.log("WS: SOCKET DISCONNECTED");
+
+			onDisconnect.temp.forEach(cb => cb());
+			onDisconnect.persist.forEach(cb => cb());
+
+			// try to reconnect every 1000ms
+			setTimeout(() => this.init(url), 1000);
+		};
+
+		this.socket.onerror = err => {
+			console.log("WS: SOCKET ERROR", err);
+
+			new Toast({
+				content: "Cannot perform this action at this time.",
+				timeout: 8000
+			});
+		};
+	}
+};

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä