diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a1e3ce9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*.js] +indent_style = tab +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*] +insert_final_newline = true + +[{package.json,*.yml}] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 1d77459..624ec21 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,17 +1,21 @@ { - "extends": "airbnb", - "rules": { - "comma-dangle": 0, - "max-len": 0, - "no-console": 0, - "no-param-reassign": 0, - "no-shadow": 0, - "consistent-return": 0, - "func-names": 0 - }, - "env": { - "browser": true, - "node": true, - "jquery": true - } + "extends": "airbnb", + "rules": { + "comma-dangle": 0, + "max-len": 0, + "no-console": 0, + "no-param-reassign": 0, + "no-shadow": 0, + "consistent-return": 0, + "func-names": 0, + "indent": ["error", "tab", { "SwitchCase": 1 }], + "strict": 0, + "guard-for-in": "warn", + "no-restricted-syntax": ["warn", "ForInStatement"] + }, + "env": { + "browser": true, + "node": true, + "jquery": true + } } \ No newline at end of file diff --git a/.gitignore b/.gitignore index 61fd71f..63d331a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ webconfig.js webserver/public/lib/js/webconfig.js pidfile log.txt +config.hjson # IDE files .idea diff --git a/.travis.yml b/.travis.yml index f9b90b4..9838d30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,9 @@ language: node_js node_js: +- '6' - '5' -- '5.1' - '4' -- '4.1' env: - CXX=g++-4.8 diff --git a/README.md b/README.md index 84e1e48..27d0acd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# mqp-server [![Version npm](https://img.shields.io/npm/v/mqp-server.svg?style=flat-square)](https://www.npmjs.com/package/mqp-server) [![npm Downloads](https://img.shields.io/npm/dm/mqp-server.svg?style=flat-square)](https://www.npmjs.com/package/mqp-server) [![Build Status](https://img.shields.io/travis/musiqpad/mqp-server/master.svg?style=flat-square)](https://travis-ci.org/musiqpad/mqp-server) +# mqp-server [![Version npm](https://img.shields.io/npm/v/mqp-server.svg?style=flat-square)](https://www.npmjs.com/package/mqp-server) [![npm Downloads](https://img.shields.io/npm/dm/mqp-server.svg?style=flat-square)](https://www.npmjs.com/package/mqp-server) [![Build Status](https://img.shields.io/travis/musiqpad/mqp-server/master.svg?style=flat-square)](https://travis-ci.org/musiqpad/mqp-server) [![devDependency Status](https://david-dm.org/musiqpad/mqp-server/dev-status.svg?style=flat-square)](https://david-dm.org/musiqpad/mqp-server#info=devDependencies) [![NPM](https://nodei.co/npm/mqp-server.png)](https://npmjs.org/package/mqp-server) @@ -17,9 +17,10 @@ The base for creating a self-hosted pad. 2. Download the [latest stable version](https://github.com/musiqpad/mqp-server/releases/latest) 3. Unzip it in the location you want to install 4. Open a terminal and `npm install --production` it -5. Copy the `serverconfig.example.js` to create the file `serverconfig.js` -6. Start the server by running `npm start` -7. If everything went well, there should be no error messages. +5. Start the server by running `npm start` +6. If everything went well, there should be no error messages! + +To change the settings, edit the config.hjson file! If you want to start musiqpad using an application manager like forever, start the app.js file. To see server logs, run `npm run log` You can also download the latest pre-release [here](https://github.com/musiqpad/mqp-server/releases) (rc = release candidate, exp = experimental) @@ -57,7 +58,8 @@ Params: debug: false, stream: false } - } + }, + config: fs.readFileSync('config.hjson'), // example config: config.example.hjson } ``` diff --git a/config.example.hjson b/config.example.hjson new file mode 100644 index 0000000..c667f09 --- /dev/null +++ b/config.example.hjson @@ -0,0 +1,500 @@ +{ + /* _ _ + _ __ ___ _ _ ___(_) __ _ _ __ __ _ __| | + | '_ ` _ \| | | / __| |/ _` | '_ \ / _` |/ _` | + | | | | | | |_| \__ \ | (_| | |_) | (_| | (_| | + |_| |_| |_|\__,_|___/_|\__, | .__/ \__,_|\__,_| + |_|_| + + More infos about the config syntax: https://hjson.org/ + + Set this flag to false to disable web server hosting or true to enable web server hosting. + This is useful if you want to host static files in another web server such as nginx. + + If you are only hosting the socket and want musiqpad to host the frontend set this to false. + */ + hostWebserver: true + socketServer: { + host: "" // Host name or IP that the socket server is located at. Leave blank to bind to process IP address + port: 8082// Leave blank to bind to process PORT + } + webServer: { + address: "" // Leave blank to bind to process IP address. + port: 8080// Leave blank to bind to process PORT. + + redirectHTTP: false// Set to true if you want HTTP redirect to HTTPS. + redirectPort: 80// Required if setting above is true. Set to the port you want to redirect HTTP to HTTPS from (Default: 80). + } + useSSL: false// If you want your pad to be accesible over HTTPS set SSL to true and add the path of your certificates + certificate: { + key: "path-to-key" + cert: "path-to-cert" + } + room: { + name: "Pad Name" // This is your pad name. It is shown as a user friendly description on the lounge and tab name. + slug: "this-is-your-slug" // Slugs are used to identify your pad when connecting to musiqpad! This slug must be unique and all in lowecase. + greet: "Welcome to musiqpad!" + bg: "" // Background image file path. Accepts external images. If this is undefined the default background will be used. + maxCon: 0 + ownerEmail: "user@domain.tld" // This needs to be set then the server restarted to take effect. + guestCanSeeChat: true + bannedCanSeeChat: false + lastmsglimit: 6// How many messages a user can see after joining. + signupcd: 0// How many miliseconds the user cannot do certain things after they sign up. + allowemojis: true + allowrecovery: false + recaptcha: false + queue: { + cycle: true + lock: false + limit: 50 + } + history: { + limit_save: 0 + limit_send: 50 + } + mail: { + confirmation: false// Whether to force user to confirm his email address before he is able to do anything + sender: "user@domain.tld" // Domain should point to this box when using direct mode + + // DIRECT PROBABLY WON'T WORK BECAUSE YOUR IP DOESN'T HAVE A GOOD REPUTATION! + + transport: "direct" // 'smtp', 'direct' or 'xoauth' + options: {} + /* + EXAMPLES: + + -- SMTP -- + transport: "smtp" + options: { + service: "gmail" + auth: { + user: "mail@somewebsite.com", + pass: "pass", + } + } + ---------- + + -- XOAUTH2 -- + transport: "xoauth" + options: { + service: "gmail" + auth: { + xoauth: { + user: '{username}' + clientId: '{Client ID}' + clientSecret: '{Client Secret}' + refreshToken: '{refresh-token}' + accessToken: '{cached access token} + } + } + } + ------------- + + -- DIRECT -- + transport: "direct" + options: {} + ------------- + */ + } + description: + ''' +

Pad Description

+ Here you can put anything you want in HTML! + ''' + tags: { // Tags for Google & co + keywords: "musiqpad" + description: "" + image: "https://cdn.musiqpad.com/img/icon-256.png" // Image on twitter/facebook/slack/... + twitter: "@musiqpad" + description: // A one to two sentence description for search engines & co + ''' + Real time music streaming and chat with friends. Musiqpad is a place where people can discover new music. + ''' + themeColor: "" // a hex color for the theme on chrome for android + favicon: "/pads/lib/img/icon.png" + } + scripts: { // Only if you host the frontend yourself + js: [], // Example: ["https://exapmple.com/musiqpad.js", "lib/js/custom.js"] + css: [] + } + } + apis: { + YT: { + key: "" // Required api key in order for YouTube search to work. + restrictSearchToMusic: false + } + SC: { + key: "" + } + reCaptcha: { + key: "" + secret: "" + } + musiqpad: { + key: "" // This is required in order for your socket to update the musiqpad lounge. Request an API Key here: https://musiqpad.com/lounge + sendLobbyStats: false + } + } + + // The amount of time users stay logged in for before having to login again, eg "2 days", "10h", "7d" + loginExpire: "5d", + db: { + dbType: "level" // Values "level" for LevelDB, "mysql" for MySQL and "mongo" for MongoDB + dbDir: "./socketserver/db" // Only used for LevelDB. Directory to save databases. Default is ./socketserver/db + mysqlUser: "" // Only used for MySQL. Database username + mysqlPassword: "" // Only used for MySQL. Database password + mysqlHost: "" // Only used for MySQL. Host address + mysqlDatabase: "" // Only used for MySQL. Database being used + mongoUser: "" // Only used for MongoDB. Database username + mongoPassword: "" // Only used for MongoDB. Database password + mongoHost: "" // Only used for MongoDB. Host address + mongoDatabase: "" // Only used for MongoDB. Database being used + } + + /* + "djqueue.join": Ability to join queue + "djqueue.joinlocked": Ability to join locked queue + "djqueue.leave": Ability to leave queue + "djqueue.skip.self": Ability to skip self + "djqueue.skip.other": Ability to skip others + "djqueue.lock": Ability to lock/unlock queue + "djqueue.limit": Ability to change waitlist limit + "djqueue.cycle": Ability to enable/disable queue cycle + "djqueue.move": Ability to move, swap, add and remove people in the queue + "djqueue.playLiveVideos": Ability to play live videos with undefined duration + "djqueue.lock.bypass": Bypass locked queue + "djqueue.limit.bypass": Bypass queue limit + "chat.send": Abilty to send chat messages + "chat.delete": Ability to delete others" chat messages + "chat.specialMention": Ability to use @everyone, @guest and @djs as mention + "chat.broadcast": Ability to send a highlighted broadcast message + "chat.private": Ability to send PMs + "chat.staff": Ability to send and receive special staff chat + "playlist.create": Ability to create playlists + "playlist.delete": Ability to delete playlists + "playlist.rename": Ability to rename playlists + "playlist.import": Ability to import playlists + "playlist.shuffle": Ability to shuffle playlists + "room.grantroles": Ability to change user roles (requires canGrantPerms property) + "room.restrict.ban": Ability to ban and unban users + "room.restrict.mute": Ability to mute and unmute users + "room.restrict.mute_silent": Ability to shadow mute and unmute users + "room.ratelimit.bypass": Will bypass ratelimit + "room.whois": Possibility to request additional information about a user + "room.whois.iphistory": Possibility to request all IP addresses that the user logged from since account creation + + NOTE: Changing the PROPERTY NAME will break role assignments. Title can be changed + without breaking things, but property name must stay the same. + */ + + + // Defines the order that roles will appear on the user list + // PROPERTY names. NOT title. (case-sensitive) + roleOrder: [ + "dev" + "owner" + "coowner" + "supervisor" + "bot" + "regular" + "default" + ] + + // Defines which roles are "staff" members + // PROPERTY names. NOT title. (case-sensitive) + staffRoles: [ + "dev" + "owner" + "coowner" + "supervisor" + "bot" + ] + + + /* + + Role Options: + + rolename:{ + title: "", // This is the title that gets displayed on the frontend. + showtitle: true/false, // This is whether or not to display the title on the frontend. + badge: "", // This can be any icon from the mdi package. A list of the icons is available here: https://materialdesignicons.com + style: {}, // This can be used to set specific styles to the Username of a user with this role. + permissions: [], // A list of permissions a user with this role is allowed to use. + canGrantRoles: [], // A list of the roles that a user with this role can grant. I.e. an owner should be able to grant manager. + mention: "" // A custom mention. I.e. "owner" would mention this group when someone typed @owner. + } + + Below are a list of roles we suggest using. + + */ + + // Defines roles and permissions + roles: { + owner: { // REQUIRED ROLE + title: "Owner" + showtitle: true + style: { + color: "#F46B40" + } + permissions: [ + "djqueue.join" + "djqueue.joinlocked" + "djqueue.leave" + "djqueue.skip.self" + "djqueue.skip.other" + "djqueue.lock" + "djqueue.cycle" + "djqueue.limit" + "djqueue.move" + "djqueue.playLiveVideos" + "djqueue.limit.bypass" + "djqueue.lock.bypass" + "chat.send" + "chat.private" + "chat.broadcast" + "chat.delete" + "chat.specialMention" + "chat.staff" + "playlist.create" + "playlist.delete" + "playlist.rename" + "playlist.import" + "playlist.shuffle" + "room.grantroles" + "room.restrict.ban" + "room.restrict.mute" + "room.restrict.mute_silent" + "room.ratelimit.bypass" + "room.whois" + "room.whois.iphistory" + "server.checkForUpdates" + ] + canGrantRoles: [ + "dev" + "coowner" + "supervisor" + "bot" + "regular" + "default" + ] + } + dev: { // OPTIONAL ROLE FOR MUSIQPAD DEVS + title: "Dev" + showtitle: true + style: { + color: "#A77DC2" + } + permissions: [ + "djqueue.join" + "djqueue.joinlocked" + "djqueue.leave" + "djqueue.skip.self" + "djqueue.skip.other" + "djqueue.lock" + "djqueue.cycle" + "djqueue.limit" + "djqueue.move" + "djqueue.playLiveVideos" + "djqueue.limit.bypass" + "djqueue.lock.bypass" + "chat.send" + "chat.private" + "chat.broadcast" + "chat.delete" + "chat.specialMention" + "chat.staff" + "playlist.create" + "playlist.delete" + "playlist.rename" + "playlist.import" + "playlist.shuffle" + "room.grantroles" + "room.restrict.ban" + "room.restrict.mute" + "room.restrict.mute_silent" + "room.ratelimit.bypass" + "room.whois" + ] + canGrantRoles: [ + "dev" + "coowner" + "supervisor" + "bot" + "regular" + "default" + ] + mention: "devs" + } + coowner: { + title: "Co-owner" + showtitle: true + style: { + color: "#89BE6C" + } + permissions: [ + "djqueue.join" + "djqueue.joinlocked" + "djqueue.leave" + "djqueue.skip.self" + "djqueue.skip.other" + "djqueue.lock" + "djqueue.cycle" + "djqueue.limit" + "djqueue.move" + "djqueue.playLiveVideos" + "djqueue.limit.bypass" + "djqueue.lock.bypass" + "chat.send" + "chat.private" + "chat.delete" + "chat.specialMention" + "chat.broadcast" + "chat.staff" + "playlist.create" + "playlist.delete" + "playlist.rename" + "playlist.import" + "playlist.shuffle" + "room.grantroles" + "room.restrict.ban" + "room.restrict.mute" + "room.restrict.mute_silent" + "room.ratelimit.bypass" + "room.whois" + "room.whois.iphistory" + ] + canGrantRoles: [ + "supervisor" + "bot" + "regular" + "default" + ] + } + supervisor: { + title: "Supervisor" + showtitle: true + style: { + color: "#009CDD" + } + permissions: [ + "djqueue.join" + "djqueue.joinlocked" + "djqueue.leave" + "djqueue.skip.self" + "djqueue.skip.other" + "djqueue.lock" + "djqueue.cycle" + "djqueue.move" + "djqueue.playLiveVideos" + "djqueue.limit.bypass" + "djqueue.lock.bypass" + "chat.send" + "chat.private" + "chat.delete" + "chat.specialMention" + "chat.staff" + "playlist.create" + "playlist.delete" + "playlist.rename" + "playlist.import" + "playlist.shuffle" + "room.grantroles" + "room.restrict.ban" + "room.restrict.mute" + "room.restrict.mute_silent" + "room.ratelimit.bypass" + "room.whois" + ] + canGrantRoles: [ + "regular" + "default" + ] + } + bot: { + title: "Bot" + showtitle: true + badge: "android" + style: { + color: "#964B74" + } + permissions: [ + "djqueue.join" + "djqueue.joinlocked" + "djqueue.leave" + "djqueue.skip.self" + "djqueue.skip.other" + "djqueue.lock" + "djqueue.cycle" + "djqueue.limit" + "djqueue.move" + "djqueue.playLiveVideos" + "djqueue.limit.bypass" + "djqueue.lock.bypass" + "chat.send" + "chat.private" + "chat.delete" + "chat.specialMention" + "chat.broadcast" + "chat.staff" + "playlist.create" + "playlist.delete" + "playlist.rename" + "playlist.import" + "playlist.shuffle" + "room.grantroles" + "room.restrict.ban" + "room.restrict.mute" + "room.restrict.mute_silent" + "room.ratelimit.bypass" + "room.whois" + "room.whois.iphistory" + ] + canGrantRoles: [ + ] + } + regular: { + title: "Regular" + showtitle: false + style: { + color: "#925AFF" + } + permissions: [ + "djqueue.join" + "djqueue.joinlocked" + "djqueue.leave" + "chat.send" + "chat.private" + "djqueue.skip.self" + "playlist.create" + "playlist.delete" + "playlist.rename" + "playlist.import" + ] + canGrantRoles: [ + ] + } + default: { // REQUIRED ROLE + title: "Default" + showtitle: false + style: { + color: "#ffffff" + } + permissions: [ + "djqueue.join" + "djqueue.leave" + "chat.send" + "chat.private" + "djqueue.skip.self" + "playlist.create" + "playlist.delete" + "playlist.rename" + "playlist.import" + ] + canGrantRoles: [ + ] + } + } + tokenSecret: "" // This should be a random string that needs to be kept private. Automatically generated if empty. +} diff --git a/mqp.js b/mqp.js index 38ca176..8bdeb7e 100644 --- a/mqp.js +++ b/mqp.js @@ -1,5 +1,4 @@ const chalk = require('chalk'); -const cproc = require('child_process'); const fs = require('fs'); const daemon = require('daemon'); const path = require('path'); @@ -13,14 +12,13 @@ const notifier = updateNotifier({ updateCheckInterval: 0, }); if (notifier.update) { - console.log('Update available ' + chalk.dim(notifier.update.current) + chalk.reset(' → ') + chalk.green(notifier.update.latest)); -} else { + console.log(`Update available ${chalk.dim(notifier.update.current)}${chalk.reset(' → ')}${chalk.green(notifier.update.latest)}`); } function getRunningPid(callback) { - fs.readFile(__dirname + '/pidfile', { + fs.readFile(`${__dirname}/pidfile`, { encoding: 'utf-8', - }, function (err, pid) { + }, (err, pid) => { if (err) { return callback(err); } @@ -36,25 +34,26 @@ function getRunningPid(callback) { switch (process.argv[2]) { case 'start': - getRunningPid(function (err, pid) { + getRunningPid((err, pid) => { if (!err) { console.log('Musiqpad is already running!'); } else { console.log('\nStarting musiqpad'); - console.log(' "' + chalk.yellow.bold('npm stop') + '" to stop the musiqpad server'); - console.log(' "' + chalk.yellow.bold('npm run log') + '" to view server output'); - console.log(' "' + chalk.yellow.bold('npm restart') + '" to restart musiqpad'); + console.log(` "${chalk.yellow.bold('npm stop')}" to stop the musiqpad server`); + console.log(` "${chalk.yellow.bold('npm run log')}" to view server output`); + console.log(` "${chalk.yellow.bold('npm restart')}" to restart musiqpad`); // Spawn a new musiqpad daemon process, might need some more settings but I'm waiting for the new config storage for that. - daemon.daemon(__dirname + '/start.js', '--daemon', { + daemon.daemon(`${__dirname}/start.js`, '--daemon', { stdout: fs.openSync(path.join(process.cwd(), 'log.txt'), 'a'), + stderr: fs.openSync(path.join(process.cwd(), 'log.txt'), 'a'), }); } }); break; case 'stop': - getRunningPid(function (err, pid) { + getRunningPid((err, pid) => { if (!err) { process.kill(pid, 'SIGTERM'); console.log('Stopping musiqpad!'); @@ -65,32 +64,30 @@ switch (process.argv[2]) { break; case 'restart': - getRunningPid(function (err, pid) { + getRunningPid((err, pid) => { if (!err) { process.kill(pid, 'SIGTERM'); console.log('\nRestarting musiqpad'); - daemon.daemon(__dirname + '/start.js', '--daemon', { + daemon.daemon(`${__dirname}/start.js`, '--daemon', { stdout: fs.openSync(path.join(process.cwd(), 'log.txt'), 'a'), + stderr: fs.openSync(path.join(process.cwd(), 'log.txt'), 'a'), }); - } else { console.log('musiqpad could not be restarted, as a running instance could not be found.'); } }); break; - case 'log': - console.log('Type ' + 'Ctrl-C ' + 'to exit'); - - ft = tail.startTailing('./log.txt'); - ft.on('line', function (line) { + case 'log': { + console.log('Type Ctrl-C to exit'); + const ft = tail.startTailing('./log.txt'); + ft.on('line', line => { console.log(line); }); - break; - + } case 'update': - getRunningPid(function (err, pid) { + getRunningPid((err, pid) => { if (!err) { process.kill(pid, 'SIGTERM'); console.log('Stopping musiqpad!'); diff --git a/package.json b/package.json index aac3fc9..8d946fc 100644 --- a/package.json +++ b/package.json @@ -1,63 +1,69 @@ -{ - "name": "mqp-server", - "version": "0.7.1", - "description": "musiqpad self-hosted server", - "main": "server-package.js", - "author": "musiqpad Team ", - "private": false, - "repository": { - "type": "git", - "url": "git+https://github.com/musiqpad/mqp-server.git" - }, - "license": "MIT", - "bugs": { - "url": "https://github.com/musiqpad/mqp-server/issues" - }, - "homepage": "https://github.com/musiqpad/mqp-server#readme", - "scripts": { - "start": "node ./mqp.js start", - "stop": "node ./mqp.js stop", - "restart": "node ./mqp.js restart", - "log": "node ./mqp.js log", - "update": "node ./mqp.js update", - "test": "ava --verbose" - }, - "dependencies": { - "basic-logger": "^0.4.4", - "chalk": "^1.0.0", - "clean-css": "^3.4.9", - "compression": "^1.6.2", - "daemon": "^1.1.0", - "deasync": "^0.1.4", - "download-git-repo": "^0.1.2", - "durationjs": "^1.1.1", - "express": "^4.13.3", - "extend": "^3.0.0", - "file-tail": "^0.3.0", - "forever": "^0.15.1", - "leveldown": "1.4.4", - "levelup": "^1.3.1", - "mongodb": "^2.1.16", - "mysql": "^2.10.2", - "nodemailer": "^2.1.0", - "path": "^0.12.7", - "ps-tree": "^1.0.1", - "request": "^2.67.0", - "update-notifier": "^0.7.0", - "ws": "^1.0.1", - "xoauth2": "^1.1.0", - "yesno": "0.0.1" - }, - "devDependencies": { - "ava": "^0.15.2", - "eslint": "^2.13.1", - "eslint-config-airbnb": "^9.0.1", - "eslint-plugin-import": "^1.9.2", - "eslint-plugin-jsx-a11y": "^1.5.3", - "eslint-plugin-react": "^5.2.2" - }, - "ava": { - "concurrency": 5, - "failFast": true - } -} +{ + "name": "mqp-server", + "version": "0.8.0", + "description": "musiqpad self-hosted server", + "main": "server-package.js", + "author": "musiqpad Team ", + "private": false, + "repository": { + "type": "git", + "url": "git+https://github.com/musiqpad/mqp-server.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/musiqpad/mqp-server/issues" + }, + "homepage": "https://github.com/musiqpad/mqp-server#readme", + "scripts": { + "start": "node ./mqp.js start", + "stop": "node ./mqp.js stop", + "restart": "node ./mqp.js restart", + "log": "node ./mqp.js log", + "update": "node ./mqp.js update", + "test": "ava --verbose" + }, + "dependencies": { + "basic-logger": "^0.4.4", + "bcrypt-nodejs": "0.0.3", + "chalk": "^1.0.0", + "clean-css": "^3.4.9", + "compression": "^1.6.2", + "daemon": "^1.1.0", + "deasync": "^0.1.4", + "download-git-repo": "^0.1.2", + "durationjs": "^1.1.1", + "ejs": "^2.4.2", + "express": "^4.13.3", + "extend": "^3.0.0", + "file-tail": "^0.3.0", + "forever": "^0.15.1", + "fs-extra": "^0.30.0", + "helmet": "^2.1.1", + "hjson": "^1.8.4", + "jsonwebtoken": "^7.1.6", + "leveldown": "1.4.4", + "levelup": "^1.3.1", + "mongodb": "^2.2.4", + "mysql": "^2.10.2", + "nconf": "^0.8.4", + "nodemailer": "^2.1.0", + "ps-tree": "^1.0.1", + "request": "^2.74.0", + "update-notifier": "^1.0.2", + "ws": "^1.1.1", + "xoauth2": "^1.1.0", + "yesno": "0.0.1" + }, + "devDependencies": { + "ava": "^0.15.2", + "eslint": "^2.13.1", + "eslint-config-airbnb": "^9.0.1", + "eslint-plugin-import": "^1.11.1", + "eslint-plugin-jsx-a11y": "^1.5.5", + "eslint-plugin-react": "^5.2.2" + }, + "ava": { + "concurrency": 5, + "failFast": true + } +} diff --git a/server-package.js b/server-package.js index 5fb5553..bb5937f 100644 --- a/server-package.js +++ b/server-package.js @@ -5,6 +5,7 @@ var log = new(require('basic-logger'))({ showTimestamp: true, prefix: "ServerContainer" }); +var fs = require('fs'); var extend = require('extend'); @@ -23,8 +24,11 @@ var server = function (params) { } } extend(true, this.settings, params); - + this.start = function() { + if(this.settings.config) { + fs.writeFileSync('./config.hjson', this.settings.config, 'utf8'); + } if (this.settings.forever.enabled) { forever.load(this.settings.forever.options); that.pid = forever.start('./start.js'); @@ -35,11 +39,11 @@ var server = function (params) { }); } }; - + this.stop = function() { stopServer(); }; - + function stopServer() { if (that.settings.forever.enabled) { forever.stop(); diff --git a/serverconfig.example.js b/serverconfig.example.js deleted file mode 100644 index df0d5e5..0000000 --- a/serverconfig.example.js +++ /dev/null @@ -1,418 +0,0 @@ -var fs = require('fs'); -var config = {}; - -// IMPORTANT: In order to be able to launch the musiqpad server, set this to true -config.setup = false; - -/* - Set this flag to false to disable web server hosting or true to enable web server hosting. - This is useful if you want to host static files in another web server such as nginx. - - If you are only hosting the socket and want musiqpad to host the frontend set this to false. -*/ -config.hostWebserver = false; - -config.socketServer = { - host: '', // Host name or IP that the socket server is located at. Leave blank to bind to process IP address - port: '8082', // Leave blank to bind to process PORT -}; - -config.webServer = { - address: '', // Leave blank to bind to process IP address - port: '8080', // Leave blank to bind to process PORT - - redirectHTTP: false, // Set to true if you want HTTP redirect to HTTPS. - redirectPort: '80' // Required if setting above is true. Set to the port you want to redirect HTTP to HTTPS from (Default: 80). -}; - -config.useSSL = true; - -config.certificate = { -// key: fs.readFileSync('../cert.key'), -// cert: fs.readFileSync('../cert.crt') -}; - -config.room = { - name: 'Pad Name', // This is your pad name. It is shown as a user friendly description on the lounge and tab name. - slug: 'this-is-your-slug', // Slugs are used to identify your pad when connecting to musiqpad! This slug must be unique and all in lowecase. - greet: 'Welcome to musiqpad!', - //bg: null, // Background image file path. Accepts external images. If this is undefined the default background will be used. - maxCon: 0, - ownerEmail: 'pad.owner@self-hosted.com', // This needs to be set, then the server restarted to take effect. - guestCanSeeChat: true, - bannedCanSeeChat: false, - lastmsglimit: 6, // How many messages a user can see after joining. - signupcd: 0, // How many miliseconds the user cannot do certain things after they sign up. - allowemojis: true, - allowrecovery: false, - recaptcha: false, - queue: { - cycle: true, - lock: false, - limit: 50, - }, - history: { - limit_save: 0, - limit_send: 50, - }, - email: { - confirmation: false, // Whether to force user to confirm his email address before he is able to do anything - sender: 'your@email.tld', - /* - description: Email server setup, please refer to https://github.com/nodemailer/nodemailer documention on what the options are, supports xOAuth 2.0 - default: {} - */ - options: {}, - }, - description: '\ -

Pad Description

\ - Here you can put anything you want in HTML!\ - ', -}; - -config.apis = { - YT: { - key: '', // Required api key in order for YouTube search to work. - restrictSearchToMusic: false, - }, - SC: { - key: '', - }, - reCaptcha: { - key: '', - secret: '', - }, - musiqpad: { - key: '', // This is required in order for your socket to update the musiqpad lounge. Request an API Key here: https://musiqpad.com/lounge - sendLobbyStats: true, - }, -}; - -// The amount of time users stay logged in for before having to login again in days. -// 0 = login every time; -config.loginExpire = 7; - -// Database config -config.db = { - dbType: 'level', // Values "level" for LevelDB, "mysql" for MySQL and "mongo" for MongoDB - dbDir: './socketserver/db', // Only used for LevelDB. Directory to save databases. Default is ./socketserver/db - mysqlUser: '', // Only used for MySQL. Database username - mysqlPassword: '', // Only used for MySQL. Database password - mysqlHost: '', // Only used for MySQL. Host address - mysqlDatabase: '', // Only used for MySQL. Database being used - mongoUser: '', // Only used for MongoDB. Database username - mongoPassword: '', // Only used for MongoDB. Database password - mongoHost: '', // Only used for MongoDB. Host address - mongoDatabase: '' // Only used for MongoDB. Database being used -}; - -/* - 'djqueue.join': Ability to join queue - 'djqueue.joinlocked': Ability to join locked queue - 'djqueue.leave': Ability to leave queue - 'djqueue.skip.self': Ability to skip self - 'djqueue.skip.other': Ability to skip others - 'djqueue.lock': Ability to lock/unlock queue - 'djqueue.limit': Ability to change waitlist limit - 'djqueue.cycle': Ability to enable/disable queue cycle - 'djqueue.move': Ability to move, swap, add and remove people in the queue - 'djqueue.playLiveVideos': Ability to play live videos with undefined duration - 'djqueue.lock.bypass': Bypass locked queue - 'djqueue.limit.bypass': Bypass queue limit - 'chat.send': Abilty to send chat messages - 'chat.delete': Ability to delete others' chat messages - 'chat.specialMention': Ability to use @everyone, @guest and @djs as mention - 'chat.broadcast': Ability to send a highlighted broadcast message - 'chat.private': Ability to send PMs - 'chat.staff': Ability to send and receive special staff chat - 'playlist.create': Ability to create playlists - 'playlist.delete': Ability to delete playlists - 'playlist.rename': Ability to rename playlists - 'playlist.import': Ability to import playlists - 'playlist.shuffle': Ability to shuffle playlists - 'room.grantroles': Ability to change user roles (requires canGrantPerms property) - 'room.restrict.ban': Ability to ban and unban users - 'room.restrict.mute': Ability to mute and unmute users - 'room.restrict.mute_silent': Ability to shadow mute and unmute users - 'room.ratelimit.bypass': Will bypass ratelimit - 'room.whois': Possibility to request additional information about a user - 'room.whois.iphistory': Possibility to request all IP addresses that the user logged from since account creation - - NOTE: Changing the PROPERTY NAME will break role assignments. Title can be changed - without breaking things, but property name must stay the same. -*/ - -// Defines the order that roles will appear on the user list -// PROPERTY names. NOT title. (case-sensitive) -config.roleOrder = ['dev', 'owner', 'coowner', 'supervisor', 'bot', 'regular', 'default']; - - -// Defines which roles are 'staff' members -// PROPERTY names. NOT title. (case-sensitive) -config.staffRoles = ['dev', 'owner', 'coowner', 'supervisor', 'bot']; - - -/* - -Role Options: - -rolename:{ - title: '', // This is the title that gets displayed on the frontend. - showtitle: true/false, // This is whether or not to display the title on the frontend. - badge: '', // This can be any icon from the mdi package. A list of the icons is available here: https://materialdesignicons.com - style: {}, // This can be used to set specific styles to the Username of a user with this role. - permissions: [], // A list of permissions a user with this role is allowed to use. - canGrantRoles: [], // A list of the roles that a user with this role can grant. I.e. an owner should be able to grant manager. - mention: '' // A custom mention. I.e. 'owner' would mention this group when someone typed @owner. -} - -Below are a list of roles we suggest using. - -*/ - -// Defines roles and permissions -config.roles = { - owner: { // REQUIRED ROLE - title: 'Owner', - showtitle: true, - style: { - 'color': '#F46B40' - }, - permissions: [ - 'djqueue.join', - 'djqueue.joinlocked', - 'djqueue.leave', - 'djqueue.skip.self', - 'djqueue.skip.other', - 'djqueue.lock', - 'djqueue.cycle', - 'djqueue.limit', - 'djqueue.move', - 'djqueue.playLiveVideos', - 'djqueue.limit.bypass', - 'djqueue.lock.bypass', - 'chat.send', - 'chat.private', - 'chat.broadcast', - 'chat.delete', - 'chat.specialMention', - 'chat.staff', - 'playlist.create', - 'playlist.delete', - 'playlist.rename', - 'playlist.import', - 'playlist.shuffle', - 'room.grantroles', - 'room.restrict.ban', - 'room.restrict.mute', - 'room.restrict.mute_silent', - 'room.ratelimit.bypass', - 'room.whois', - 'room.whois.iphistory', - 'server.checkForUpdates', - ], - canGrantRoles: [ - 'dev', - 'coowner', - 'supervisor', - 'bot', - 'regular', - 'default', - ], - }, - dev: { // OPTIONAL ROLE - FOR MUSIQPAD DEVELOPERS - title: 'Dev', - showtitle: true, - style: { - 'color': '#A77DC2' - }, - permissions: [ - 'djqueue.join', - 'djqueue.joinlocked', - 'djqueue.leave', - 'djqueue.skip.self', - 'djqueue.skip.other', - 'djqueue.lock', - 'djqueue.cycle', - 'djqueue.limit', - 'djqueue.move', - 'djqueue.playLiveVideos', - 'djqueue.limit.bypass', - 'djqueue.lock.bypass', - 'chat.send', - 'chat.private', - 'chat.broadcast', - 'chat.delete', - 'chat.specialMention', - 'chat.staff', - 'playlist.create', - 'playlist.delete', - 'playlist.rename', - 'playlist.import', - 'playlist.shuffle', - 'room.grantroles', - 'room.restrict.ban', - 'room.restrict.mute', - 'room.restrict.mute_silent', - 'room.ratelimit.bypass', - 'room.whois', - ], - canGrantRoles: [ - 'dev', - 'coowner', - 'supervisor', - 'bot', - 'regular', - 'default' - ], - mention: 'devs', - }, - coowner: { - title: 'Co-owner', - showtitle: true, - style: { - 'color': '#89BE6C' - }, - permissions: [ - 'djqueue.join', - 'djqueue.joinlocked', - 'djqueue.leave', - 'djqueue.skip.self', - 'djqueue.skip.other', - 'djqueue.lock', - 'djqueue.cycle', - 'djqueue.limit', - 'djqueue.move', - 'djqueue.playLiveVideos', - 'djqueue.limit.bypass', - 'djqueue.lock.bypass', - 'chat.send', - 'chat.private', - 'chat.delete', - 'chat.specialMention', - 'chat.broadcast', - 'chat.staff', - 'playlist.create', - 'playlist.delete', - 'playlist.rename', - 'playlist.import', - 'playlist.shuffle', - 'room.grantroles', - 'room.restrict.ban', - 'room.restrict.mute', - 'room.restrict.mute_silent', - 'room.ratelimit.bypass', - 'room.whois', - 'room.whois.iphistory', - ], - canGrantRoles: [ - 'supervisor', - 'bot', - 'regular', - 'default', - ], - }, - supervisor: { - title: 'Supervisor', - showtitle: true, - style: { - 'color': '#009CDD' - }, - permissions: [ - 'djqueue.join', - 'djqueue.joinlocked', - 'djqueue.leave', - 'djqueue.skip.self', - 'djqueue.skip.other', - 'djqueue.lock', - 'djqueue.cycle', - 'djqueue.move', - 'djqueue.playLiveVideos', - 'djqueue.limit.bypass', - 'djqueue.lock.bypass', - 'chat.send', - 'chat.private', - 'chat.delete', - 'chat.specialMention', - 'chat.staff', - 'playlist.create', - 'playlist.delete', - 'playlist.rename', - 'playlist.import', - 'playlist.shuffle', - 'room.grantroles', - 'room.restrict.ban', - 'room.restrict.mute', - 'room.restrict.mute_silent', - 'room.ratelimit.bypass', - 'room.whois', - ], - canGrantRoles: [ - 'regular', - 'default' - ], - }, - bot: { - title: 'Bot', - showtitle: true, - badge: 'android', - style: { - 'color': '#964B74' - }, - permissions: [ - 'djqueue.skip.other', - 'djqueue.lock', - 'djqueue.cycle', - 'djqueue.move', - 'chat.send', - 'chat.delete', - 'chat.specialMention', - 'room.restrict.ban', - 'room.restrict.mute', - 'room.restrict.mute_silent', - 'room.ratelimit.bypass', - ], - canGrantRoles: [], - }, - regular: { - title: 'Regular', - showtitle: false, - style: { - 'color': '#925AFF' - }, - permissions: [ - 'djqueue.join', - 'djqueue.joinlocked', - 'djqueue.leave', - 'chat.send', - 'chat.private', - 'djqueue.skip.self', - 'playlist.create', - 'playlist.delete', - 'playlist.rename', - 'playlist.import', - ], - canGrantRoles: [], - }, - default: { // REQUIRED ROLE - title: 'Default', - showtitle: false, - style: { - 'color': '#ffffff' - }, - permissions: [ - 'djqueue.join', - 'djqueue.leave', - 'chat.send', - 'chat.private', - 'djqueue.skip.self', - 'playlist.create', - 'playlist.delete', - 'playlist.rename', - 'playlist.import' - ], - canGrantRoles: [], - } -}; - -module.exports = config; diff --git a/socketserver/SC.js b/socketserver/SC.js index 3dce6ea..ee1bd96 100644 --- a/socketserver/SC.js +++ b/socketserver/SC.js @@ -1,11 +1,11 @@ // API reference: https://developers.soundcloud.com/docs/api/reference#track -var https = require('https'); -var util = require('util'); -var log = new (require('basic-logger'))({showTimestamp: true, prefix: "SC"}); -var querystring = require('querystring'); -var config = require('../serverconfig'); -var key = config.apis.SC.key; +const https = require('https'); +const util = require('util'); +const log = new (require('basic-logger'))({showTimestamp: true, prefix: "SC"}); +const querystring = require('querystring'); +const nconf = require('nconf'); +const key = nconf.get('apis:SC:key'); var SC = function(){ }; diff --git a/socketserver/YT.js b/socketserver/YT.js index 89cfb0e..06ae7a9 100644 --- a/socketserver/YT.js +++ b/socketserver/YT.js @@ -1,10 +1,10 @@ -var https = require('https'); -var util = require('util'); -var log = new (require('basic-logger'))({showTimestamp: true, prefix: "YT"}); -var querystring = require('querystring'); -var Duration = require("durationjs"); -var config = require('../serverconfig'); -var key = key = config.apis.YT.key; +const https = require('https'); +const util = require('util'); +const log = new (require('basic-logger'))({showTimestamp: true, prefix: "YT"}); +const querystring = require('querystring'); +const Duration = require("durationjs"); +const nconf = require('nconf'); +const key = nconf.get('apis:YT:key'); https.globalAgent.keepAlive = true; https.globalAgent.keepAliveMsecs = 60e3; @@ -146,7 +146,7 @@ YT.prototype.search = function(query, callback){ key: key }; - if (config.apis.YT.restrictSearchToMusic) + if (nconf.get('apis:YT:restrictSearchToMusic')) inObj.videoCategoryId = 10; // This is restricting the search to things categorized as music var url = "https://www.googleapis.com/youtube/v3/search?" + querystring.stringify(inObj); diff --git a/socketserver/database.js b/socketserver/database.js index 9f2cfc0..da5932f 100644 --- a/socketserver/database.js +++ b/socketserver/database.js @@ -1,16 +1,100 @@ -var config = require('../serverconfig'); +'use strict'; +const nconf = require('nconf'); +const LevelDB = require('./db_level'); +const MySQL = require('./db_mysql'); +const MongoDB = require('./db_mongo'); +const utils = require('./utils'); +let Database; -function Database(){ - config.db.dbType = config.db.dbType.toLowerCase() || 'level'; +var util = require('util'); - switch(config.db.dbType){ - case 'level': - return require('./db_level'); - case 'mysql': - return require('./db_mysql'); - case 'mongo': - return require('./db_mongo'); - } +switch (nconf.get('db:dbType')) { + case 'level': + Database = LevelDB; + break; + case 'mysql': + Database = MySQL; + break; + case 'mongo': + Database = MongoDB; + break; + default: + Database = LevelDB; } -module.exports = new Database(); \ No newline at end of file +function loginCallback(callback) { + return function (err, user, email) { + if (email) { + callback(null, user, utils.token.createToken({ email }, nconf.get('tokenSecret'), nconf.get('loginExpire'))); + return; + } + callback(err); + }; +} + +class DB extends Database { + loginUser(obj, callback) { + if (obj.token) { + try { + obj.email = utils.token.verify(obj.token, nconf.get('tokenSecret')).email; + } catch (e) { + if (e) { + callback('InvalidToken'); + return; + } + } + } + + this.getUser(obj.email, (err, user) => { + if ((err && err.notFound) || user == null) { + callback('UserNotFound'); + return; + } + + if (err) { + callback(err); + return; + } + // If the user has an old md5 password saved in the db + if (typeof user.data.pw === 'string' && utils.hash.isMD5(user.data.pw) && !obj.token) { + // And if that md5 password matches with the supplied pw + if (utils.db.makePassMD5(obj.pw, user.data.salt) !== user.data.pw) { + callback('IncorrectPassword'); + return; + } + // Update the pw to a new bcrypt password + user.pw = obj.pw; + super.loginUser(obj.email, loginCallback(callback)); + // If user has an md5 password and only supplied a token + } else if (utils.hash.isMD5(user.data.pw) && obj.token) { + // Say token is invalid so we get the password instead of the token next time + callback('InvalidToken'); + } else if (obj.token) { + // Check if the token is correct + utils.token.verify(obj.token, nconf.get('tokenSecret'), (err, decoded) => { + if (err) { + callback('InvalidToken'); + return; + } + const email = decoded.email; + super.loginUser(email, loginCallback(callback)); + }); + } else if (obj.pw && utils.hash.compareBcrypt(obj.pw, user.data.pw)) { + super.loginUser(obj.email, loginCallback(callback)); + } else { + callback('IncorrectPassword'); + } + }); + } + createUser(obj, callback) { + if (obj.pw) { + obj.pw = utils.hash.bcrypt(obj.pw); + super.createUser(obj, loginCallback(callback)); + } else { + callback('InvalidPassword'); + } + } +} + +const db = new DB(); +module.exports = db; diff --git a/socketserver/database_util.js b/socketserver/database_util.js deleted file mode 100644 index 04ad32d..0000000 --- a/socketserver/database_util.js +++ /dev/null @@ -1,17 +0,0 @@ -var Hash = require('./hash'); - -function DBUtils(){} - -DBUtils.prototype.makePass = function(inPass, salt) { - return Hash.md5(('' + inPass) + (salt || '')).toString(); -}; - -DBUtils.prototype.validateEmail = function(email) { - return /^.+@.+\..+$/.test(email); -}; - -DBUtils.prototype.validateUsername = function(un) { - return /^[a-z0-9_-]{3,20}$/i.test(un); -}; - -module.exports = new DBUtils(); \ No newline at end of file diff --git a/socketserver/db_level.js b/socketserver/db_level.js index cb8bc1a..c808e02 100644 --- a/socketserver/db_level.js +++ b/socketserver/db_level.js @@ -1,101 +1,112 @@ -//Modules -var levelup = require('levelup'); -var path = require('path'); -var util = require('util'); -var fs = require('fs'); -var log = new(require('basic-logger'))({ +'use strict'; +// Modules +const levelup = require('levelup'); +const path = require('path'); +const util = require('util'); +const fs = require('fs'); +const log = new(require('basic-logger'))({ showTimestamp: true, - prefix: "LevelDB" + prefix: 'LevelDB' }); +const nconf = require('nconf'); + +// Files +const Mailer = require('./mail/mailer'); +const DBUtils = require('./utils').db; +const User = require('./user'); + +// Variables +let currentPID = 0; +let currentUID = 0; +let currentCID = 0; +const expires = 1000 * 60 * 60 * 24 * nconf.get('loginExpire'); +let usernames = []; + +function setupDB(dir, setup, callback) { + setup = setup || function () {}; + callback = callback || function () {}; + + return levelup(dir, null, (err, newdb) => { + if (err) { + log.error('Could not open db'); + callback(err); + return; + } -//Files -var config = require('../serverconfig.js'); -var Mailer = require('./mailer'); -var DBUtils = require('./database_util'); - -//Variables -var currentPID = 0; -var currentUID = 0; -var currentCID = 0; -var expires = 1000 * 60 * 60 * 24 * config.loginExpire; -var usernames = []; + newdb.get('setup', (err) => { + if (err && err.notFound) { + newdb.put('setup', 1); + setup(newdb); + callback(null, newdb); + } else { + callback(null, newdb); + } + }); + }); +} function LevelDB(callback) { - var dbdir = path.resolve(config.db.dbDir || './socketserver/db'); + const dbdir = path.resolve(nconf.get('db:dbDir') || './socketserver/db'); try { fs.statSync(dbdir); - } catch(e) { + } catch (e) { fs.mkdirSync(dbdir); } - //PlaylistDB - if(!this.PlaylistDB) - this.PlaylistDB = setupDB(dbdir + '/playlists', - - //If new DB is created - function(newdb) { - currentPID = 1; - log.debug('PIDCOUNTER set to 1'); - newdb.put('PIDCOUNTER', 1); - }, - - //Callback - function(err, db) { - if (err) log.error('Could not open PlaylistDB: ' + err); - - if (currentPID != 0) return; - - db.get('PIDCOUNTER', function(err, val) { - if (err) { - throw new Error('Cannot get PIDCOUNTER from UserDB. Might be corrupt'); - } - currentPID = parseInt(val); - }); - }); - - //RoomDB - if(!this.RoomDB) - this.RoomDB = setupDB(dbdir + '/room', - - //If new DB is created - function(newdb) {}, - - //Callback - function(err, db) { - if (err) throw new Error('Could not open RoomDB: ' + err); - if (callback) callback(null, db); - }); - - //TokenDB - if(!this.TokenDB) - this.TokenDB = setupDB(dbdir + '/tokens', - - //If new DB is created - function(newdb) {}, - - //Callback - function(err, db) { - if (err) log.error('Could not open TokenDB: ' + err); - }); + // PlaylistDB + if (!this.PlaylistDB) { + this.PlaylistDB = setupDB(`${dbdir}/playlists`, + // If new DB is created + (newdb) => { + currentPID = 1; + log.debug('PIDCOUNTER set to 1'); + newdb.put('PIDCOUNTER', 1); + }, + + // Callback + (err, db) => { + if (err) log.error(`Could not open PlaylistDB: ${err}`); + if (currentPID !== 0) return; + db.get('PIDCOUNTER', (err, val) => { + if (err) { + throw new Error('Cannot get PIDCOUNTER from UserDB. Might be corrupt'); + } + currentPID = parseInt(val, 10); + }); + }); + } - //UserDB - if(!this.UserDB) - this.UserDB = setupDB(dbdir + '/users', + // RoomDB + if (!this.RoomDB) { + this.RoomDB = setupDB(`${dbdir}/room`, + // If new DB is created + () => {}, + + // Callback + (err, db) => { + if (err) throw new Error(`Could not open RoomDB: ${err}`); + if (callback) callback(null, db); + }); + } + + // UserDB + if (!this.UserDB) + this.UserDB = setupDB(`${dbdir}/users`, - //If new DB is created - function(newdb) { + // If new DB is created + function (newdb) { currentUID = 1; log.debug('UIDCOUNTER set to 1'); newdb.put('UIDCOUNTER', 1); }, - //Callback - function(err, newdb) { + // Callback + function (err, newdb) { if (err) { throw new Error('Could not open UserDB: ' + err); } if (currentUID != 0) return; - newdb.get('UIDCOUNTER', function(err, val) { + newdb.get('UIDCOUNTER', function (err, val) { if (err) { throw new Error('Cannot get UIDCOUNTER from UserDB. Might be corrupt'); } @@ -103,7 +114,7 @@ function LevelDB(callback) { }); newdb.createReadStream() - .on('data', function(data) { + .on('data', function (data) { if (data.key.indexOf('@') == -1) return; try { var user = JSON.parse(data.value); @@ -115,88 +126,65 @@ function LevelDB(callback) { user.lastdj = false; newdb.put(data.key, JSON.stringify(user)); }) - .on('end', function() { + .on('end', function () { return false; }); }); - - //ChatDB - if(!this.ChatDB) + + // ChatDB + if (!this.ChatDB) this.ChatDB = setupDB(dbdir + '/chat', - //If new DB is created - function(newdb) { + // If new DB is created + function (newdb) { currentCID = 1; log.debug('CIDCOUNTER set to 1'); newdb.put('CIDCOUNTER', 1); }, - //Callback - function(err, newdb) { + // Callback + function (err, newdb) { if (err) { throw new Error('Could not open ChatDB: ' + err); } if (currentCID != 0) return; - newdb.get('CIDCOUNTER', function(err, val) { + newdb.get('CIDCOUNTER', function (err, val) { if (err) { throw new Error('Cannot get CIDCOUNTER from PmDB. Might be corrupt'); } currentCID = parseInt(val); }); }); - - //PmDB - if(!this.PmDB) + // PmDB + if (!this.PmDB) this.PmDB = setupDB(dbdir + '/pm', - //If new DB is created - function(newdb) {}, + // If new DB is created + function (newdb) {}, - //Callback - function(err, newdb) { + // Callback + function (err, newdb) { if (err) { throw new Error('Could not open PmDB: ' + err); } }); - - //IpDB - if(!this.IpDB) + + // IpDB + if (!this.IpDB) this.IpDB = setupDB(dbdir + '/ip', - //If new DB is created - function(newdb) {}, + // If new DB is created + function (newdb) {}, - //Callback - function(err, newdb) { + // Callback + function (err, newdb) { if (err) { throw new Error('Could not open IpDB: ' + err); } }); } -function setupDB(dir, setup, callback){ - setup = setup || function(){}; - callback = callback || function(){}; - - return levelup(dir, null, function(err, newdb){ - if (err){ - log.error('Could not open db'); - callback(err); - return; - } - - newdb.get("setup", function( err, val ){ - if (err && err.notFound){ - newdb.put('setup', 1); - setup(newdb); - callback(null, newdb); - }else{ - callback(null, newdb); - } - }); - }); -} /** * getJSON() gives the callback function a parsed JSON object @@ -206,15 +194,15 @@ function setupDB(dir, setup, callback){ * @param {Function} callback * @return {Object} this */ -LevelDB.prototype.getJSON = function(db, key, callback) { - callback = callback || function() {}; +LevelDB.prototype.getJSON = function (db, key, callback) { + callback = callback || function () {}; - db.get(key, function(err, val) { + db.get(key, function (err, val) { if (val) { try { val = JSON.parse(val); } catch (e) { - console.log('Database key "' + key + '" returned malformed JSON object'); + console.log('Database key ' + key + ' returned malformed JSON object'); val = null; } } @@ -231,17 +219,17 @@ LevelDB.prototype.getJSON = function(db, key, callback) { * @param {Function} callback * @return {Object} this */ -LevelDB.prototype.putJSON = function(db, key, val, callback) { - callback = callback || function() {}; +LevelDB.prototype.putJSON = function (db, key, val, callback) { + callback = callback || function () {}; db.put(key, JSON.stringify(val), callback); return this; }; -//PlaylistDB -LevelDB.prototype.getPlaylist = function(pid, callback) { +// PlaylistDB +LevelDB.prototype.getPlaylist = function (pid, callback) { var Playlist = require('./playlist'); - this.getJSON(this.PlaylistDB, pid, function(err, data) { + this.getJSON(this.PlaylistDB, pid, function (err, data) { if (err) { callback('PlaylistNotFound'); return; @@ -257,7 +245,7 @@ LevelDB.prototype.getPlaylist = function(pid, callback) { return this; }; -LevelDB.prototype.createPlaylist = function(owner, name, callback) { +LevelDB.prototype.createPlaylist = function (owner, name, callback) { var Playlist = require('./playlist'); var pl = new Playlist(); @@ -271,60 +259,26 @@ LevelDB.prototype.createPlaylist = function(owner, name, callback) { callback(null, pl); }; -LevelDB.prototype.deletePlaylist = function(pid, callback) { +LevelDB.prototype.deletePlaylist = function (pid, callback) { this.PlaylistDB.del(pid.toString(), callback); }; -LevelDB.prototype.putPlaylist = function(pid, data, callback) { +LevelDB.prototype.putPlaylist = function (pid, data, callback) { this.putJSON(this.PlaylistDB, pid, data, callback); }; -//RoomDB -LevelDB.prototype.getRoom = function(slug, callback) { +// RoomDB +LevelDB.prototype.getRoom = function (slug, callback) { this.getJSON(this.RoomDB, slug, callback); return this; }; -LevelDB.prototype.setRoom = function(slug, val, callback) { +LevelDB.prototype.setRoom = function (slug, val, callback) { this.putJSON(this.RoomDB, slug, val, callback); return this; }; -//TokenDB -LevelDB.prototype.deleteToken = function(tok) { - this.TokenDB.del(tok); -}; - -LevelDB.prototype.createToken = function(email) { - var tok = DBUtils.makePass(email, Date.now()); - - this.putJSON(this.TokenDB, tok, { - email: email, - time: Date.now(), - }); - - return tok; -}; - -LevelDB.prototype.isTokenValid = function(tok, callback) { - var that = this; - - this.getJSON(this.TokenDB, tok, function(err, data) { - if (err || data == null) { - callback('InvalidToken'); - return; - } - - if (config.loginExpire && (Date.now() - data.time) < expires) { - callback(null, data.email); - } else { - that.deleteToken(data.token); - callback('InvalidToken'); - } - }); -}; - -//UserDB +// UserDB function addUsername(un) { usernames.push(un.toLowerCase()); } @@ -341,8 +295,7 @@ function usernameExists(un) { return ((ind = usernames.indexOf(un)) != -1 ? ind : false); } -LevelDB.prototype.createUser = function(obj, callback) { - var User = require('./user'); +LevelDB.prototype.createUser = function (obj, callback) { var that = this; var defaultCreateObj = { @@ -355,7 +308,7 @@ LevelDB.prototype.createUser = function(obj, callback) { var inData = defaultCreateObj; inData.email = inData.email.toLowerCase(); - //Validation + // Validation if (!inData.email || !DBUtils.validateEmail(inData.email)) { callback('InvalidEmail'); return; @@ -368,159 +321,98 @@ LevelDB.prototype.createUser = function(obj, callback) { callback('UsernameExists'); return; } - if (!inData.pw || inData.pw == 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') { - callback('PasswordBlank'); - return; - } - //Check for existing account - this.userEmailExists(inData.email, function(err, res) { + // Check for existing account + this.userEmailExists(inData.email, function (err, res) { if (!err) { if (callback) callback('AccountExists'); return; } var user = new User(); - + user.data.uid = currentUID++; that.UserDB.put('UIDCOUNTER', currentUID); user.data.un = inData.un; - user.data.salt = DBUtils.makePass(Date.now()).slice(0, 10); - user.data.pw = DBUtils.makePass(inData.pw, user.data.salt); + user.data.pw = inData.pw; user.data.created = Date.now(); - if (config.room.email.confirmation) user.data.confirmation = DBUtils.makePass(Date.now()); + if (nconf.get('room:mail:confirmation')) user.data.confirmation = DBUtils.randomBytes(18, 'base64'); var updatedUserObj = user.makeDbObj(); - var tok = that.createToken(inData.email); - - that.putJSON(that.UserDB, inData.email, updatedUserObj, function(err) { + that.putJSON(that.UserDB, inData.email, updatedUserObj, function (err) { if (err) { callback(err); return; } - //Send confirmation email - if (config.room.email.confirmation) { + // Send confirmation email + if (nconf.get('room:mail:confirmation')) { Mailer.sendEmail('signup', { code: user.data.confirmation, user: inData.un, - }, inData.email, function(data) { + }, inData.email, function (data) { console.log(data); }); } - //Do other ~messy~ stuff + // Do other ~messy~ stuff addUsername(inData.un); user.login(inData.email); - callback(null, user, tok); + callback(null, user, inData.email); }); }); }; -LevelDB.prototype.loginUser = function(obj, callback) { - var User = require('./user'); - var that = this; - - var defaultLoginObj = { - email: null, - pw: null, - token: null, - }; - util._extend(defaultLoginObj, obj); - - var inData = defaultLoginObj; - - if (inData.email && inData.pw) { - inData.email = inData.email.toLowerCase(); - - this.getJSON(this.UserDB, inData.email, function(err, data) { - if ((err && err.notFound) || data == null) { - callback('UserNotFound'); - return; - } - - if (err) { - callback(err); - return; - } - - if (DBUtils.makePass(inData.pw, data.salt) != data.pw) { - callback('IncorrectPassword'); - return; - } - - var tok = that.createToken(inData.email); - var user = new User(); - - user.login(inData.email, data, function() { - - callback(null, user, tok); - }); - }); - } else if (inData.token) { - that.isTokenValid(inData.token, function(err, email) { - if (err) { - callback(err); - return; - } - - that.getJSON(that.UserDB, email, function(err, data) { - if ((err && err.notFound) || data == null) { - callback('UserNotFound'); - return; - } - - if (err) { - callback(err); - return; - } - - var user = new User(); - user.login(email, data, function() { - - callback(null, user); - }); - }); - }); - } else { - callback('InvalidArgs'); - } +LevelDB.prototype.loginUser = function (email, callback) { + if (email) { + email = email.toLowerCase(); + this.getJSON(this.UserDB, email, (err, data) => { + if ((err && err.notFound) || data == null) { + callback('UserNotFound'); + return; + } + if (err) { + callback(err); + return; + } + const user = new User(); + user.login(email, data, () => { + callback(null, user, email); + }); + }); + } }; -LevelDB.prototype.putUser = function(email, data, callback) { +LevelDB.prototype.putUser = function (email, data, callback) { this.putJSON(this.UserDB, email, data, callback); }; -LevelDB.prototype.getUser = function(email, callback){ - var User = require('./user'); +LevelDB.prototype.getUser = function (email, callback) { + this.getJSON(this.UserDB, email, function (err, data) { + if ((err && err.notFound) || data == null) { callback('UserNotFound'); return; } - this.getJSON(this.UserDB, email, function(err, data){ - if ((err && err.notFound) || data == null) {callback('UserNotFound'); return; } - - if (err) {callback(err); return; } + if (err) { callback(err); return; } var user = new User(); - - user.login(email, data, function(){ + user.login(email, data, function () { callback(null, user); }); }); }; -LevelDB.prototype.deleteUser = function(email, callback){ +LevelDB.prototype.deleteUser = function (email, callback) { var that = this; - - this.getUser(email, function(err, user){ - if (err){ if (callback) callback(err); return; } - + + this.getUser(email, function (err, user) { + if (err) { if (callback) callback(err); return; } + that.UserDB.del(email); - + callback(null, true); }); }; -LevelDB.prototype.getUserByUid = function(uid, opts, callback) { - var User = require('./user'); +LevelDB.prototype.getUserByUid = function (uid, opts, callback) { var done = false; if (typeof opts === 'function') { @@ -542,7 +434,7 @@ LevelDB.prototype.getUserByUid = function(uid, opts, callback) { var len = 0; var stream = this.UserDB.createReadStream() - .on('data', function(data) { + .on('data', function (data) { var obj = {}; try { @@ -555,7 +447,7 @@ LevelDB.prototype.getUserByUid = function(uid, opts, callback) { if (uid.indexOf(obj.uid) > -1) { var user = new User(); - user.login(data.key, obj, opts, function() { + user.login(data.key, obj, opts, function () { out[obj.uid] = user; len++; @@ -572,13 +464,13 @@ LevelDB.prototype.getUserByUid = function(uid, opts, callback) { stream.destroy(); var user = new User(); - user.login(data.key, obj, opts, function() { + user.login(data.key, obj, opts, function () { callback(null, user); }); } } }) - .on('end', function() { + .on('end', function () { if (!done) { if (typeof uid === 'number') { callback('UserNotFound'); @@ -590,8 +482,7 @@ LevelDB.prototype.getUserByUid = function(uid, opts, callback) { }); }; -LevelDB.prototype.getUserByName = function(name, opts, callback) { - var User = require('./user'); +LevelDB.prototype.getUserByName = function (name, opts, callback) { var done = false; if (typeof opts === 'function') { @@ -600,7 +491,7 @@ LevelDB.prototype.getUserByName = function(name, opts, callback) { } var stream = this.UserDB.createReadStream() - .on('data', function(data) { + .on('data', function (data) { var obj = {}; try { @@ -615,20 +506,19 @@ LevelDB.prototype.getUserByName = function(name, opts, callback) { stream.destroy(); var user = new User(); - user.login(data.key, obj, opts, function() { + user.login(data.key, obj, opts, function () { if (callback) callback(null, user); }); } }) - .on('end', function() { + .on('end', function () { if (!done && callback) callback('UserNotFound'); }); }; -LevelDB.prototype.userEmailExists = function(key, callback) { - this.getJSON(this.UserDB, key, function(err, data) { - +LevelDB.prototype.userEmailExists = function (key, callback) { + this.getJSON(this.UserDB, key, function (err, data) { if (err && err.notFound) { if (callback) callback(err, false); return; @@ -638,38 +528,38 @@ LevelDB.prototype.userEmailExists = function(key, callback) { }); }; -//ChatDB -LevelDB.prototype.logChat = function(uid, msg, special, callback) { - this.putJSON(this.ChatDB, currentCID, { uid: uid, msg: msg, special: special }); +// ChatDB +LevelDB.prototype.logChat = function (uid, msg, special, callback) { + this.putJSON(this.ChatDB, currentCID, { uid, msg, special }); callback(null, currentCID++); }; -//PmDB -LevelDB.prototype.logPM = function(from, to, msg, callback) { +// PmDB +LevelDB.prototype.logPM = function (from, to, msg, callback) { var that = this; - var key = Math.min(from, to) + ":" + Math.max(from, to); - - this.getJSON(this.PmDB, key, function(err, res){ + var key = Math.min(from, to) + ':' + Math.max(from, to); + + this.getJSON(this.PmDB, key, function (err, res) { var out = []; - - if(!err) out = res; - + + if (!err) out = res; + out.push({ message: msg, time: new Date(), - from: from, + from, unread: true, }); - + that.putJSON(that.PmDB, key, out); }); }; -LevelDB.prototype.getConversation = function(from, to, callback) { - var key = Math.min(from, to) + ":" + Math.max(from, to); - - this.getJSON(this.PmDB, key, function(err, res){ - if(err){ +LevelDB.prototype.getConversation = function (from, to, callback) { + var key = Math.min(from, to) + ':' + Math.max(from, to); + + this.getJSON(this.PmDB, key, function (err, res) { + if (err) { callback(null, []); } else { callback(null, res); @@ -677,15 +567,15 @@ LevelDB.prototype.getConversation = function(from, to, callback) { }); }; -LevelDB.prototype.getConversations = function(uid, callback) { +LevelDB.prototype.getConversations = function (uid, callback) { var that = this; - + var out = {}; var uids; uid = uid.toString(); - + this.PmDB.createReadStream() - .on('data', function(data) { + .on('data', function (data) { if (data.key.indexOf(':') == -1 || (uids = data.key.split(':')).indexOf(uid) == -1) return; try { @@ -693,10 +583,10 @@ LevelDB.prototype.getConversations = function(uid, callback) { } catch (e) { return; } - + var unread = 0; - convo.map(function(e){ - if(e.unread && e.from != uid) unread++; + convo.map(function (e) { + if (e.unread && e.from != uid) unread++; return { messages: e.messages, time: e.time, @@ -706,15 +596,15 @@ LevelDB.prototype.getConversations = function(uid, callback) { out[uids[(uids.indexOf(uid) + 1) % 2]] = { user: null, - messages: [ convo.pop() ], - unread: unread, + messages: [convo.pop()], + unread, }; }) - .on('end', function() { - var uids = Object.keys(out).map(function(e){ return parseInt(e); }); - + .on('end', function () { + var uids = Object.keys(out).map(function (e) { return parseInt(e); }); + if (uids.length > 0) { - that.getUserByUid(uids, function(err, result){ + that.getUserByUid(uids, function (err, result) { if (err) { callback(err); } else { @@ -731,45 +621,47 @@ LevelDB.prototype.getConversations = function(uid, callback) { }); }; -LevelDB.prototype.markConversationRead = function(uid, uid2, time) { +LevelDB.prototype.markConversationRead = function (uid, uid2, time) { var that = this; - var key = Math.min(uid, uid2) + ":" + Math.max(uid, uid2); - - this.getJSON(this.PmDB, key, function(err, res) { - if(err) return; - - res.map(function(e){ - if(e.from == uid2 && new Date(e.time) < new Date(time)) e.unread = false; + var key = Math.min(uid, uid2) + ':' + Math.max(uid, uid2); + + this.getJSON(this.PmDB, key, function (err, res) { + if (err) return; + + res.map(function (e) { + if (e.from == uid2 && new Date(e.time) < new Date(time)) e.unread = false; return e; }); - + that.putJSON(that.PmDB, key, res); }); }; -//IpDB -LevelDB.prototype.logIp = function(address, uid) { - var that = this; +// IpDB +LevelDB.prototype.logIp = function (address, uid) { + let that = this; + + this.getJSON(this.IpDB, uid, function (err, res) { + let out = res || []; - this.getJSON(this.IpDB, uid, function(err, res){ - var out = res || []; - out.push({ - address: address, + address, time: new Date(), }); - + that.putJSON(that.IpDB, uid, out); }); }; -LevelDB.prototype.getIpHistory = function(uid, callback) { - this.getJSON(this.IpDB, uid, function(err, data) { - if(err) - callback(err) - else - callback(null, data.sort(function(a, b){ return a.address > b.address; }).reverse().filter(function(e, i, a){ return i == 0 || a[i - 1].address != e.address; }).sort(function(a, b){ return a.time < b.time; })); +LevelDB.prototype.getIpHistory = function (uid, callback) { + this.getJSON(this.IpDB, uid, (err, data) => { + if (err) { + callback(err); + } + else { + callback(null, data.sort(function (a, b) { return a.address > b.address; }).reverse().filter(function (e, i, a) { return i == 0 || a[i - 1].address != e.address; }).sort(function (a, b) { return a.time < b.time; })); + } }); }; -module.exports = new LevelDB(); \ No newline at end of file +module.exports = LevelDB; diff --git a/socketserver/db_mongo.js b/socketserver/db_mongo.js index edac477..011022e 100644 --- a/socketserver/db_mongo.js +++ b/socketserver/db_mongo.js @@ -1,40 +1,41 @@ -//Modules -var mongodb = require('mongodb').MongoClient; -var util = require('util'); -var log = new(require('basic-logger'))({ - showTimestamp: true, - prefix: "MongoDB" +'use strict'; +// Modules +const mongodb = require('mongodb').MongoClient; +const util = require('util'); +const log = new(require('basic-logger'))({ + showTimestamp: true, + prefix: 'MongoDB' }); -//Files -var config = require('../serverconfig.js'); -var Mailer = require('./mailer'); -var DBUtils = require('./database_util'); - -//Variables -var expires = 1000 * 60 * 60 * 24 * config.loginExpire; -var usernames = []; -var db = null; -var poolqueue = []; -var ready = false; - -var playlistscol = null; -var roomcol = null; -var tokenscol = null; -var userscol = null; -var chatcol = null; -var pmscol = null; -var ipcol = null; +// Files +const nconf = require('nconf'); +const Mailer = require('./mail/mailer'); +const DBUtils = require('./utils').db; +const User = require('./user'); + +// Variables +const expires = 1000 * 60 * 60 * 24 * nconf.get('loginExpire'); +let usernames = []; +let db = null; +let poolqueue = []; +let ready = false; + +let playlistscol = null; +let roomcol = null; +let userscol = null; +let chatcol = null; +let pmscol = null; +let ipcol = null; function dbQueue(callback) { if (callback === true) { while (poolqueue.length > 0) (poolqueue.shift())(); - + ready = true; return; } - + if (!ready) { return poolqueue.push(callback); } @@ -44,111 +45,104 @@ function dbQueue(callback) { function createCollectionsIfNoExist(callback) { var step = 0; - var total = 7; - - db.collection('playlists', {strict:true}, function(err, col) { + var total = 6; + + db.collection('playlists', { + strict: true + }, function (err, col) { if (err) { - db.createCollection('playlists', function(errc, result) { - if (errc) - throw new Error('Failed to create the playlists collection'); - - playlistscol = result; - if (++step == total) callback(); - }); + db.createCollection('playlists', function (errc, result) { + if (errc) + throw new Error('Failed to create the playlists collection'); + + playlistscol = result; + if (++step == total) callback(); + }); } else { playlistscol = col; if (++step == total) callback(); } - }); - - db.collection('room', {strict:true}, function(err, col) { + + db.collection('room', { + strict: true + }, function (err, col) { if (err) { - db.createCollection('room', function(errc, result) { - if (errc) - throw new Error('Failed to create the room collection'); - - roomcol = result; - if (++step == total) callback(); - }); + db.createCollection('room', function (errc, result) { + if (errc) + throw new Error('Failed to create the room collection'); + + roomcol = result; + if (++step == total) callback(); + }); } else { roomcol = col; if (++step == total) callback(); } - }); - - db.collection('tokens', {strict:true}, function(err, col) { - if (err) { - db.createCollection('tokens', function(errc, result) { - if (errc) - throw new Error('Failed to create the tokens collection'); - - tokenscol = result; - if (++step == total) callback(); - }); - } else { - tokenscol = col; - if (++step == total) callback(); - } - - }); - - db.collection('users', {strict:true}, function(err, col) { + + db.collection('users', { + strict: true + }, function (err, col) { if (err) { - db.createCollection('users', function(errc, result) { - if (errc) - throw new Error('Failed to create the users collection'); - - userscol = result; - if (++step == total) callback(); - }); + db.createCollection('users', function (errc, result) { + if (errc) + throw new Error('Failed to create the users collection'); + + userscol = result; + if (++step == total) callback(); + }); } else { userscol = col; if (++step == total) callback(); } - }); - - db.collection('chat', {strict:true}, function(err, col) { + + db.collection('chat', { + strict: true + }, function (err, col) { if (err) { - db.createCollection('chat', function(errc, result) { - if (errc) - throw new Error('Failed to create the chat collection'); - - chatcol = result; - if (++step == total) callback(); - }); + db.createCollection('chat', function (errc, result) { + if (errc) + throw new Error('Failed to create the chat collection'); + + chatcol = result; + if (++step == total) callback(); + }); } else { chatcol = col; if (++step == total) callback(); } }); - - db.collection('pms', {strict:true}, function(err, col) { + + db.collection('pms', { + strict: true + }, function (err, col) { if (err) { - db.createCollection('pms', function(errc, result) { - if (errc) - throw new Error('Failed to create the pms collection'); - - pmscol = result; - if (++step == total) callback(); - }); + db.createCollection('pms', function (errc, result) { + if (errc) + throw new Error('Failed to create the pms collection'); + + pmscol = result; + if (++step == total) callback(); + }); } else { pmscol = col; if (++step == total) callback(); } }); - - db.collection('ip', {strict:true}, function(err, col) { + + db.collection('ip', { + strict: true + }, function (err, col) { if (err) { - db.createCollection('ip', function(errc, result) { - if (errc) - throw new Error('Failed to create the ip collection'); - - ipcol = result; - if (++step == total) callback(); - }); + db.createCollection('ip', function (errc, result) { + if (errc) + throw new Error('Failed to create the ip collection'); + + ipcol = result; + if (++step == total) callback(); + }); } else { ipcol = col; if (++step == total) callback(); @@ -159,232 +153,237 @@ function createCollectionsIfNoExist(callback) { function initCollections(callback) { var step = 0; var total = 4; - - //Playlists - playlistscol.findOne({_id: 'PIDCOUNTER'}, function(err, pidobj) { - if (err) { - throw new Error('Cannot get PIDCOUNTER from playlists'); - } - - if (!pidobj) { - playlistscol.insert({_id: "PIDCOUNTER", seq: 1}, function(error, data) { - if (error) { - throw new Error('Cannot set PIDCOUNTER to playlists'); - } - if (++step == total) callback(); - }); - } else { - if (++step == total) callback(); - } - }); - - //Users - userscol.findOne({_id: 'UIDCOUNTER'}, function(err, pidobj) { - if (err) { - throw new Error('Cannot get UIDCOUNTER from users'); - } - - if (!pidobj) { - userscol.insert({_id: "UIDCOUNTER", seq: 1}, function(error, data) { - if (error) { - throw new Error('Cannot set UIDCOUNTER to users'); - } - if (++step == total) callback(); - }); - } else { - if (++step == total) callback(); - } - }); - - //Chat - chatcol.findOne({_id: 'CIDCOUNTER'}, function(err, pidobj) { - if (err) { - throw new Error('Cannot get CIDCOUNTER from chat'); - } - if (!pidobj) { - chatcol.insert({_id: "CIDCOUNTER", seq: 1}, function(error, data) { - if (error) { - throw new Error('Cannot set CIDCOUNTER to chat'); - } - if (++step == total) callback(); - }); - } else { - if (++step == total) callback(); - } - }); - - //PMs - pmscol.findOne({_id: 'PMIDCOUNTER'}, function(err, pidobj) { - if (err) { - throw new Error('Cannot get PMIDCOUNTER from pms'); - } - if (!pidobj) { - pmscol.insert({_id: "PMIDCOUNTER", seq: 1}, function(error, data) { - if (error) { - throw new Error('Cannot set PMIDCOUNTER to pms'); - } - if (++step == total) callback(); - }); - } else { - if (++step == total) callback(); - } - }); + + // Playlists + playlistscol.findOne({ + _id: 'PIDCOUNTER' + }, function (err, pidobj) { + if (err) { + throw new Error('Cannot get PIDCOUNTER from playlists'); + } + + if (!pidobj) { + playlistscol.insert({ + _id: 'PIDCOUNTER', + seq: 1 + }, function (error, data) { + if (error) { + throw new Error('Cannot set PIDCOUNTER to playlists'); + } + if (++step == total) callback(); + }); + } else { + if (++step == total) callback(); + } + }); + + // Users + userscol.findOne({ + _id: 'UIDCOUNTER' + }, function (err, pidobj) { + if (err) { + throw new Error('Cannot get UIDCOUNTER from users'); + } + + if (!pidobj) { + userscol.insert({ + _id: 'UIDCOUNTER', + seq: 1 + }, function (error, data) { + if (error) { + throw new Error('Cannot set UIDCOUNTER to users'); + } + if (++step == total) callback(); + }); + } else { + if (++step == total) callback(); + } + }); + + // Chat + chatcol.findOne({ + _id: 'CIDCOUNTER' + }, function (err, pidobj) { + if (err) { + throw new Error('Cannot get CIDCOUNTER from chat'); + } + if (!pidobj) { + chatcol.insert({ + _id: 'CIDCOUNTER', + seq: 1 + }, function (error, data) { + if (error) { + throw new Error('Cannot set CIDCOUNTER to chat'); + } + if (++step == total) callback(); + }); + } else { + if (++step == total) callback(); + } + }); + + // PMs + pmscol.findOne({ + _id: 'PMIDCOUNTER' + }, function (err, pidobj) { + if (err) { + throw new Error('Cannot get PMIDCOUNTER from pms'); + } + if (!pidobj) { + pmscol.insert({ + _id: 'PMIDCOUNTER', + seq: 1 + }, function (error, data) { + if (error) { + throw new Error('Cannot set PMIDCOUNTER to pms'); + } + if (++step == total) callback(); + }); + } else { + if (++step == total) callback(); + } + }); } function MongoDB(cb) { - var dburl = 'mongodb://' + config.db.mongoUser + ':' + config.db.mongoPassword + '@' + config.db.mongoHost + ':27017/' + config.db.mongoDatabase; - - mongodb.connect(dburl, function(err, database) { - if (err) { - throw new Error('Could not connect to database: ' + err); - } - - db = database; - - createCollectionsIfNoExist(function() { - initCollections(function() { - dbQueue(true); - }); - }); - }); -} + const dburl = `mongodb://${nconf.get('db:mongoUser')}:${nconf.get('db:mongoPassword')}@${nconf.get('db:mongoHost')}:27017/${nconf.get('db:mongoDatabase')}`; -function getNextSequence(collection, id, callback) { - dbQueue(function(){ - db.collection(collection).findOneAndUpdate({_id: id}, { $inc: { seq: 1 } }, function(err, r) { - if (err) throw new Error('Cannot update index counter'); - callback(r.value.seq); + mongodb.connect(dburl, function (err, database) { + if (err) { + throw new Error(`Could not connect to database: ${err}`); + } + + db = database; + + createCollectionsIfNoExist(() => { + initCollections(() => { + dbQueue(true); + }); }); }); } -//PlaylistDB -MongoDB.prototype.getPlaylist = function(pid, callback) { +function getNextSequence(collection, id, callback) { + dbQueue(() => { + db.collection(collection).findOneAndUpdate({ + _id: id + }, { + $inc: { + seq: 1 + } + }, (err, r) => { + if (err) throw new Error('Cannot update index counter'); + callback(r.value.seq); + }); + }); +} + +// PlaylistDB +MongoDB.prototype.getPlaylist = function (pid, callback) { var Playlist = require('./playlist'); - - dbQueue(function(){ - playlistscol.findOne({_id: pid}, {_id: 0}, function(err, data) { + + dbQueue(function () { + playlistscol.findOne({ + _id: pid + }, { + _id: 0 + }, function (err, data) { if (err || !data) { - callback('PlaylistNotFound'); - return; + callback('PlaylistNotFound'); + return; } - + var pl = new Playlist(); pl.id = pid; util._extend(pl.data, data); - + callback(err, pl); - }); + }); }); - + return this; }; -MongoDB.prototype.createPlaylist = function(owner, name, callback) { +MongoDB.prototype.createPlaylist = function (owner, name, callback) { var Playlist = require('./playlist'); - dbQueue(function(){ - getNextSequence('playlists', 'PIDCOUNTER', function(currentPID) { - var pl = new Playlist(); - - pl.id = currentPID; - pl.data.created = Date.now(); - pl.data.owner = owner; - pl.data.name = name.substr(0, 100); - - var updatedPlObj = pl.makeDbObj(); - updatedPlObj._id = currentPID; - - playlistscol.insert(updatedPlObj, function(error, data) { - callback(error, pl); - }); + dbQueue(function () { + getNextSequence('playlists', 'PIDCOUNTER', function (currentPID) { + var pl = new Playlist(); + + pl.id = currentPID; + pl.data.created = Date.now(); + pl.data.owner = owner; + pl.data.name = name.substr(0, 100); + + var updatedPlObj = pl.makeDbObj(); + updatedPlObj._id = currentPID; + + playlistscol.insert(updatedPlObj, function (error, data) { + callback(error, pl); + }); }); }); }; -MongoDB.prototype.deletePlaylist = function(pid, callback) { - dbQueue(function(){ - playlistscol.remove({_id: pid}, callback); - }); +MongoDB.prototype.deletePlaylist = function (pid, callback) { + dbQueue(function () { + playlistscol.remove({ + _id: pid + }, callback); + }); }; -MongoDB.prototype.putPlaylist = function(pid, data, callback) { +MongoDB.prototype.putPlaylist = function (pid, data, callback) { var newData = {}; util._extend(newData, data); - + newData._id = pid; - - dbQueue(function(){ - playlistscol.updateOne({_id: pid}, newData, {upsert:true, w: 1}, function(error, res) { - callback(data); + + dbQueue(function () { + playlistscol.updateOne({ + _id: pid + }, newData, { + upsert: true, + w: 1 + }, function (error, res) { + callback(data); }); }); }; -//RoomDB -MongoDB.prototype.getRoom = function(slug, callback) { - dbQueue(function(){ - roomcol.findOne({slug: slug}, {_id: 0}, callback); +// RoomDB +MongoDB.prototype.getRoom = function (slug, callback) { + dbQueue(function () { + roomcol.findOne({ + slug + }, { + _id: 0 + }, callback); }); return this; }; -MongoDB.prototype.setRoom = function(slug, val, callback) { - dbQueue(function(){ +MongoDB.prototype.setRoom = function (slug, val, callback) { + dbQueue(function () { var newData = {}; util._extend(newData, val); - + newData.slug = slug; - roomcol.updateOne({slug: slug}, newData, {upsert:true, w: 1}, function(error, data) { - if (callback) callback(error, data); + roomcol.updateOne({ + slug + }, newData, { + upsert: true, + w: 1 + }, function (error, data) { + if (callback) callback(error, data); }); }); return this; }; -//TokenDB -MongoDB.prototype.deleteToken = function(tok) { - dbQueue(function(){ - tokenscol.remove({tok: tok}, function(){}); - }); -}; - -MongoDB.prototype.createToken = function(email) { - var tok = DBUtils.makePass(email, Date.now()); - - dbQueue(function(){ - tokenscol.insert({ - tok: tok, - email: email, - time: Date.now(), - }, function() {}); - }); - - return tok; -}; - -MongoDB.prototype.isTokenValid = function(tok, callback) { - var that = this; - - dbQueue(function(){ - tokenscol.findOne({tok: tok}, function(err, data) { - if (err || data == null) { - callback('InvalidToken'); - return; - } - - if (config.loginExpire && (Date.now() - data.time) < expires) { - callback(null, data.email); - } else { - that.deleteToken(data.token); - callback('InvalidToken'); - } - }); - }); -}; - -//UserDB +// UserDB function addUsername(un) { usernames.push(un.toLowerCase()); } @@ -396,8 +395,7 @@ function usernameExists(un) { return ((ind = usernames.indexOf(un)) != -1 ? ind : false); } -MongoDB.prototype.createUser = function(obj, callback) { - var User = require('./user'); +MongoDB.prototype.createUser = function (obj, callback) { var that = this; var defaultCreateObj = { @@ -410,7 +408,7 @@ MongoDB.prototype.createUser = function(obj, callback) { var inData = defaultCreateObj; inData.email = inData.email.toLowerCase(); - //Validation + // Validation if (!inData.email || !DBUtils.validateEmail(inData.email)) { callback('InvalidEmail'); return; @@ -423,186 +421,140 @@ MongoDB.prototype.createUser = function(obj, callback) { callback('UsernameExists'); return; } - if (!inData.pw || inData.pw == 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') { - callback('PasswordBlank'); - return; - } - dbQueue(function(){ - //Check for existing account - that.userEmailExists(inData.email, function(err, res) { + dbQueue(function () { + // Check for existing account + that.userEmailExists(inData.email, function (err, res) { if (err) { if (callback) callback('AccountExists'); return; } - - getNextSequence('users', 'UIDCOUNTER', function(currentUID) { + + getNextSequence('users', 'UIDCOUNTER', function (currentUID) { var user = new User(); - + user.data.uid = currentUID; user.data.un = inData.un; - user.data.salt = DBUtils.makePass(Date.now()).slice(0, 10); - user.data.pw = DBUtils.makePass(inData.pw, user.data.salt); + user.data.pw = inData.pw; user.data.created = Date.now(); - if (config.room.email.confirmation) user.data.confirmation = DBUtils.makePass(Date.now()); + if (nconf.get('room:mail:confirmation')) user.data.confirmation = DBUtils.randomBytes(18, 'base64'); + var updatedUserObj = user.makeDbObj(); updatedUserObj._id = currentUID; updatedUserObj.email = inData.email; - - var tok = that.createToken(inData.email); - - userscol.insert(updatedUserObj, function(error, data) { - if (error) { + + userscol.insert(updatedUserObj, function (error, data) { + if (error) { callback(error); return; } - - //Send confirmation email - if (config.room.email.confirmation) { + + // Send confirmation email + if (nconf.get('room:mail:confirmation')) { Mailer.sendEmail('signup', { code: user.data.confirmation, user: inData.un, - }, inData.email, function(data) { + }, inData.email, function (data) { console.log(data); }); } - - //Do other ~messy~ stuff + + // Do other ~messy~ stuff addUsername(inData.un); user.login(inData.email); - callback(null, user, tok); - }); + callback(null, user, inData.email); + }); }); - }); }); }; -MongoDB.prototype.loginUser = function(obj, callback) { - var User = require('./user'); - var that = this; - - var defaultLoginObj = { - email: null, - pw: null, - token: null, - }; - util._extend(defaultLoginObj, obj); - - var inData = defaultLoginObj; - - dbQueue(function(){ - if (inData.email && inData.pw) { - inData.email = inData.email.toLowerCase(); - - userscol.findOne({email: inData.email}, {_id: 0}, function(err, data) { - if (err) { - callback(err); - return; - } - - if (!data) { - callback('UserNotFound'); - return; - } - - if (DBUtils.makePass(inData.pw, data.salt) != data.pw) { - callback('IncorrectPassword'); - return; - } - - var tok = that.createToken(inData.email); - var user = new User(); - - user.login(inData.email, data, function() { - callback(null, user, tok); - }); - }); - } else if (inData.token) { - that.isTokenValid(inData.token, function(err, email) { - if (err) { - callback(err); - return; - } - - userscol.findOne({email: email}, {_id: 0}, function(err, data) { - if (err) { - callback(err); - return; - } - - if (!data) { - callback('UserNotFound'); - return; - } - - var user = new User(); - user.login(email, data, function() { - - callback(null, user); - }); - }); - }); - } else { - callback('InvalidArgs'); - } - }); +MongoDB.prototype.loginUser = function (email, callback) { + if (email) { + email = email.toLowerCase(); + dbQueue(() => { + userscol.findOne({ email }, { _id: 0 }, (err, data) => { + if (err) { + callback(err); + return; + } + if (!data) { + callback('UserNotFound'); + return; + } + + const user = new User(); + user.login(email, data, () => { + callback(null, user, email); + }); + }); + }); + } }; -MongoDB.prototype.putUser = function(email, data, callback) { +MongoDB.prototype.putUser = function (email, data, callback) { var newData = {}; util._extend(newData, data); - + newData._id = data.uid; newData.email = email; - - dbQueue(function(){ - userscol.updateOne({email: email}, newData, {upsert: true, w: 1}, callback); + + dbQueue(function () { + userscol.updateOne({ + email + }, newData, { + upsert: true, + w: 1 + }, callback); }); }; -MongoDB.prototype.getUser = function(email, callback){ - var User = require('./user'); - - dbQueue(function(){ - userscol.findOne({email: email}, {_id: 0}, function(err, data) { - if (err) { +MongoDB.prototype.getUser = function (email, callback) { + dbQueue(function () { + userscol.findOne({ + email + }, { + _id: 0 + }, function (err, data) { + if (err) { callback(err); return; } - + if (!data) { callback('UserNotFound'); return; } - - var user = new User(); - - user.login(email, data, function(){ - - callback(null, user); - }); - }); + + var user = new User(); + + user.login(email, data, function () { + callback(null, user); + }); + }); }); }; -MongoDB.prototype.deleteUser = function(email, callback) { +MongoDB.prototype.deleteUser = function (email, callback) { var that = this; - - dbQueue(function(){ - that.getUser(email, function(err, user){ - if (err){ if (callback) callback(err); return; } - - userscol.remove({email: email}, function(error, data){ - callback(error || null, error ? false : true); - }); - }); + + dbQueue(function () { + that.getUser(email, function (err, user) { + if (err) { + if (callback) callback(err); + return; + } + + userscol.remove({ + email + }, function (error, data) { + callback(error || null, error ? false : true); + }); + }); }); }; -MongoDB.prototype.getUserByUid = function(uid, opts, callback) { - var User = require('./user'); - +MongoDB.prototype.getUserByUid = function (uid, opts, callback) { if (typeof opts === 'function') { callback = opts; opts = {}; @@ -618,32 +570,38 @@ MongoDB.prototype.getUserByUid = function(uid, opts, callback) { } var isArray = Array.isArray(uid); - + if (!Array.isArray(uid)) uid = [uid]; - + var out = {}; var len = 0; - dbQueue(function(){ - userscol.find({_id: { $in: uid}}, {_id: 0}).toArray(function(err, data) { - if(err || !data || data.length == 0){ + dbQueue(function () { + userscol.find({ + _id: { + $in: uid + } + }, { + _id: 0 + }).toArray(function (err, data) { + if (err || !data || data.length == 0) { callback('SomeUsersNotFound', out); return; } - - data.forEach(function(userobj) { + + data.forEach(function (userobj) { var user = new User(); - - user.login(userobj.email, userobj, opts, function(){ + + user.login(userobj.email, userobj, opts, function () { if (isArray) out[userobj.uid] = user; else out = user; - - console.log("Initialized user " + user.email); - if(++len == data.length){ - if(uid.length == data.length) callback(null, out); + + console.log('Initialized user ' + user.email); + if (++len == data.length) { + if (uid.length == data.length) callback(null, out); else callback('SomeUsersNotFound', out); } }); @@ -652,70 +610,102 @@ MongoDB.prototype.getUserByUid = function(uid, opts, callback) { }); }; -MongoDB.prototype.getUserByName = function(name, opts, callback) { - var User = require('./user'); - +MongoDB.prototype.getUserByName = function (name, opts, callback) { if (typeof opts === 'function') { callback = opts; opts = {}; } - - dbQueue(function(){ - userscol.findOne({un: name}, {_id: 0}, function(err, userobj) { - if(err || !userobj){ + + dbQueue(function () { + userscol.findOne({ + un: name + }, { + _id: 0 + }, function (err, userobj) { + if (err || !userobj) { if (callback) callback('UserNotFound'); return; } - + var user = new User(); - - user.login(userobj.email, userobj, opts, function() { + + user.login(userobj.email, userobj, opts, function () { if (callback) callback(null, user); }); }); }); }; -MongoDB.prototype.userEmailExists = function(key, callback) { - dbQueue(function(){ - userscol.findOne({email: key}, {_id: 0}, function(err, data) { +MongoDB.prototype.userEmailExists = function (key, callback) { + dbQueue(function () { + userscol.findOne({ + email: key + }, { + _id: 0 + }, function (err, data) { if (callback) callback(err, data ? true : false); }); }); }; -//ChatDB -MongoDB.prototype.logChat = function(uid, msg, special, callback) { - dbQueue(function(){ - getNextSequence('chat', 'CIDCOUNTER', function(currentCID) { - chatcol.insert({_id: currentCID, uid: uid, msg: msg, special: special}, function(error, data) { - if (callback) callback(error, currentCID); - }); +// ChatDB +MongoDB.prototype.logChat = function (uid, msg, special, callback) { + dbQueue(function () { + getNextSequence('chat', 'CIDCOUNTER', function (currentCID) { + chatcol.insert({ + _id: currentCID, + uid, + msg, + special + }, function (error, data) { + if (callback) callback(error, currentCID); + }); }); }); }; -//PmDB -MongoDB.prototype.logPM = function(from, to, msg, callback) { - dbQueue(function(){ - getNextSequence('pms', 'PMIDCOUNTER', function(currentCID) { - pmscol.insert({_id: currentCID, msg: msg, from: from, to: to, time: new Date(), unread: true }, function(error, data) { - if (error) log.error("Error logging chat message"); - if (callback) callback(error, currentCID); - }); +// PmDB +MongoDB.prototype.logPM = function (from, to, msg, callback) { + dbQueue(function () { + getNextSequence('pms', 'PMIDCOUNTER', function (currentCID) { + pmscol.insert({ + _id: currentCID, + msg, + from, + to, + time: new Date(), + unread: true + }, function (error, data) { + if (error) log.error('Error logging chat message'); + if (callback) callback(error, currentCID); + }); }); }); }; -MongoDB.prototype.getConversation = function(from, to, callback) { - dbQueue(function(){ - pmscol.find({ $or: [ {from: from, to: to}, {from: to, to: from}] }, {_id: 0}).toArray(function(err, data) { - if(err){ +MongoDB.prototype.getConversation = function (from, to, callback) { + dbQueue(function () { + pmscol.find({ + $or: [{ + from, + to + }, { + from: to, + to: from + }] + }, { + _id: 0 + }).toArray(function (err, data) { + if (err) { callback(err); } else { var out = []; - for(var key in data){ - out.push({message:data[key].msg,time:data[key].time,from:data[key].from}); + for (var key in data) { + out.push({ + message: data[key].msg, + time: data[key].time, + from: data[key].from + }); } callback(null, out); } @@ -723,19 +713,27 @@ MongoDB.prototype.getConversation = function(from, to, callback) { }); }; -MongoDB.prototype.getConversations = function(uid, callback) { +MongoDB.prototype.getConversations = function (uid, callback) { var that = this; - - dbQueue(function(){ - pmscol.find({ $or: [ {from: uid}, {to: uid}] }, {_id: 0}).toArray(function(err, data) { - if(err){ + + dbQueue(function () { + pmscol.find({ + $or: [{ + from: uid + }, { + to: uid + }] + }, { + _id: 0 + }).toArray(function (err, data) { + if (err) { callback(err); } else { var out = {}; var uids = []; for (var key in data) { var otherUid = data[key].to == uid ? data[key].from : data[key].to; - + if (out[otherUid] === undefined) { uids.push(otherUid); out[otherUid] = { @@ -744,14 +742,18 @@ MongoDB.prototype.getConversations = function(uid, callback) { unread: 0 }; } - out[otherUid].messages.push({ message: data[key].msg, time: data[key].time, from: data[key].from }); - + out[otherUid].messages.push({ + message: data[key].msg, + time: data[key].time, + from: data[key].from + }); + if (data[key].unread && data[key].from != uid) out[otherUid].unread++; } - + if (uids.length > 0) { - that.getUserByUid(uids, function(err, result){ + that.getUserByUid(uids, function (err, result) { if (err) { callback(err); } else { @@ -763,8 +765,7 @@ MongoDB.prototype.getConversations = function(uid, callback) { callback(null, out); } }); - } - else { + } else { callback(null, out); } } @@ -772,32 +773,53 @@ MongoDB.prototype.getConversations = function(uid, callback) { }); }; -MongoDB.prototype.markConversationRead = function(uid, uid2, time) { - dbQueue(function(){ - pmscol.updateMany({to: uid, from: uid2, time: {$lt: new Date(time)}}, {$set: {unread: false}}, function(){}); +MongoDB.prototype.markConversationRead = function (uid, uid2, time) { + dbQueue(function () { + pmscol.updateMany({ + to: uid, + from: uid2, + time: { + $lt: new Date(time) + } + }, { + $set: { + unread: false + } + }, function () {}); }); }; -//IpDB -MongoDB.prototype.logIp = function(address, uid) { - dbQueue(function(){ +// IpDB +MongoDB.prototype.logIp = function (address, uid) { + dbQueue(function () { ipcol.insert({ - uid: uid, - address: address, + uid, + address, time: new Date() }); }); }; -MongoDB.prototype.getIpHistory = function(uid, callback) { - dbQueue(function(){ - ipcol.find({uid: uid}, {_id: 0, uid: 0}).toArray(function(err, data) { - if(err) +MongoDB.prototype.getIpHistory = function (uid, callback) { + dbQueue(function () { + ipcol.find({ + uid + }, { + _id: 0, + uid: 0 + }).toArray(function (err, data) { + if (err) callback(err); else - callback(null, data.sort(function(a, b){ return a.address > b.address; }).reverse().filter(function(e, i, a){ return i == 0 || a[i - 1].address != e.address; }).sort(function(a, b){ return a.time < b.time; })); + callback(null, data.sort(function (a, b) { + return a.address > b.address; + }).reverse().filter(function (e, i, a) { + return i == 0 || a[i - 1].address != e.address; + }).sort(function (a, b) { + return a.time < b.time; + })); }); }); }; -module.exports = new MongoDB(); +module.exports = MongoDB; \ No newline at end of file diff --git a/socketserver/db_mysql.js b/socketserver/db_mysql.js index 244d095..bc43f7c 100644 --- a/socketserver/db_mysql.js +++ b/socketserver/db_mysql.js @@ -1,30 +1,31 @@ +'use strict'; //Modules -var mysql = require('mysql'); -var util = require('util'); -var _ = require('underscore'); -var log = new(require('basic-logger'))({ +const mysql = require('mysql'); +const util = require('util'); +const log = new(require('basic-logger'))({ showTimestamp: true, prefix: "MysqlDB" }); +const nconf = require('nconf'); //Files -var config = require('../serverconfig.js'); -var Hash = require('./hash'); -var Mailer = require('./mailer'); -var DBUtils = require('./database_util'); -var Roles = require('./role.js'); - -var db = null; -var pool = null; - -var MysqlDB = function(){ - var that = this; - - var mysqlConfig = { - host: config.db.mysqlHost, - user: config.db.mysqlUser, - password: config.db.mysqlPassword, - database: config.db.mysqlDatabase, +const Mailer = require('./mail/mailer'); +const DBUtils = require('./utils').db; +const Roles = require('./role.js'); +const User = require('./user'); +const _ = require('lodash'); + +let db = null; +let pool = null; + +const MysqlDB = function(){ + const that = this; + + const mysqlConfig = { + host: nconf.get('db:mysqlHost'), + user: nconf.get('db:mysqlUser'), + password: nconf.get('db:mysqlPassword'), + database: nconf.get('db:mysqlDatabase'), charset: "UTF8_GENERAL_CI", multipleStatements: true, connectionLimit: 1, @@ -43,7 +44,7 @@ var MysqlDB = function(){ `id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,\ `email` VARCHAR(254) UNIQUE NOT NULL DEFAULT 'NULL',\ `un` VARCHAR(20) UNIQUE NOT NULL DEFAULT 'NULL',\ - `pw` VARCHAR(32) NOT NULL DEFAULT 'NULL',\ + `pw` VARCHAR(60) NOT NULL DEFAULT 'NULL',\ `salt` VARCHAR(10),\ `activepl` INTEGER UNSIGNED NULL DEFAULT NULL,\ `created` DATETIME NULL,\ @@ -56,6 +57,7 @@ var MysqlDB = function(){ `lastdj` TINYINT(1) NOT NULL DEFAULT 0,\ PRIMARY KEY (`id`)\ );\ + ALTER TABLE `users` MODIFY `pw` VARCHAR(60);\ \ CREATE TABLE IF NOT EXISTS `playlists` (\ `id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,\ @@ -429,36 +431,9 @@ MysqlDB.prototype.setRoom = function(slug, val, callback) { return that; }; -//TokenDB -MysqlDB.prototype.deleteToken = function(tok) { - this.execute("DELETE FROM `tokens` WHERE ?;", { token: tok, }); -}; - -MysqlDB.prototype.createToken = function(email) { - var tok = DBUtils.makePass(email, Date.now()); - - this.execute("DELETE FROM `tokens` WHERE ?; INSERT INTO `tokens` SET ?;", [ { email: email, }, { token: tok, email: email, created: new Date() } ]); - - return tok; -}; - -MysqlDB.prototype.isTokenValid = function(tok, callback) { - this.execute("SELECT `token`, `email` FROM `tokens` WHERE ? AND DATEDIFF(NOW(), `created`) < ?;", [{ token: tok, }, config.loginExpire || 365], function(err, res) { - if (err || res.length == 0) { - callback('InvalidToken'); - return; - } - - callback(null, res[0].email); - }); -}; - //UserDB MysqlDB.prototype.getUserNoLogin = function(uid, callback){ var that = this; - - var User = require('./user'); - this.execute("SELECT `salt`, `lastdj`, `uptime`, `recovery`, UNIX_TIMESTAMP(`recovery_timeout`) as `recovery_timeout`, `confirmation`, `badge_top`, `badge_bottom`, `created`, `activepl`, `pw`, `un`, `id` FROM `users` WHERE ?", { id: uid, }, function(err, res){ if (err || res.length == 0) { callback('UserNotFound'); return; } @@ -506,7 +481,6 @@ MysqlDB.prototype.usernameExists = function(name, callback){ }; MysqlDB.prototype.createUser = function(obj, callback) { - var User = require('./user'); var that = this; var defaultCreateObj = { @@ -547,14 +521,11 @@ MysqlDB.prototype.createUser = function(obj, callback) { var user = new User(); user.data.un = inData.un; - user.data.salt = DBUtils.makePass(Date.now()).slice(0, 10); - user.data.pw = DBUtils.makePass(inData.pw, user.data.salt); + user.data.pw = inData.pw; user.data.created = Date.now(); - if (config.room.email.confirmation) user.data.confirmation = DBUtils.makePass(Date.now()); + if (nconf.get('room:mail:confirmation')) user.data.confirmation = DBUtils.randomBytes(18, 'base64'); var updatedUserObj = user.makeDbObj(); - var tok = that.createToken(inData.email); - delete updatedUserObj.uid; that.putUser(inData.email, updatedUserObj, function(err, id) { @@ -564,7 +535,7 @@ MysqlDB.prototype.createUser = function(obj, callback) { } //Send confirmation email - if (config.room.email.confirmation) { + if (nconf.get('room:mail:confirmation')) { Mailer.sendEmail('signup', { code: user.data.confirmation, user: inData.un, @@ -576,71 +547,30 @@ MysqlDB.prototype.createUser = function(obj, callback) { //Login user user.data.uid = id; user.login(inData.email); - callback(null, user, tok); + callback(null, user, inData.email); }); }); } }); }; -MysqlDB.prototype.loginUser = function(obj, callback) { - var User = require('./user'); - var that = this; - - var defaultLoginObj = { - email: null, - pw: null, - token: null, - }; - util._extend(defaultLoginObj, obj); - var inData = defaultLoginObj; - - if (inData.email && inData.pw) { - inData.email = inData.email.toLowerCase(); - that.execute("SELECT `id` FROM `users` WHERE ?;", { email: inData.email, }, function(err, res) { - if(err || res.length == 0) callback("UserNotFound"); - else { - that.getUserNoLogin(res[0].id, function(err, data) { - if (DBUtils.makePass(inData.pw, data.salt) != data.pw) { - callback('IncorrectPassword'); - return; - } - - var tok = that.createToken(inData.email); - - var user = new User(); - - user.login(inData.email, data, function() { - - callback(null, user, tok); - }); - }); - } - }); - } else if (inData.token) { - that.isTokenValid(inData.token, function(err, email) { - if (err) { - callback(err); - return; - } - - that.execute("SELECT `id` FROM `users` WHERE ?;", { email: email, }, function(err, res) { - if(err || res.length == 0) callback("UserNotFound"); - else { - that.getUserNoLogin(res[0].id, function(err, data) { - var user = new User(); - - user.login(email, data, function() { - - callback(null, user); - }); - }); - } - }); - }); - } else { - callback('InvalidArgs'); - } +MysqlDB.prototype.loginUser = function (email, callback) { + const that = this; + if (email) { + email = email.toLowerCase(); + that.execute('SELECT `id` FROM `users` WHERE ?;', { email }, (err, res) => { + if (err || res.length === 0) { + callback('UserNotFound'); + } else { + that.getUserNoLogin(res[0].id, (err, data) => { + const user = new User(); + user.login(email, data, () => { + callback(null, user, email); + }); + }); + } + }); + } }; MysqlDB.prototype.putUser = function(email, data, callback) { @@ -679,9 +609,6 @@ MysqlDB.prototype.putUser = function(email, data, callback) { MysqlDB.prototype.getUser = function(email, callback){ var that = this; - - var User = require('./user'); - this.execute("SELECT `id` FROM `users` WHERE ?;", { email: email, }, function(err, res) { if(err || res.length == 0){ callback('UserNotFound'); @@ -722,9 +649,6 @@ MysqlDB.prototype.getUserByUid = function(uid, opts, callback) { return; } } - - var User = require('./user'); - if(Array.isArray(uid)){ var out = {}; var initialized = 0; @@ -764,9 +688,6 @@ MysqlDB.prototype.getUserByUid = function(uid, opts, callback) { MysqlDB.prototype.getUserByName = function(name, opts, callback) { var that = this; - - var User = require('./user'); - if (typeof opts === 'function') { callback = opts; opts = {}; @@ -889,4 +810,4 @@ MysqlDB.prototype.getIpHistory = function(uid, callback) { }); }; -module.exports = new MysqlDB; +module.exports = MysqlDB; diff --git a/socketserver/djqueue.js b/socketserver/djqueue.js index 97182f2..3c18973 100644 --- a/socketserver/djqueue.js +++ b/socketserver/djqueue.js @@ -1,5 +1,5 @@ var Roles = require('./role'); -var config = require('../serverconfig'); +const nconf = require('nconf'); var defaultVoteObj = function(){ return { @@ -30,9 +30,9 @@ function djqueue(room){ this.currentsong = null; this.songstart = null; this.lasttimer = null; - this.limit = config.room.queue.limit; - this.cycle = config.room.queue.cycle; - this.lock = config.room.queue.lock; + this.limit = nconf.get('room:queue:limit'); + this.cycle = nconf.get('room:queue:cycle'); + this.lock = nconf.get('room:queue:lock'); this.votes = new defaultVoteObj; } diff --git a/socketserver/hash.js b/socketserver/hash.js deleted file mode 100644 index ac2489b..0000000 --- a/socketserver/hash.js +++ /dev/null @@ -1,9 +0,0 @@ -function md5(str){ - var crypto = require('crypto'); - - var hash = crypto.createHash('md5'); - hash.update(str); - return hash.digest('hex'); -} - -module.exports.md5 = md5; \ No newline at end of file diff --git a/socketserver/mail/mailer.js b/socketserver/mail/mailer.js new file mode 100644 index 0000000..e397714 --- /dev/null +++ b/socketserver/mail/mailer.js @@ -0,0 +1,76 @@ +'use strict'; +const nodemailer = require('nodemailer'); +const xoauth2 = require('xoauth2'); +const fs = require('fs-extra'); +const nconf = require('nconf'); +const ejs = require('ejs'); + +class Mailer { + constructor() { + const options = nconf.get('room:mail:options'); + const self = this; + + if (nconf.get('room:allowrecovery') || nconf.get('room:mail:confirmation')) { + switch (nconf.get('room:mail:transport')) { + case 'smtp': { + self.transporter = nodemailer.createTransport(options); + break; + } + case 'xoauth': { + const xoauth = xoauth2.createXOAuth2Generator({ + user: options.auth.xoauth.user, + clientId: options.auth.xoauth.clientId, + clientSecret: options.auth.xoauth.clientSecret, + refreshToken: options.auth.xoauth.refreshToken, + accessToken: options.auth.xoauth.accessToken + }); + options.auth.xoauth = xoauth; + self.transporter = nodemailer.createTransport(options); + break; + } + case 'direct': { + const domain = nconf.get('room:mail:sender').split('@')[1]; + options.name = domain; + self.transporter = nodemailer.createTransport(options); + break; + } + default: { + break; + } + } + } + } + + sendEmail(type, opts, receiver, callback) { + const html = ejs.render(fs.readFileSync(`socketserver/mail/templates/${type}.html`, 'utf8'), { + opts, + room: nconf.get('room'), + }); + + let subject; + switch (type) { + case 'signup': + subject = 'Welcome to musiqpad!'; + break; + case 'recovery': + subject = 'Password recovery'; + break; + default: + break; + } + + const emailObj = { + from: nconf.get('room:mail:sender'), + to: receiver, + subject, + html, + }; + + this.transporter.sendMail(emailObj, (error, response) => { + if (error) callback(error); + else callback(null, response); + }); + } +} + +module.exports = new Mailer(); diff --git a/socketserver/mail/templates/recovery.html b/socketserver/mail/templates/recovery.html new file mode 100644 index 0000000..e04d60e --- /dev/null +++ b/socketserver/mail/templates/recovery.html @@ -0,0 +1,182 @@ + + + + + + + <%- room.name -%> | Password recovery + + + + + + + + + + +
+
+ + + + + + +
+ + + + + + +
+ + + + + + + +
+ + + + + + +
+
+

+ <%- room.name -%> +

+
+
+
+ + + + + + +
+ + + + + + + +
+ + + + + + +
+
+
+ +
+ + + + + + +
+
+

+ Forgot Your Password, <%- opts.user -%>? +

+ + + + + + +
+
+

+ It happens. To reset your password, go to <%- room.name -%>, open the login / signup dialog, click "Forgot password" button + and fill in the following: +


+
+ + + + + + + + + + + + + + + +
+ E-mail + + <%- opts.email -%> +
+ Recovery Code + + <%- opts.code -%> +
+ New Password + + your new password
+
+
+

+ The recovery code will be valid until <%- opts.timeout -%> +

+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/socketserver/mail/templates/signup.html b/socketserver/mail/templates/signup.html new file mode 100644 index 0000000..8ce83ae --- /dev/null +++ b/socketserver/mail/templates/signup.html @@ -0,0 +1,147 @@ + + + + + + + <%- room.name -%> + + + + + + + + + + +
+
+ + + + + + +
+ + + + + + +
+ + + + + + + +
+

+ Welcome to <%- room.name -%>! +

+
+ + + +
+
+
+
+
+ + + + + + +
+ + + + + + +
+ + + + + + + +
+ + + + + + +
+
+
+ +
+ + + + + + +
+
+

+ Before you are able to do anything, you are required to confirm your email address.
+ To do so, type the following line in chat:
+
+

+

+ /confirm <%- opts.code -%> +

+
+
+
+ + + + + + +
+
+
+
+
+ + \ No newline at end of file diff --git a/socketserver/mailer.js b/socketserver/mailer.js deleted file mode 100644 index 914488d..0000000 --- a/socketserver/mailer.js +++ /dev/null @@ -1,59 +0,0 @@ -var NM = require('nodemailer'); -var util = require('util'); -var config = require('../serverconfig'); -var xoauth2 = require('xoauth2'); -var fs = require('fs'); - -function Mailer(){ - //Check if we need to authorize against email server - if(config.room.allowrecovery || config.room.email.confirmation){ - var opts = config.room.email.options; - this.trans = NM.createTransport(((opts || {}).auth || {}).xoauth2 ? util._extend(opts, { - auth: { - xoauth2: xoauth2.createXOAuth2Generator(opts.auth.xoauth2), - }, - }) : opts) - } -} - -Mailer.prototype.sendEmail = function(type, opts, receiver, callback){ - this.trans.sendMail(this.makeEmailObj(type, receiver, opts), function(error, info){ - if(error) callback(error); - else callback(null, info); - }); -}; - -Mailer.prototype.makeEmailObj = function(type, receiver, opts){ - //Get email type - type = this.getType(type); - - //Replace all variables - type.body = type.body.replace(/%%[A-Z]+%%/g, function(k){ return opts[k.slice(2, -2).toLowerCase()] || k; }); - - //Return email options - return { - from: config.room.email.sender, - to: receiver, - subject: type.subject, - html: type.body, - }; -}; - -Mailer.prototype.getType = function(type){ - var returnObj = { - body: fs.readFileSync('socketserver/templates/' + type + '.html', 'utf8'), - }; - - switch(type){ - case 'signup': - returnObj.subject = 'Welcome to musiqpad!'; - break; - case 'recovery': - returnObj.subject = 'Password recovery'; - break; - } - - return returnObj; -} - -module.exports = new Mailer(); \ No newline at end of file diff --git a/socketserver/playlist.js b/socketserver/playlist.js index f0c7c73..1ae09eb 100644 --- a/socketserver/playlist.js +++ b/socketserver/playlist.js @@ -1,7 +1,7 @@ -var util = require('util'); -var DB = require('./database'); -var YT = require('./YT'); -var log = new (require('basic-logger'))({showTimestamp: true, prefix: "Playlist"}); +const util = require('util'); +const DB = require('./database'); +const YT = require('./YT'); +const log = new (require('basic-logger'))({showTimestamp: true, prefix: "Playlist"}); // Every user obj starts with this, then gets extended by what's in the db diff --git a/socketserver/role.js b/socketserver/role.js index 47207af..9d0e3dc 100644 --- a/socketserver/role.js +++ b/socketserver/role.js @@ -1,94 +1,94 @@ -var config = require('../serverconfig.js'); -var roles = config.roles; +'use strict'; +const nconf = require('nconf'); +let roles = nconf.get('roles'); +let roleOrder = null; +let staffRoles = null; -// Caching the value so we don't have to loop through something every login -var roleOrder = null; -var staffRoles = null; +class Role { + getRole(inRole) { + if (this.roleExists(inRole)) { + return roles[inRole]; + } -function Role(){ - -} + return roles.default; + } -Role.prototype.getRole = function(inRole){ - if (this.roleExists(inRole)) - return roles[inRole]; - - return roles.default; -}; - -Role.prototype.roleExists = function(inRole){ - if (typeof roles[inRole] !== 'undefined') - return true; - return false; -}; - -Role.prototype.checkPermission = function(inRole, inPerm){ - var role = this.getRole(inRole); - if (role){ - if((typeof inPerm) == 'string') { - return role.permissions.indexOf(inPerm) != -1; - }else{ - for(var i = 0; i < inPerm.length; i++) - if(role.permissions.indexOf(inPerm[i]) == -1) return false; + roleExists(inRole) { + if (typeof roles[inRole] !== 'undefined') { + return true; } - return true; + return false; } - return false; -}; - -Role.prototype.checkCanGrant = function(inRole, inPerm){ - var role = this.getRole(inRole); - - if (role){ - if((typeof inPerm) == 'string') { - inPerm = inPerm.toLowerCase(); - return role.canGrantRoles.indexOf(inPerm) != -1; - }else{ - for(var i = 0; i < inPerm.length; i++) - if(role.canGrantRoles.indexOf(inPerm[i].toLowerCase()) == -1) return false; + + checkPermission(inRole, inPerm) { + const role = this.getRole(inRole); + if (role) { + if ((typeof inPerm) === 'string') { + return role.permissions.indexOf(inPerm) !== -1; + } + for (let i = 0; i < inPerm.length; i++) { + if (role.permissions.indexOf(inPerm[i]) === -1) { + return false; + } + } + return true; } - - return true; + return false; } - return false; -}; - -Role.prototype.makeClientObj = function(){ - return roles; -}; - -Role.prototype.getOrder = function(){ - if (roleOrder) return roleOrder; - - if (config.roleOrder && Array.isArray(config.roleOrder)){ - for (var i in roles){ - if (config.roleOrder.indexOf(i) == -1) config.roleOrder.push(i); + + checkCanGrant(inRole, inPerm) { + const role = this.getRole(inRole); + + if (role) { + if ((typeof inPerm) === 'string') { + inPerm = inPerm.toLowerCase(); + return role.canGrantRoles.indexOf(inPerm) !== -1; + } + for (let i = 0; i < inPerm.length; i++) { + if (role.canGrantRoles.indexOf(inPerm[i].toLowerCase()) === -1) return false; + } + + return true; } - - roleOrder = config.roleOrder; - return config.roleOrder; - } - - var temp = []; - - for (var i in roles){ - temp.push(i); + return false; } - - roleOrder = temp; - return temp; -}; - -Role.prototype.getStaffRoles = function(){ - if(staffRoles) return staffRoles; - - if (config.staffRoles && Array.isArray(config.staffRoles)) { - staffRoles = config.staffRoles; - return staffRoles; + + makeClientObj() { + return roles; + } + + getOrder() { + if (roleOrder) return roleOrder; + + const roleOrderTemp = nconf.get('roleOrder'); + if (roleOrderTemp && Array.isArray(roleOrderTemp)) { + for (let i in roles) { + if (roleOrderTemp.indexOf(i) === -1) roleOrderTemp.push(i); + } + + roleOrder = roleOrderTemp; + return roleOrderTemp; + } + + const temp = []; + + for (var i in roles) { + temp.push(i); + } + + roleOrder = temp; + return temp; } - return []; -}; + getStaffRoles() { + if (staffRoles) return staffRoles; + if (nconf.get('staffRoles') && Array.isArray(nconf.get('staffRoles'))) { + staffRoles = nconf.get('staffRoles'); + return staffRoles; + } + return []; + } +} -module.exports = new Role(); \ No newline at end of file +module.exports = new Role(); diff --git a/socketserver/room.js b/socketserver/room.js index 07b28ae..1abedb0 100644 --- a/socketserver/room.js +++ b/socketserver/room.js @@ -1,710 +1,699 @@ -var extend = require('extend'); -var ws = require('ws'); -var https = require('https'); -var http = require('http'); -var log = new (require('basic-logger'))({showTimestamp: true, prefix: "Room"}); -var DJQueue = require('./djqueue.js'); -var Roles = require('./role'); -var config = require('../serverconfig'); -var DB = require('./database'); - -var defaultDBObj = function(){ +'use strict'; +const extend = require('extend'); +const ws = require('ws'); +const https = require('https'); +const log = new (require('basic-logger'))({ showTimestamp: true, prefix: 'Room' }); +const DJQueue = require('./djqueue.js'); +const Roles = require('./role'); +const DB = require('./database'); +const nconf = require('nconf'); + +const DefaultDBObj = function () { return { - roles: {}, + roles: {}, restrictions: {}, // Uses UID as key, object containing reason and end time as value. history: [] }; }; -var Room = function(socketServer, options){ - var that = this; - - this.roomInfo = extend(true, { - name: "", // Room name - slug: "", // Room name shorthand (no spaces, alphanumeric with dashes) - greet: "", // Room greetings - maxCon: 0, // Max connections; 0 = unlimited - ownerEmail: "", // Owner email for owner promotion - guestCanSeeChat: true, // Whether guests can see the chat or not - bannedCanSeeChat: true, // Whether banned users can see the chat - roomOwnerUN: null, // Username of the room owner to use with lobby API - }, options); - - this.socketServer = socketServer; - this.queue = new DJQueue( this ); - this.attendeeList = []; - this.data = new defaultDBObj(); - this.apiUpdateTimeout = null; - this.lastChat = []; - this.createApiTimeout(); - - this.restrictiontypes = [ - 'BAN', - 'MUTE', - 'SILENT_MUTE' +class Room { + constructor(socketServer, options) { + const that = this; + + this.roomInfo = extend(true, { + name: '', // Room name + slug: '', // Room name shorthand (no spaces, alphanumeric with dashes) + greet: '', // Room greetings + maxCon: 0, // Max connections; 0 = unlimited + ownerEmail: '', // Owner email for owner promotion + guestCanSeeChat: true, // Whether guests can see the chat or not + bannedCanSeeChat: true, // Whether banned users can see the chat + roomOwnerUN: null, // Username of the room owner to use with lobby API + }, options); + + this.socketServer = socketServer; + this.queue = new DJQueue(this); + this.attendeeList = []; + this.data = new DefaultDBObj(); + this.apiUpdateTimeout = null; + this.lastChat = []; + this.createApiTimeout(); + + this.restrictiontypes = [ + 'BAN', + 'MUTE', + 'SILENT_MUTE' ]; - DB.getRoom(this.roomInfo.slug, function(err, data){ - // Just in case the slug doesn't exist yet - data = data || {}; + DB.getRoom(this.roomInfo.slug, (err, data) => { + // Just in case the slug doesn't exist yet + data = data || {}; - // If the slug doesn't exist, make owner will make the slug - if (err && !err.notFound){ - console.log(err); - return; - } - - extend(true, that.data, data); + // If the slug doesn't exist, make owner will make the slug + if (err && !err.notFound) { + console.log(err); + return; + } - that.makeOwner(); - }); -}; + extend(true, that.data, data); -Room.prototype.getRoomMeta = function(){ - return { - name: this.roomInfo.name, - slug: this.roomInfo.slug, - greet: this.roomInfo.greet, - bg: this.roomInfo.bg, - guestCanSeeChat: this.roomInfo.guestCanSeeChat, - bannedCanSeeChat: this.roomInfo.bannedCanSeeChat, - roomOwnerUN: this.roomInfo.roomOwnerUN - }; -}; + that.makeOwner(); + }); + } -Room.prototype.makeOwner = function(){ - if (!config.room.ownerEmail) return; + getRoomMeta() { + return { + name: this.roomInfo.name, + slug: this.roomInfo.slug, + greet: this.roomInfo.greet, + bg: this.roomInfo.bg, + guestCanSeeChat: this.roomInfo.guestCanSeeChat, + bannedCanSeeChat: this.roomInfo.bannedCanSeeChat, + roomOwnerUN: this.roomInfo.roomOwnerUN + }; + } - var that = this; + makeOwner() { + if (!nconf.get('room:ownerEmail')) return; - DB.getUser(this.roomInfo.ownerEmail, function(err, data){ - if (err == 'UserNotFound') { console.log('Owner does not exist yet.'); that.data.roles.owner = []; return; } - if (err) { console.log('Cannot make Room Owner: ' + err); return; } + const that = this; - if (typeof data.uid !== 'number') { console.log('Cannot make room owner: UserUIDError'); return; } + DB.getUser(this.roomInfo.ownerEmail, (err, data) => { + if (err === 'UserNotFound') { + console.log('Owner does not exist yet.'); + that.data.roles.owner = []; + return; + } + if (err) { + console.log(`Cannot make Room Owner: ${err}`); + return; + } - log.info('Granting ' + data.un + ' (' + data.uid + ') Owner permissions'); + if (typeof data.uid !== 'number') { + console.log('Cannot make room owner: UserUIDError'); + return; + } - // Remove user from other roles to avoid interesting bugs - for (var i in that.data.roles){ - var ind = that.data.roles[i].indexOf(data.uid); - if ( ind > -1 ) that.data.roles[i].splice(ind, 1); - } + log.info(`Granting ${data.un} (${data.uid}) Owner permissions`); - // Only one owner, set entire array to one UID and set owner username for API - that.data.roles.owner = [ data.uid ]; - that.data.roomOwnerUN = data.un; - that.roomInfo.roomOwnerUN = data.un; - data.role = that.findRole(data.uid); - that.sendUserUpdate(data); - that.save(); - }); -}; + // Remove user from other roles to avoid interesting bugs + for (const i in that.data.roles) { + const ind = that.data.roles[i].indexOf(data.uid); + if (ind > -1) that.data.roles[i].splice(ind, 1); + } -Room.prototype.addUser = function( sock ){ - this.attendeeList.push( sock ); - var userSend = null; - var numGuests = 0; - sock.room = this.roomInfo.slug; + // Only one owner, set entire array to one UID and set owner username for API + that.data.roles.owner = [data.uid]; + that.data.roomOwnerUN = data.un; + that.roomInfo.roomOwnerUN = data.un; + data.role = that.findRole(data.uid); + that.sendUserUpdate(data); + that.save(); + }); + } - if (sock.user){ - this.checkMakeOwner(); - sock.user.data.role = this.findRole(sock.user.data.uid); - userSend = sock.user.getClientObj(); + addUser(sock) { + this.attendeeList.push(sock); + let userSend = null; + let numGuests = 0; + sock.room = this.roomInfo.slug; + + if (sock.user) { + this.checkMakeOwner(); + sock.user.data.role = this.findRole(sock.user.data.uid); + userSend = sock.user.getClientObj(); - for (var i = 0; i < this.attendeeList.length; i++){ - var sockObj = this.attendeeList[i]; + for (let i = 0; i < this.attendeeList.length; i++) { + const sockObj = this.attendeeList[i]; - if (!sockObj.user){ - numGuests++; - continue; - } + if (!sockObj.user) { + numGuests++; + continue; + } - if (sockObj == sock) continue; + if (sockObj === sock) continue; - if (sockObj.user && sock.user && sockObj.user.data.uid == sock.user.data.uid){ - this.removeUser(sockObj); - sockObj.close(1000, JSON.stringify({ - type: 'ConnectedElsewhere' - })); + if (sockObj.user && sock.user && sockObj.user.data.uid === sock.user.data.uid) { + this.removeUser(sockObj); + sockObj.close(1000, JSON.stringify({ + type: 'ConnectedElsewhere' + })); + } } - } - }else{ - for (var i = 0; i < this.attendeeList.length; i++){ - var sockObj = this.attendeeList[i]; + } else { + for (let i = 0; i < this.attendeeList.length; i++) { + const sockObj = this.attendeeList[i]; - if (!sockObj.user){ - numGuests++; + if (!sockObj.user) { + numGuests++; + } } } - } - //TODO: Find and add role key to user object from room db - - this.sendAll({ - type: 'userJoined', - data: { - user: userSend, - guests: numGuests - } - }, - function(sockObj){ - return sockObj != sock; - }); + // TODO: Find and add role key to user object from room db -}; + this.sendAll({ + type: 'userJoined', + data: { + user: userSend, + guests: numGuests + } + }, + sockObj => sockObj !== sock); + } -Room.prototype.replaceUser = function( sock_old, sock_new ){ - if (!sock_old || !sock_old.user || !sock_new || !sock_new.user || sock_old.user.data.uid != sock_new.user.data.uid) return false; - var ind = this.attendeeList.indexOf(sock_old); - this.checkMakeOwner(); + replaceUser(sockOld, sockNew) { + if (!sockOld || !sockOld.user || !sockNew || !sockNew.user || sockOld.user.data.uid !== sockNew.user.data.uid) return false; + const ind = this.attendeeList.indexOf(sockOld); + this.checkMakeOwner(); + if (ind === -1) return false; - if (ind == -1 ) return false; + sockNew.room = this.roomInfo.slug; + sockNew.user.data.role = this.findRole(sockOld.user.data.uid); + this.attendeeList[ind] = sockNew; + this.queue.replaceSocket(sockOld, sockNew); - sock_new.room = this.roomInfo.slug; + return true; + } - sock_new.user.data.role = this.findRole(sock_old.user.data.uid); + removeUser(sock) { + const that = this; + const ind = this.attendeeList.indexOf(sock); - this.attendeeList[ind] = sock_new; + if (ind > -1) { + sock.room = null; - this.queue.replaceSocket(sock_old, sock_new); + let userSend = null; - return true; -}; + this.queue.remove(sock); -Room.prototype.removeUser = function( sock ){ - var that = this; - var ind = this.attendeeList.indexOf(sock); + if (sock.user) { + userSend = sock.user.getClientObj(); + sock.user.data.role = null; + } - if (ind > -1) { - sock.room = null; + this.attendeeList.splice(ind, 1); - var userSend = null; + this.sendAll({ + type: 'userLeft', + data: { + user: userSend, + guests: ((() => { + let num = 0; + for (let i = 0; i < that.attendeeList.length; i++) { + if (!that.attendeeList[i].user) num++; + } + return num; + }))() + } + }); + } + } - this.queue.remove( sock ); + restrictUser(restrictObj, callback) { + /* + Expects { + restrictObj: { + uid: uid, + end: int, + start: int, + reason: '', + type: '', + source: { + uid: uid, + role: role + } + } + } + */ + const that = this; - if (sock.user) { - userSend = sock.user.getClientObj(); - sock.user.data.role = null; + if (this.restrictiontypes.indexOf(restrictObj.type) === -1) { + if (callback) callback('InvalidRestrictionType'); + return; } - this.attendeeList.splice( ind, 1 ); - - this.sendAll({ - type: 'userLeft', - data: { - user: userSend, - guests: (function(){ - var num = 0; - for (var i = 0; i < that.attendeeList.length; i++){ - if (!that.attendeeList[i].user) num++; - } - return num; - })() + DB.getUserByUid(restrictObj.uid, (err, user) => { + if (err) { + if (callback) { + callback(err); + } + return; } - }); - } -}; -Room.prototype.restrictUser = function(restrictObj, callback){ - /* - Expects { - restrictObj: { - uid: uid, - end: int, - start: int, - reason: '', - type: '', - source: { - uid: uid, - role: role + if (that.isUserRestricted(restrictObj.uid, restrictObj.type)) { + if (callback) callback('UserAlreadyRestricted'); + return; } - } - } - */ - var that = this; - - if(this.restrictiontypes.indexOf(restrictObj.type) == -1) - { - if (callback) callback("InvalidRestrictionType"); - return; - } - - DB.getUserByUid(restrictObj.uid, function(err, user) { - if (err) { - if (callback) - callback(err); - return; - } - - if (that.isUserRestricted(restrictObj.uid, restrictObj.type)){ - if (callback) callback('UserAlreadyRestricted'); - return; - } - user.role = that.findRole(user.uid); - - if (!Roles.checkCanGrant(restrictObj.source.role, [user.role])) { - if (callback) callback('UserCannotBeRestricted'); - return; - } - - restrictObj.reason = restrictObj.reason.substr(0, 50); - - that.data.restrictions[restrictObj.uid] = that.data.restrictions[restrictObj.uid] || {}; - that.data.restrictions[restrictObj.uid][restrictObj.type] = restrictObj; - that.save(); - - that.sendAll({ - type: 'userRestricted', - data: { - uid: restrictObj.uid, - type: restrictObj.type, - source: restrictObj.source.uid, + user.role = that.findRole(user.uid); + + if (!Roles.checkCanGrant(restrictObj.source.role, [user.role])) { + if (callback) callback('UserCannotBeRestricted'); + return; } - }, function(obj){ - return restrictObj.type != 'SILENT_MUTE' || (obj.user && Roles.checkPermission(obj.user.role, 'room.restrict.silent_mute')); - }); - var userSock = that.findSocketByUid(restrictObj.uid); + restrictObj.reason = restrictObj.reason.substr(0, 50); + + that.data.restrictions[restrictObj.uid] = that.data.restrictions[restrictObj.uid] || {}; + that.data.restrictions[restrictObj.uid][restrictObj.type] = restrictObj; + that.save(); - //Check if user is online - if (userSock && restrictObj.type == 'BAN'){ - that.removeUser(userSock); - userSock.close(1000, JSON.stringify({ - type: 'banned', + that.sendAll({ + type: 'userRestricted', data: { - end: restrictObj.end, - reason: restrictObj.reason + uid: restrictObj.uid, + type: restrictObj.type, + source: restrictObj.source.uid, } - })); - } - - if (callback) callback(null); - }); -}; + }, obj => restrictObj.type !== 'SILENT_MUTE' || (obj.user && Roles.checkPermission(obj.user.role, 'room.restrict.silent_mute'))); + + const userSock = that.findSocketByUid(restrictObj.uid); + + // Check if user is online + if (userSock && restrictObj.type === 'BAN') { + that.removeUser(userSock); + userSock.close(1000, JSON.stringify({ + type: 'banned', + data: { + end: restrictObj.end, + reason: restrictObj.reason + } + })); + } -Room.prototype.getRestrictions = function(arr, uid){ - var out = {}; - - for(var key in this.data.restrictions[uid]){ - if(key.indexOf(arr)) - out[key] = this.data.restrictions[uid][key]; + if (callback) callback(null); + }); } - - return out; -}; -Room.prototype.unrestrictUser = function(uid, type, sock){ - if (this.data.restrictions[uid][type]){ - delete this.data.restrictions[uid][type]; - this.save(); + getRestrictions(arr, uid) { + const out = {}; - this.sendAll({ - type: 'userUnrestricted', - data: { - uid: uid, - type: type, - source: (sock ? sock.user.data.uid : null) - } - }, function(obj){ - return type != 'SILENT_MUTE' || (obj.user && Roles.checkPermission(obj.user.role, 'room.restrict.silent_mute')); - }); + for (const key in this.data.restrictions[uid]) { + if (key.indexOf(arr)) + out[key] = this.data.restrictions[uid][key]; + } - return true; + return out; } - return false; -}; -Room.prototype.isUserRestricted = function(uid, type){ - if ((this.data.restrictions[uid] || {})[type]) { - if (this.data.restrictions[uid][type].end < new Date(Date.now())) { - this.unrestrictUser(uid, type); - return false; + unrestrictUser(uid, type, sock) { + if (this.data.restrictions[uid][type]) { + delete this.data.restrictions[uid][type]; + this.save(); + + this.sendAll({ + type: 'userUnrestricted', + data: { + uid, + type, + source: (sock ? sock.user.data.uid : null) + } + }, obj => type !== 'SILENT_MUTE' || (obj.user && Roles.checkPermission(obj.user.role, 'room.restrict.silent_mute'))); + + return true; } - else { + return false; + } + + isUserRestricted(uid, type) { + if ((this.data.restrictions[uid] || {})[type]) { + if (this.data.restrictions[uid][type].end < new Date(Date.now())) { + this.unrestrictUser(uid, type); + return false; + } return true; } + return false; } - return false; -}; -Room.prototype.setRole = function(user, role){ - if (!user) return false; + setRole(user, role) { + if (!user) return false; - if (!role) role = 'default'; + if (!role) role = 'default'; - role = role.toLowerCase(); + role = role.toLowerCase(); - if (Roles.roleExists(role)){ - if (typeof this.data.roles[role] === 'undefined') this.data.roles[role] = []; + if (Roles.roleExists(role)) { + if (typeof this.data.roles[role] === 'undefined') this.data.roles[role] = []; - var userSock = this.findSocketByUid(user.uid); + const userSock = this.findSocketByUid(user.uid); - // Remove user from other role - this.removeRole(user); + // Remove user from other role + this.removeRole(user); - if (role != 'default') - this.data.roles[role].push(user.uid); + if (role !== 'default') this.data.roles[role].push(user.uid); - user.role = role; + user.role = role; - - // Save the changes - this.save(); + // Save the changes + this.save(); - if (userSock){ - // We can't assign this user object to the socket because it lacks playlists - userSock.user.data.role = role; - } + if (userSock) { + // We can't assign this user object to the socket because it lacks playlists + userSock.user.data.role = role; + } - this.sendUserUpdate(user); + this.sendUserUpdate(user); - return true; + return true; + } + return false; } - return false; -}; + removeRole(user) { + if (!user) return; -Room.prototype.removeRole = function(user){ - if (!user) return; - - for (var i in this.data.roles){ - var ind = this.data.roles[i].indexOf(user.uid); - if ( ind > -1){ - this.data.roles[i].splice(ind, 1); + for (const i in this.data.roles) { + const ind = this.data.roles[i].indexOf(user.uid); + if (ind > -1) { + this.data.roles[i].splice(ind, 1); + } } } -}; -Room.prototype.findRole = function(uid){ - if (!uid) return 'default'; + findRole(uid) { + if (!uid) return 'default'; - for (var i in this.data.roles){ - var ind = this.data.roles[i].indexOf(uid); - if ( ind > -1 && Roles.roleExists(i) ){ - return i; + for (const i in this.data.roles) { + const ind = this.data.roles[i].indexOf(uid); + if (ind > -1 && Roles.roleExists(i)) { + return i; + } } + + return 'default'; } - return 'default'; -}; + findSocketByUid(uid) { + for (const i in this.attendeeList) { + if (!this.attendeeList[i].user) continue; -Room.prototype.findSocketByUid = function( uid ){ + if (this.attendeeList[i].user.data.uid === uid) return this.attendeeList[i]; + } - for (var i in this.attendeeList){ - if (!this.attendeeList[i].user) continue; + return null; + } - if (this.attendeeList[i].user.data.uid == uid) return this.attendeeList[i]; + getAttendees() { + return this.attendeeList; } - return null; -}; + getBannedUsers(callback) { + const banned = []; + const rawBanned = []; + const that = this; -Room.prototype.getAttendees = function(){ - return this.attendeeList; -}; + for (const i in this.data.restrictions) { + // This will unban appropriately when the list is viewed. + if (this.isUserRestricted(i, 'BAN')) + rawBanned.push(i); + } -Room.prototype.getBannedUsers = function(callback){ - var banned = []; - var rawBanned = []; - var that = this; - - for (var i in this.data.restrictions){ - // This will unban appropriately when the list is viewed. - if (this.isUserRestricted(i, 'BAN')) - rawBanned.push(i); - } + if (!rawBanned.length) { + callback('NoBans'); + return; + } + + DB.getUserByUid(rawBanned, { getPlaylists: false }, (err, users) => { + for (const j in users) { + const usr = users[j].getClientObj(); + usr.role = that.findRole(usr.uid); + banned.push(usr); + } - if (!rawBanned.length){ - callback('NoBans'); - return; + callback(err, banned); + }); } - DB.getUserByUid(rawBanned, {getPlaylists: false}, function (err, users) { - for (var j in users){ - var usr = users[j].getClientObj(); - usr.role = that.findRole(usr.uid); - banned.push(usr); + getRoomStaff(callback) { + const staff = []; + let rawStaff = []; + const that = this; + + for (const i in this.data.roles) { + if (Roles.getStaffRoles().indexOf(i) > -1) { + rawStaff = rawStaff.concat(this.data.roles[i]); + } } - callback(err, banned); - }); -}; + if (!rawStaff.length) { + callback('NoStaff'); + return; + } -Room.prototype.getRoomStaff = function(callback){ - var staff = []; - var rawStaff = []; - var that = this; + DB.getUserByUid(rawStaff, { getPlaylists: false }, (err, users) => { + for (const j in users) { + const usr = users[j].getClientObj(); + usr.role = that.findRole(usr.uid); + staff.push(usr); + } - for (var i in this.data.roles){ - if (Roles.getStaffRoles().indexOf(i) > -1) { - rawStaff = rawStaff.concat(this.data.roles[i]); - } + callback(err, staff); + }); } - if (!rawStaff.length){ - callback('NoStaff'); - return; + sendSystemMessage(message) { + this.sendAll({ type: 'systemMessage', data: message }); } - DB.getUserByUid(rawStaff, { getPlaylists: false }, function (err, users) { - for (var j in users){ - var usr = users[j].getClientObj(); - usr.role = that.findRole(usr.uid); - staff.push(usr); - } + sendBroadcastMessage(message) { + this.sendAll({ type: 'broadcastMessage', data: message }); + } - callback(err, staff); - }); -}; + sendMessage(sock, message, ext, specdata, callback) { + const that = this; -Room.prototype.sendSystemMessage = function(message) { - this.sendAll({type:'systemMessage', data:message}); -}; + message = message.substring(0, 255).replace(//g, '>'); -Room.prototype.sendBroadcastMessage = function(message) { - this.sendAll({type:'broadcastMessage', data:message}); -}; + callback = callback || (() => {}); -Room.prototype.sendMessage = function( sock, message, ext, specdata, callback ){ - var that = this; + if (this.isUserRestricted(sock.user.uid, 'SILENT_MUTE')) { + DB.logChat(sock.user.uid, message, 'res:mute_s', (err, cid) => { + sock.sendJSON({ + type: 'chat', + data: { + uid: sock.user.uid, + message, + time: Date.now(), + cid, + special: specdata, + } + }); + callback(cid); + }); + } else if (this.isUserRestricted(sock.user.uid, 'MUTE')) { + callback(null); + } else { + DB.logChat(sock.user.uid, message, specdata, (err, cid) => { + that.sendAll({ + type: 'chat', + data: { + uid: sock.user.uid, // Will always be present. Unauthd can't send messages + message, + time: Date.now(), + cid, + special: specdata + } + }, obj => { + // Guests can't see chat with config variable set + if (!that.roomInfo.guestCanSeeChat && !obj.user) return false; - message = message.substring(0,255).replace(//g, '>'); + // Banned users can't see chat with config variable set + if (!that.roomInfo.bannedCanSeeChat && obj.user && that.isUserRestricted(obj.user.uid, 'BAN')) return false; - callback = callback || function(){}; - - if(this.isUserRestricted(sock.user.uid, 'SILENT_MUTE')){ - DB.logChat(sock.user.uid, message, 'res:mute_s', function(err, cid){ - sock.sendJSON({ - type: 'chat', - data: { - uid: sock.user.uid, - message: message, - time: Date.now(), - cid: cid, - special: specdata, - } - }); - callback(cid); - }); - } else if(this.isUserRestricted(sock.user.uid, 'MUTE')){ - callback(null); - } else { - DB.logChat(sock.user.uid, message, specdata, function(err, cid){ - that.sendAll({ - type: 'chat', - data: { - uid: sock.user.uid, // Will always be present. Unauthd can't send messages - message: message, - time: Date.now(), - cid: cid, - special: specdata - } - }, function(obj){ - // Guests can't see chat with config variable set - if (!that.roomInfo.guestCanSeeChat && !obj.user) return false; - - // Banned users can't see chat with config variable set - if (!that.roomInfo.bannedCanSeeChat && obj.user && that.isUserRestricted(obj.user.uid, 'BAN')) return false; - - // Check for extensive function - if("function" === typeof ext) if(!ext(obj)) return false; - - return true; - }); - - //Save last X messages to show newly connected users - if(!specdata){ - that.lastChat.push({ - user: sock.user.getClientObj(), - message: message, - time: Date.now(), - cid: cid, + // Check for extensive function + if (typeof ext === 'function') if (!ext(obj)) return false; + + return true; }); - if(that.lastChat.length > config.room.lastmsglimit) that.lastChat.shift(); - } - - callback(cid); - }); - } -}; -Room.prototype.makePrevChatObj = function(){ - var uids = []; - var temp = extend(true, [], this.lastChat); + // Save last X messages to show newly connected users + if (!specdata) { + that.lastChat.push({ + user: sock.user.getClientObj(), + message, + time: Date.now(), + cid, + }); + if (that.lastChat.length > nconf.get('room:lastmsglimit')) that.lastChat.shift(); + } - for (var i = 0; i < temp.length; i++){ - var ind = uids.indexOf(temp[i].user.uid); - if ( ind == -1 ){ - uids.push( temp[i].user.uid ); - continue; + callback(cid); + }); } - - temp[i].user = { uid: temp[i].user.uid }; } - return temp; -}; + makePrevChatObj() { + const uids = []; + const temp = extend(true, [], this.lastChat); -Room.prototype.deleteChat = function(cid, uid){ - for (var i = 0; i < this.lastChat.length; i++){ - if (this.lastChat[i].cid == cid){ - this.lastChat.splice(i, 1); - break; - } - } + for (let i = 0; i < temp.length; i++) { + const ind = uids.indexOf(temp[i].user.uid); + if (ind === -1) { + uids.push(temp[i].user.uid); + continue; + } - this.sendAll({ - type: 'deleteChat', - data: { - cid: cid, - mid : uid + temp[i].user = { uid: temp[i].user.uid }; } - }); -}; -Room.prototype.sendAll = function (message, condition){ - condition = condition || function(){return true;}; - for (var i in this.attendeeList){ - var obj = this.attendeeList[i]; + return temp; + } - if (obj.readyState != ws.OPEN || !condition(obj)) continue; + deleteChat(cid, uid) { + for (let i = 0; i < this.lastChat.length; i++) { + if (this.lastChat[i].cid === cid) { + this.lastChat.splice(i, 1); + break; + } + } - obj.sendJSON(message); + this.sendAll({ + type: 'deleteChat', + data: { + cid, + mid: uid + } + }); } -}; -Room.prototype.sendUserUpdate = function(user){ - if (!user) return; + sendAll(message, condition) { + if (!condition) condition = () => true; + for (const i in this.attendeeList) { + const obj = this.attendeeList[i]; - this.sendAll({ - type: 'userUpdate', - data: { - user: user.getClientObj() - } - }); -}; - -Room.prototype.getUsersObj = function(){ - var temp = { - guests: 0, - users: {} - }; - var guestCounter = 0; + if (obj.readyState !== ws.OPEN || !condition(obj)) continue; - for (var i = 0; i < this.attendeeList.length; i++){ - var obj = this.attendeeList[i]; - if (!obj.user){ - temp.guests++; - continue; + obj.sendJSON(message); } + } + + sendUserUpdate(user) { + if (!user) return; - temp.users[ obj.user.uid ] = obj.user.getClientObj(); + this.sendAll({ + type: 'userUpdate', + data: { + user: user.getClientObj() + } + }); } - return temp; -}; + getUsersObj() { + const temp = { + guests: 0, + users: {} + }; -Room.prototype.getHistoryObj = function() { - return this.data.history.slice(-config.room.history.limit_send).reverse(); -}; + for (let i = 0; i < this.attendeeList.length; i++) { + const obj = this.attendeeList[i]; + if (!obj.user) { + temp.guests++; + continue; + } -Room.prototype.addToHistory = function(historyObj) { - //Limit history - if(config.room.history.limit_save !== 0) - while(this.data.history.length >= config.room.history.limit_save) { - this.data.history.shift(); + temp.users[obj.user.uid] = obj.user.getClientObj(); } - //Add to history and save - this.data.history.push(historyObj); - this.save(); -}; + return temp; + } -Room.prototype.updateLobbyServer = function(song, dj, callback) { - if (!config.apis.musiqpad.sendLobbyStats) { - if (callback) callback(); - return; + getHistoryObj() { + return this.data.history.slice(-nconf.get('room:history:limit_send')).reverse(); } - else if (!config.apis.musiqpad.key || config.apis.musiqpad.key == "") { - throw "A musiqpad key must be defined in the config for updating the lobby server."; - return; + + addToHistory(historyObj) { + // Limit history + if (nconf.get('room:history:limit_save') !== 0) { + while (this.data.history.length >= nconf.get('room:history:limit_save')) { + this.data.history.shift(); + } + } + + // Add to history and save + this.data.history.push(historyObj); + this.save(); } - var postData = { - song: song, - dj: dj, - room: this.getRoomMeta(), - userCount: this.attendeeList.length - }; - var postOptions = { - host: 'api.musiqpad.com', - port: '443', - path: '/pad/' + this.roomInfo.slug, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'apikey': config.apis.musiqpad.key - } - }; - try { - var postReq = https.request(postOptions, function (response) { - if (response.statusCode < 200 || response.statusCode > 299) { - console.log('Request Failed with Status Code: ' + response.statusCode); - } - if (callback) callback(); - }); - postReq.write(JSON.stringify(postData)); - postReq.on('error', function() { + + updateLobbyServer(song, dj, callback) { + if (!nconf.get('apis:musiqpad:sendLobbyStats')) { + if (callback) callback(); + return; + } else if (!nconf.get('apis:musiqpad:key') || nconf.get('apis:musiqpad:key') === '') { + console.log('A musiqpad key must be defined in the config for updating the lobby server.'); + return; + } + const postData = { + song, + dj, + room: this.getRoomMeta(), + userCount: this.attendeeList.length + }; + const postOptions = { + host: 'api.musiqpad.com', + port: '443', + path: `/pad/${this.roomInfo.slug}`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + apikey: nconf.get('apis:musiqpad:key') + } + }; + try { + const postReq = https.request(postOptions, response => { + if (response.statusCode < 200 || response.statusCode > 299) { + console.log(`Request Failed with Status Code: ${response.statusCode}`); + } + if (callback) callback(); + }); + postReq.write(JSON.stringify(postData)); + postReq.on('error', () => { + postReq.end(); + console.log('Lobby Update errored.'); + }); + postReq.setTimeout(3000, () => { + console.log('Lobby Update timed out.'); + postReq.abort(); + }); postReq.end(); - console.log('Lobby Update errored.'); - }); - postReq.setTimeout(3000, function() { - console.log('Lobby Update timed out.'); - postReq.abort(); - }); - postReq.end(); - } - catch (e) { + } catch (e) { } + this.createApiTimeout(); } - this.createApiTimeout(); -}; - -Room.prototype.createApiTimeout = function() { - var that = this; - clearTimeout(this.apiUpdateTimeout); + createApiTimeout() { + const that = this; + clearTimeout(this.apiUpdateTimeout); - this.apiUpdateTimeout = setTimeout(function() { - if (that.queue.currentsong && that.queue.currentdj) { - that.updateLobbyServer(that.queue.currentsong, that.queue.currentdj ? that.queue.currentdj.user.getClientObj() : null); - } - else { - that.updateLobbyServer(null, null); - } - }, 300000); - return this.apiUpdateTimeout; -}; + this.apiUpdateTimeout = setTimeout(() => { + if (that.queue.currentsong && that.queue.currentdj) { + that.updateLobbyServer(that.queue.currentsong, that.queue.currentdj ? that.queue.currentdj.user.getClientObj() : null); + } else { + that.updateLobbyServer(null, null); + } + }, 300000); + return this.apiUpdateTimeout; + } -Room.prototype.sockIsJoined = function(sock){ - if (this.attendeeList.indexOf(sock) > -1) - return true; - return false; -}; + sockIsJoined(sock) { + if (this.attendeeList.indexOf(sock) > -1) return true; + return false; + } -Room.prototype.makeDbObject = function(){ - return this.data; -}; + makeDbObject() { + return this.data; + } -Room.prototype.save = function(){ - DB.setRoom(this.roomInfo.slug, this.makeDbObject()); -}; + save() { + DB.setRoom(this.roomInfo.slug, this.makeDbObject()); + } -Room.prototype.checkMakeOwner = function() { - if (this.data.roles.owner && this.data.roles.owner.length == 0) { - this.makeOwner(); + checkMakeOwner() { + if (this.data.roles.owner && this.data.roles.owner.length === 0) { + this.makeOwner(); + } } } diff --git a/socketserver/socketserver.js b/socketserver/socketserver.js index d737e0c..5beec20 100644 --- a/socketserver/socketserver.js +++ b/socketserver/socketserver.js @@ -1,25 +1,24 @@ +'use strict'; //Modules -var ws = require('ws'); -var http = require('http'); -var https = require('https'); -var Duration = require('durationjs'); -var request = require('request'); -var util = require('util'); -var extend = require('extend'); -var updateNotifier = require('update-notifier'); -var _ = require('underscore'); +const ws = require('ws'); +const http = require('http'); +const https = require('https'); +const Duration = require('durationjs'); +const request = require('request'); +const extend = require('extend'); +const updateNotifier = require('update-notifier'); +const fs = require('fs-extra'); +const nconf = require('nconf'); +const crypto = require('crypto'); //Files -var config = require('../serverconfig'); -var DB = require("./database"); -var Room = require('./room'); -var User = require('./user'); -var Mailer = require('./mailer'); -var YT = require('./YT'); -var Roles = require('./role'); -var Hash = require('./hash'); -var log = new (require('basic-logger'))({showTimestamp: true, prefix: "SocketServer"}); -var WebSocketServer = ws.Server; +const DB = require("./database"); +const Room = require('./room'); +const Mailer = require('./mail/Mailer'); +const YT = require('./YT'); +const Roles = require('./role'); +const log = new (require('basic-logger'))({showTimestamp: true, prefix: "SocketServer"}); +const WebSocketServer = ws.Server; ws.prototype.sendJSON = function(obj){ @@ -195,23 +194,27 @@ var SocketServer = function(server){ if (server){ settings.server = server; }else{ - var port = config.socketServer.port || undefined; - var ip = config.socketServer.host || undefined; + var port = nconf.get('socketServer:port') || undefined; + var ip = nconf.get('socketServer:host') || undefined; - if (config.certificate && config.certificate.key && config.certificate.cert){ - settings.server = https.createServer(config.certificate).listen(port,ip); - }else{ + if (nconf.get('useSSL') && nconf.get('certificate') && nconf.get('certificate:key') && nconf.get('certificate:cert')) { + let certificates = { + key: fs.readFileSync(nconf.get('certificate:key')), + cert: fs.readFileSync(nconf.get('certificate:cert')), + } + settings.server = https.createServer(certificates).listen(port, ip); + } else { settings.server = http.createServer().listen(port,ip); } } this.wss = new WebSocketServer(settings); - log.info('Socket server listening on port ' + (config.socketServer.port || config.webServer.port)); + log.info('Socket server listening on port ' + (nconf.get('socketServer:port') || nconf.get('webServer:port'))); // this.wss = new WebSocketServer({ port: config.socketServer.port }); // log.info('Socket server listening on port ' + config.socketServer.port); - this.room = new Room(this, config.room); + this.room = new Room(this, nconf.get('room')); // Keepalive packets. This.... is messy. setInterval( function(){ @@ -361,7 +364,7 @@ var SocketServer = function(server){ } else if(socket.room && that.room.isUserRestricted(socket.user.uid, 'BAN')){ returnObj.data = { error: 'UserBanned' }; - } else if((Date.now() - socket.user.created) <= config.room.signupcd){ + } else if((Date.now() - socket.user.created) <= nconf.get('room:signupcd')){ returnObj.data = { error: 'UserOnCooldown' }; } else if(socket.user.confirmation){ @@ -401,7 +404,7 @@ var SocketServer = function(server){ } */ //Check if recovery is enabled - if (!(config.room.allowrecovery)){ + if (!(nconf.get('room:allowrecovery'))){ returnObj.data = { error: 'RecoveryDisabled' }; @@ -418,15 +421,17 @@ var SocketServer = function(server){ break; } + console.log("Sending recovery email"); var sendRecovery = function(user){ //Generate new code and send email - user.recovery = Hash.md5(Date.now() + '', user.un); + user.recovery = utils.db.randomBytes(36, 'base64'); Mailer.sendEmail('recovery', { user: user.un, code: user.recovery.code, email: data.data.email, timeout: (new Date().addDays(1)).toISOString().replace(/T/, ' ').replace(/\..+/, '') + ' UTC', }, data.data.email, function(err, data){ + console.log("Recovery email send!"); if(err){ returnObj.data = { error: 'EmailAuthIssue', @@ -664,16 +669,16 @@ var SocketServer = function(server){ votes: that.room.queue.makeVoteObj(), vote: that.room.queue.getUserVote( socket ), }, - historylimit: config.room.history.limit_send, + historylimit: nconf.get('room:history:limit_send'), roles: Roles.makeClientObj(), roleOrder: Roles.getOrder(), staffRoles: Roles.getStaffRoles(), - lastChat: ((!socket.user && !config.room.guestCanSeeChat) || (that.room.isUserRestricted((socket.user || {}).uid, 'BAN') && !config.room.bannedCanSeeChat)) ? [] : that.room.makePrevChatObj(), + lastChat: ((!socket.user && !nconf.get('room:guestCanSeeChat')) || (that.room.isUserRestricted((socket.user || {}).uid, 'BAN') && !nconf.get('room:bannedCanSeeChat'))) ? [] : that.room.makePrevChatObj(), time: new Date().getTime(), - captchakey: config.apis.reCaptcha.key, - allowemojis: config.room.allowemojis, - description: config.room.description, - recaptcha: config.room.recaptcha, + captchakey: nconf.get('apis:reCaptcha:key'), + allowemojis: nconf.get('room:allowemojis'), + description: nconf.get('room:description'), + recaptcha: nconf.get('room:recaptcha'), }; socket.sendJSON(returnObj); @@ -1380,12 +1385,12 @@ var SocketServer = function(server){ if(data.type == 'login'){ DB.loginUser(data.data, callback); } else { - if(config.room.recaptcha){ + if (nconf.get('room:recaptcha')) { request.post( 'https://www.google.com/recaptcha/api/siteverify', { form: { - secret: config.apis.reCaptcha.secret, + secret: nconf.get('apis:reCaptcha:secret'), response: data.data.captcha, remoteip: socket.upgradeReq.connection.remoteAddress, } diff --git a/socketserver/templates/recovery.html b/socketserver/templates/recovery.html deleted file mode 100644 index 8d59b91..0000000 --- a/socketserver/templates/recovery.html +++ /dev/null @@ -1,7 +0,0 @@ -There is a pending request to reset your password on musiqpad for your account %%USER%%
-If you did not request this, plese ignore this email
-To reset your password, go to musiqpad, open the login / signup dialog, click "Forgot password" button and fill in the following:
-E-mail: %%EMAIL%%
-Recovery Code: %%CODE%%
-New Password: <your new password>
-The recovery code will be valid until %%TIMEOUT%% \ No newline at end of file diff --git a/socketserver/templates/signup.html b/socketserver/templates/signup.html deleted file mode 100644 index 65c028c..0000000 --- a/socketserver/templates/signup.html +++ /dev/null @@ -1,4 +0,0 @@ -Thanks you for registering on musiqpad, %%USER%%!
-Before you are able to do anything, you are required to confirm your email address.
-To do so, type the following line in chat

-

/confirm %%CODE%%

\ No newline at end of file diff --git a/socketserver/user.js b/socketserver/user.js index a37a2c9..5ab67ee 100644 --- a/socketserver/user.js +++ b/socketserver/user.js @@ -1,13 +1,14 @@ -var util = require('util'); -var DB = require('./database'); -var DBUtils = require('./database_util'); +'use strict' +const util = require('util'); +let DB; +const utils = require('./utils'); // Every user obj starts with this, then gets extended by what's in the db var defaultObj = function(){ return { uid: 0, un: "", - pw: "", // MD5(SHA256(pass) + SALT) + pw: "", role: null, activepl: null, created: 0, @@ -85,6 +86,7 @@ function removeFields(obj, fields){ * */ function User(){ + DB = require('./database'); this.userExists = false; this.data = new defaultObj; } @@ -270,10 +272,8 @@ Object.defineProperty( User.prototype, 'pw', { return this.data.pw; }, set: function(val) { - - this.data.pw = DBUtils.makePass(val, this.data.salt); + this.data.pw = utils.hash.bcrypt(val); this.updateUser(); - return this; } }); @@ -402,4 +402,4 @@ Object.defineProperty( User.prototype, 'blocked', { } }); -module.exports = User; \ No newline at end of file +module.exports = User; diff --git a/socketserver/utils/database.js b/socketserver/utils/database.js new file mode 100644 index 0000000..6588897 --- /dev/null +++ b/socketserver/utils/database.js @@ -0,0 +1,22 @@ +const Hash = require('./hash'); +const crypto = require("crypto"); + +const DBUtils = { + validateEmail(email) { + return /^.+@.+\..+$/.test(email); + }, + + validateUsername(un) { + return /^[a-z0-9_-]{3,20}$/i.test(un); + }, + + makePassMD5(inPass, salt) { + return Hash.md5(('' + inPass) + (salt || '')).toString(); + }, + + randomBytes(bytes, format) { + return crypto.randomBytes(bytes).toString(format); + } +}; + +module.exports = DBUtils; diff --git a/socketserver/utils/hash.js b/socketserver/utils/hash.js new file mode 100644 index 0000000..85d8134 --- /dev/null +++ b/socketserver/utils/hash.js @@ -0,0 +1,18 @@ +const crypto = require('crypto'); +const bcrypt = require('bcrypt-nodejs'); + +module.exports = { + md5(str) { + const hash = crypto.createHash('md5'); + hash.update(str); + return hash.digest('hex'); + }, + isMD5(hash) { + return (/[a-fA-F0-9]{32}/).test(hash); + }, + bcrypt(str) { + const salt = bcrypt.genSaltSync(12); + return bcrypt.hashSync(str, salt); + }, + compareBcrypt: bcrypt.compareSync, +} \ No newline at end of file diff --git a/socketserver/utils/index.js b/socketserver/utils/index.js new file mode 100644 index 0000000..d864eab --- /dev/null +++ b/socketserver/utils/index.js @@ -0,0 +1,8 @@ +const database = require('./database'); +const hash = require('./hash'); +const token = require('./token'); + +const utils = { + +}; +module.exports = Object.assign(utils, { db: database }, { hash }, { token }); diff --git a/socketserver/utils/token.js b/socketserver/utils/token.js new file mode 100644 index 0000000..b8824d0 --- /dev/null +++ b/socketserver/utils/token.js @@ -0,0 +1,11 @@ +const jwt = require("jsonwebtoken"); + +module.exports = { + createToken(payload, secret, expires) { + return jwt.sign(payload, secret, { + expiresIn: expires + }); + }, + verify: jwt.verify, + decode: jwt.decode, +} \ No newline at end of file diff --git a/start.js b/start.js index df8d1b8..a118484 100644 --- a/start.js +++ b/start.js @@ -1,74 +1,86 @@ -var config = require('./serverconfig'); -var fs = require('fs'); -var SocketServer = require("./socketserver/socketserver"); -var log = new(require('basic-logger'))({ - showTimestamp: true, - prefix: "SocketServer" -}); -var path = require('path'); +// eslint-disable-next-line +'use strict'; +// NCONF +const nconf = require('nconf'); +const fs = require('fs-extra'); +const hjson = require('hjson'); +const crypto = require('crypto'); + +const hjsonWrapper = { + parse: (text) => hjson.parse(text, { keepWsc: true, }), + stringify: (text) => hjson.stringify(text, { keepWsc: true, quotes: 'always', bracesSameLine: true }), +}; +if (!fileExistsSync('config.hjson')) { + fs.copySync('config.example.hjson', 'config.hjson'); +} +nconf.argv().env().file({ file: 'config.hjson', format: hjsonWrapper }); -if(!config.setup){ - log.error("Please, setup your server by editing the 'serverconfig.js' file"); - return; +if (!nconf.get('tokenSecret')) { + const random = crypto.randomBytes(256); + nconf.set('tokenSecret', random.toString('hex')); + nconf.save(); } -var server = null; +// Modules +const SocketServer = require('./socketserver/socketserver'); +const path = require('path'); +const webserver = require('./webserver/app'); +const log = new(require('basic-logger'))({ + showTimestamp: true, + prefix: 'SocketServer', +}); +let server; -var webConfig = '// THIS IS AN AUTOMATICALLY GENERATED FILE\n\nvar config=JSON.parse(\'' + JSON.stringify( - { - useSSL: config.useSSL, - serverPort: config.socketServer.port, - selfHosted: true, - serverHost: config.socketServer.host - } - ) + '\')'; +const webConfig = `// THIS IS AN AUTOMATICALLY GENERATED FILE\n\nvar config=JSON.parse('${JSON.stringify({ + useSSL: nconf.get('useSSL'), + serverPort: nconf.get('socketServer:port'), + selfHosted: !0, + serverHost: nconf.get('socketServer:host') +})}')`; -if (config.hostWebserver){ - fs.writeFileSync(path.join(__dirname, '/webserver/public/lib/js', 'webconfig.js'), webConfig); - var webserver = require('./webserver/app'); - server = (config.socketServer.port == config.webServer.port || config.socketServer.port == '') ? webserver.server : null; +if (nconf.get('hostWebserver')) { + fs.writeFileSync(path.join(__dirname, '/webserver/public/lib/js', 'webconfig.js'), webConfig); + server = (nconf.get('socketServer:port') === nconf.get('webServer:port') || nconf.get('socketServer:port') === '') ? webserver.server : null; } -if (config.apis.musiqpad.sendLobbyStats && (!config.apis.musiqpad.key || config.apis.musiqpad.key == '')) { - throw 'In order to send stats to the lobby you must generate an key here: https://musiqpad.com/lounge'; +if (nconf.get('apis:musiqpad:sendLobbyStats') && (!nconf.get('apis:musiqpad:key') || nconf.get('apis:musiqpad:key') === '')) { + log.error('In order to send stats to the lobby you must generate an key here: https://musiqpad.com/lounge'); + process.exit(); } fs.writeFileSync(path.join(__dirname, '', 'webconfig.js'), webConfig); -var socketServer = new SocketServer(server); +const socketServer = new SocketServer(server); -process.on('uncaughtException', function(err) { +process.on('uncaughtException', (err) => { console.log(err); console.log(err.stack); socketServer.gracefulExit(); }); process.on('exit', socketServer.gracefulExit); - -//catches ctrl+c event process.on('SIGINT', socketServer.gracefulExit); -function fileExistsSync() { - var exists = false; - try { - exists = fs.statSync(path); - } catch(err) { - exists = false; - } - - return !!exists; -} - -if(process.argv[2] === "--daemon") { - if (fileExistsSync(__dirname + '/pidfile')) { +if (process.argv[2] === '--daemon') { + if (fileExistsSync(`${__dirname}/pidfile`)) { try { - var pid = fs.readFileSync(__dirname + '/pidfile', { encoding: 'utf-8' }); + const pid = fs.readFileSync(`${__dirname}/pidfile`, { encoding: 'utf-8' }); process.kill(pid, 0); process.exit(); } catch (e) { - fs.unlinkSync(__dirname + '/pidfile'); + fs.unlinkSync(`${__dirname}/pidfile`); } } - fs.writeFile(__dirname + '/pidfile', process.pid); -} \ No newline at end of file + fs.writeFile(`${__dirname}/pidfile`, process.pid); +} + +function fileExistsSync(path) { + let exists = false; + try { + exists = fs.statSync(path); + } catch (err) { + exists = false; + } + return !!exists; +} diff --git a/test/socketserver/hash.js b/test/socketserver/hash.js deleted file mode 100644 index ecfc9ba..0000000 --- a/test/socketserver/hash.js +++ /dev/null @@ -1,9 +0,0 @@ -const test = require('ava'); -const md5 = require('./../../socketserver/hash.js').md5; - -test.before(() => { -}); - -test('hash', t => { - t.is(md5('test'), '098f6bcd4621d373cade4e832627b4f6'); -}); diff --git a/test/socketserver/database_util.js b/test/socketserver/utils/database.js similarity index 60% rename from test/socketserver/database_util.js rename to test/socketserver/utils/database.js index 055b9ab..83e3ed9 100644 --- a/test/socketserver/database_util.js +++ b/test/socketserver/utils/database.js @@ -1,28 +1,20 @@ -const test = require('ava'); -const DBUtils = require('./../../socketserver/database_util'); - -function makePass(t, input, expected) { - t.is(DBUtils.makePass(input[0], input[1]), expected); -} - -function validateEmail(t, input, expected) { - t.is(DBUtils.validateEmail(input), expected); -} - -function validateUsername(t, input, expected) { - t.is(DBUtils.validateUsername(input), expected); -} - -test('Creates correct password hash with salt', makePass, ['test', 'randomSalt'], '4b4e47738ba3b7aab65e421787b519ff'); -test('Creates correct hash without salt', makePass, ['test', null], '098f6bcd4621d373cade4e832627b4f6'); -test('Hash is always converted to a string', makePass, ['ximaz', null], '61529519452809720693702583126814'); - -test('user@example.com is a valid email', validateEmail, 'user@example.com', true); -test('@example.com isn\'t a valid email', validateEmail, '@example.com', false); -test('user@example. isn\'t a valid email', validateEmail, 'user@example.', false); - -test('123 is a valid username', validateUsername, '123', true); -test('123456789101112131415 is\n a valid username', validateUsername, '123456789101112131415', false); -test('*user* is\n a valid username', validateUsername, '*user*', false); -test('test_user is a valid username', validateUsername, 'test_user', true); - +const test = require('ava'); +const DBUtils = require('./../../../socketserver/utils/index').db; + +function validateEmail(t, input, expected) { + t.is(DBUtils.validateEmail(input), expected); +} + +function validateUsername(t, input, expected) { + t.is(DBUtils.validateUsername(input), expected); +} + +test('user@example.com is a valid email', validateEmail, 'user@example.com', true); +test('@example.com isn\'t a valid email', validateEmail, '@example.com', false); +test('user@example. isn\'t a valid email', validateEmail, 'user@example.', false); + +test('123 is a valid username', validateUsername, '123', true); +test('123456789101112131415 is\n a valid username', validateUsername, '123456789101112131415', false); +test('*user* is\n a valid username', validateUsername, '*user*', false); +test('test_user is a valid username', validateUsername, 'test_user', true); + diff --git a/webserver/app.js b/webserver/app.js index 3fcfb52..fdf669e 100644 --- a/webserver/app.js +++ b/webserver/app.js @@ -1,67 +1,98 @@ -var express = require('express'); -var compression = require('compression'); -var path = require('path'); -var http = require('http'); -var https = require('https'); -var fs = require('fs'); -var config = require('../serverconfig.js'); +'use strict'; +const express = require('express'); +const compression = require('compression'); +const path = require('path'); +const http = require('http'); +const https = require('https'); +const fs = require('fs'); +const nconf = require('nconf'); +const ejs = require('ejs'); +const helmet = require('helmet'); -var app = express(); -var server = null; -var server2 = null; -var socketServer = null; +const app = express(); +let server2 = null; +let server = null; +// eslint-disable-next-line +let socketServer = null; -if (config.certificate && config.certificate.key && config.certificate.cert){ - server = https.createServer(config.certificate, app); - if(config.webServer.redirectHTTP && config.webServer.redirectPort != '') - server2 = http.createServer(app); +/* SSL */ +if (nconf.get('useSSL') && nconf.get('certificate') && nconf.get('certificate:key') && nconf.get('certificate:cert')) { + const certificate = { + key: fs.readFileSync(nconf.get('certificate:key')), + cert: fs.readFileSync(nconf.get('certificate:cert')), + }; + + server = https.createServer(certificate, app); + if (nconf.get('webServer:redirectHTTP') && nconf.get('webServer:redirectPort') !== '') { + server2 = http.createServer(app); + } +} else { + server = http.createServer(app); } -else { - server = http.createServer(app); + +if (nconf.get('webServer:redirectHTTP')) { + app.use((req, res, next) => { + if (!req.secure) { + return res.redirect(['https://', req.hostname, ':', nconf.get('webServer:port') || process.env.PORT, req.url].join('')); + } + next(); + }); } +app.set('view engine', 'html'); +app.set('views', `${__dirname}/public`); +app.engine('html', ejs.renderFile); app.use(compression()); +app.use(helmet.frameguard()); +app.use(helmet.xssFilter()); +app.use(helmet.hidePoweredBy()); -if(config.webServer.redirectHTTP) - app.use(function(req, res, next) { - if(!req.secure) { - return res.redirect(['https://', req.hostname, ":", config.webServer.port || process.env.PORT, req.url].join('')); - } - next(); - }); +app.get(['/', '/index.html'], (req, res) => { + res.render('index', { + tags: nconf.get('room:tags'), + room: nconf.get('room'), + scripts: nconf.get('room:scripts'), + }); +}); -app.use(express.static(path.resolve(__dirname, 'public'))); app.use('/pads', express.static(path.resolve(__dirname, 'public'))); -app.get('/config', function(req, res) { - res.setHeader("Content-Type", "application/javascript"); - res.send(fs.readFileSync(__dirname + '/public/lib/js/webconfig.js')); +app.use(express.static(path.resolve(__dirname, 'public'))); + +app.get('/config', (req, res) => { + res.setHeader('Content-Type', 'application/javascript'); + res.send(fs.readFileSync(`${__dirname}/public/lib/js/webconfig.js`)); }); -app.get('/api/room', function(req, res) { - var roomInfo = { - "slug": config.room.slug, - "name": config.room.name, - "people": null, - "queue": null, - "media": null, - }; - res.send(roomInfo); + +app.get('/api/room', (req, res) => { + const roomInfo = { + slug: nconf.get('room:slug'), + name: nconf.get('room:name'), + people: null, + queue: null, + media: null, + }; + res.send(roomInfo); }); -server.listen(config.webServer.port || process.env.PORT, config.webServer.address || process.env.IP, function(){ - var addr = server.address(); - console.log("Webserver listening at", addr.address + ":" + addr.port); +server.listen(nconf.get('webServer:port') || process.env.PORT, nconf.get('webServer:address') || process.env.IP, () => { + const addr = server.address(); + console.log('Webserver listening at', `${addr.address}:${addr.port}`); }); -if(server2 != null){ - server2.listen(config.webServer.redirectPort || 80, config.webServer.address || process.env.IP, function(){ - var addr2 = server2.address(); - console.log("HTTP Webserver listening at", addr2.address + ":" + addr2.port); - }); +if (server2 != null) { + server2.listen(nconf.get('webServer:redirectPort') || 80, nconf.get('webServer:address') || process.env.IP, () => { + const addr2 = server2.address(); + console.log('HTTP Webserver listening at', `${addr2.address}:${addr2.port}`); + }); } -var setSocketServer = function(ss){ - socketServer = ss; +const setSocketServer = ss => { + socketServer = ss; }; - -module.exports = {app: app, server: server, server2: server2, setSocketServer: setSocketServer}; +module.exports = { + app, + server, + server2, + setSocketServer, +}; diff --git a/webserver/public/index.html b/webserver/public/index.html index ef92eef..902e5bd 100644 --- a/webserver/public/index.html +++ b/webserver/public/index.html @@ -2,9 +2,26 @@ - - - musiqpad - join us! + + + <% if (tags) { -%> + + + + + + + + + <% } else { -%> + <% } -%> + + <%- room.name -%> + <% if (scripts.css) { -%> + <% scripts.css.forEach(function(url){ -%> + + <% }); -%> + <% } -%> @@ -745,5 +762,10 @@ + <% if (scripts.js) { -%> + <% scripts.js.forEach(function(url){ -%> + + <% }); -%> + <% } -%>