UNPKG

plugapi

Version:

Generic API for building plug.dj bots

1,213 lines (1,071 loc) 136 kB
'use strict'; // Node.JS Core Modules const http = require('http'); const Https = require('https'); const util = require('util'); // Third-party Modules const autoBind = require('auto-bind'); const constants = require('./constants.json'); const EventEmitter3 = require('eventemitter3'); const handleErrors = require('handle-errors')('PlugAPI', true); const pRetry = require('p-retry'); const WebSocket = require('ws'); const chalk = require('chalk'); const got = require('got'); const plugMessageSplit = require('plug-message-split'); const tough = require('tough-cookie'); const Socket = require('./socket.js'); /** * node.js HTTPS agent with keepAlive enabled * @const * @type {exports} * @private */ const agent = new Https.Agent({ keepAlive: true }); // plugAPI /** * BufferObject class * @type {BufferObject|exports} * @private */ const BufferObject = require('./bufferObject'); /** * Event Object Types * @type {exports} * @private */ const EventObjectTypes = require('./eventObjectTypes'); /** * Room class * @type {Room|exports} * @private */ const Room = require('./room'); /** * Package.json of plugAPI * @const * @type {exports} * @private */ const PlugAPIInfo = require('../package.json'); /** * Storage for private data that should never be publiclly accessable * @type {exports|WeakMap} * @private */ const store = require('./privateVars'); /** * REST Endpoints * @const * @type {{CHAT_DELETE: String, HISTORY: String, MODERATE_ADD_DJ: String, MODERATE_BAN: String, MODERATE_BOOTH: String, MODERATE_MOVE_DJ: String, MODERATE_MUTE: String, MODERATE_PERMISSIONS: String, MODERATE_REMOVE_DJ: String, MODERATE_SKIP: String, MODERATE_UNBAN: String, MODERATE_UNMUTE: String, PLAYLIST: String, ROOM_CYCLE_BOOTH: String, ROOM_INFO: String, ROOM_LOCK_BOOTH: String, USER_SET_AVATAR: String, USER_GET_AVATARS: String}} * @private */ const endpoints = require('./endpoints.json'); /** * DateUtilities written by plug.dj & Modified by TAT (TAT@plugcubed.net) * @copyright 2014 - 2017 by Plug DJ, Inc. * @private */ const DateUtilities = { MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], SERVER_TIME: null, OFFSET: 0, setServerTime(e) { this.SERVER_TIME = this.convertUnixDateStringToDate(e); this.OFFSET = this.SERVER_TIME.getTime() - Date.now(); }, yearsSince(e) { return this.serverDate().getFullYear() - e.getFullYear(); }, monthsSince(e) { const t = this.serverDate(); return ((t.getFullYear() - e.getFullYear()) * 12) + (t.getMonth() - e.getMonth()); }, daysSince(e) { const t = this.serverDate(); const n = t.getTime(); const r = e.getTime(); const i = 864e5; let s = (n - r) / i; const o = (n - r) % i / i; if (o > 0 && o * i > this.secondsSinceMidnight(t) * 1e3) { s++; } return ~~s; }, hoursSince(e) { return ~~((this.serverDate().getTime() - e.getTime()) / 36e5); }, minutesSince(e) { return ~~((this.serverDate().getTime() - e.getTime()) / 6e4); }, secondsSince(e) { return ~~((this.serverDate().getTime() - e.getTime()) / 1e3); }, monthName(e, t) { const n = this.MONTHS[e.getMonth()]; return t ? n : n.substr(0, 3); }, secondsSinceMidnight(e) { const t = new Date(e.getTime()); this.midnight(t); return ~~((e.getTime() - t.getTime()) / 1e3); }, midnight(e) { e.setHours(0); e.setMinutes(0); e.setSeconds(0); e.setMilliseconds(0); }, minutesUntil(e) { return ~~((e.getTime() - this.serverDate().getTime()) / 6e4); }, secondsUntil(e) { return ~~((e.getTime() - this.serverDate().getTime()) / 1e3); }, millisecondsUntil(e) { return e.getTime() - this.serverDate().getTime(); }, serverDate() { return new Date(Date.now() + this.OFFSET); }, getServerEpoch() { return Date.now() + this.OFFSET; }, getSecondsElapsed(e) { return !e || Object.is(e, '0') ? 0 : this.secondsSince(new Date(e.substr(0, e.indexOf('.')))); }, convertUnixDateStringToDate(e) { return e ? new Date(e.substr(0, 4), Number(e.substr(5, 2)) - 1, e.substr(8, 2), e.substr(11, 2), e.substr(14, 2), e.substr(17, 2)) : null; } }; /** * Create instance of PlugAPI. * @param {String} authenticationData.email The email to login with. * @param {String} authenticationData.password The password to login with. * @param {Boolean} [authenticationData.guest = null] True to login as guest. * @param {String} authenticationData.facebook.accessToken The access token for Facebook login. * @param {String} authenticationData.facebook.userID The user ID for facebook login. * @param {Function} [callback] An optional callback utilized in async mode * @extends {EventEmitter3} * * @example * <caption>There are multiple ways to create a bot. Sync vs Async, FB vs guest or email. Please choose only one of the examples to use.</caption> * // Sync * // Password * const bot = new PlugAPI({email: 'something@something.com', password: 'hunter2'}); * * // Facebook * // To login with fb will require logging in via plug and viewing the data sent in to /_/auth/facebook via the network tab of dev tools. * * const bot = new PlugAPI({ * facebook: { * userID: 'xxxxxxxx', * accessToken: 'xxxxxx' * } * }); * * // Guest * const bot = new PlugAPI(); * // OR * const bot = new PlugAPI({ guest: true }); * * // Async * * // Password * new PlugAPI({email: 'something@something.com', password: 'hunter2'}, (err, bot) => { * if (err) throw new Error(err); * * }); * * // Facebook * new PlugAPI({ * facebook: { * userID: 'xxxxxxxx', * accessToken: 'xxxxxx' * } * }, (err, bot) => { * if (err) throw new Error(err); * }); * * // Guest * new PlugAPI({ guest: true }, (err, data) => { * if (err) throw new Error(err); * ]}); * * @constructor */ class PlugAPI extends EventEmitter3 { constructor(authenticationData, callback) { super(); autoBind(this); /** * Jethro Logger * @type {Logger|exports} * @private */ this.logger = require('./logger'); /** * Is User a guest? * @type {Boolean} * @private */ this.guest = false; /** * Is User logging in via Facebook? * @type {Boolean} * @private */ this.fbLogin = false; /** * Slug of room, that the bot is currently connecting to * @type {null|String} * @private */ store(this)._connectingRoomSlug = null; /** * Authentication information (e-mail and password) * THIS MUST NEVER BE ACCESSIBLE NOR PRINTING OUT TO CONSOLE * @type {Object} * @private */ store(this)._auththenticationInfo = null; /** * Queue of outgoing chat messages * @type {Object} * @private */ store(this)._chatQueue = { queue: [], sent: 0, limit: 10, running: false }; /** * plug.dj url to send https requests * @private * @type {String} */ this.baseUrl = 'https://plug.dj'; /** * Socket url to send socket events to. * @private * @type {String} */ this.socketUrl = 'wss://ws-prod.plug.dj:443/socket'; /** * WebSocket (connection to plug.DJ's socket server) * @type {null|WebSocket} * @private */ store(this)._ws = null; /** * Instance of Room * @type {Room} * @private */ store(this)._room = new Room(); /** * Is everything initialized? * @type {Boolean} * @private */ store(this)._initialized = false; /** * Prefix that defines if a message is a command. Default is ! * @type {String} */ this.commandPrefix = '!'; /** * List over chat history * Contains up to 512 messages * @type {Array} * @private */ store(this)._chatHistory = []; /** * Contains informations about requests sent to server * @type {{queue: Array, sent: Number, limit: Number, running: Boolean}} * @private */ store(this)._serverRequests = { queue: [], sent: 0, limit: 10, running: false }; /** * Checks if a number is finite and not an empty string. * @param {*} number The variable to check if it's a number or not. * @returns {Boolean} True if a number. False if not. * @private */ store(this)._isNumber = (number) => isFinite(Number(number)) && !Object.is(number, ''); /** * Queue that the bot should connect to the socket server * @param {String} roomSlug Slug of room to join after connection * @private */ store(this)._queueConnectSocket = (roomSlug) => { store(this)._serverRequests.queue.push({ type: 'connect', server: 'socket', room: roomSlug }); if (!store(this)._serverRequests.running) { store(this)._queueTicker(); } }; /** * Connect to plug.DJ's socket server * @param {String} roomSlug Slug of room to join after connection * @private */ store(this)._connectSocket = (roomSlug) => { store(this)._ws = new Socket(this.socketUrl); store(this)._ws.on('open', () => { store(this)._getAuthCode((authCode) => { store(this)._sendEvent('auth', authCode); this.logger.success('plug.dj Socket Server', chalk`{green Authenticated as a ${this.guest ? 'guest' : 'user'}}`); this.emit('connected'); this.emit('server:socket:connected'); }); }); store(this)._ws.on('message', (data) => { if (!Object.is(data, 'h')) { const payload = JSON.parse(data || '[]'); for (let i = 0; i < payload.length; i++) { store(this)._ws.emit('data', payload[i]); } } }); store(this)._ws.on('data', store(this)._messageHandler.bind(this)); store(this)._ws.on('data', (data) => this.emit('tcpMessage', data)); store(this)._ws.on('error', (a) => { this.logger.error('plug.dj Socket Server', a); }); store(this)._ws.on('reconnecting', () => { this.logger.info('Socket Server', 'Reconnecting'); }); store(this)._ws.on('close', (a) => { this.logger.warn('plug.dj Socket Server', `Closed with Code ${a}`); }); store(this)._ws.connect(); }; /** * The ticker that runs through the server queue and executes them when it's time * @private */ store(this)._queueTicker = () => { store(this)._serverRequests.running = true; const canSend = store(this)._serverRequests.sent < store(this)._serverRequests.limit; const obj = store(this)._serverRequests.queue.pop(); if (canSend && obj) { store(this)._serverRequests.sent++; if (Object.is(obj.type, 'rest')) { store(this)._sendREST(obj.opts, obj.callback); } else if (Object.is(obj.type, 'connect')) { if (Object.is(obj.server, 'socket')) { store(this)._connectSocket(obj.room); } } setTimeout(() => { store(this)._serverRequests.sent--; }, 6e4); } if (store(this)._serverRequests.queue.length > 0) { setImmediate(store(this)._queueTicker); } else { store(this)._serverRequests.running = false; } }; /** * The ticker that runs through the chat queue and executes them when it's time * @private */ store(this)._queueChatTicker = () => { const delay = (time) => { return new Promise((resolve) => setTimeout(resolve, time)); }; store(this)._chatQueue.running = true; if (store(this)._chatQueue.queue.length > 0) { if (store(this)._floodProtectionDelay < store(this)._floodProtectionDelayMax) { store(this)._floodProtectionDelay += 75; } const obj = store(this)._chatQueue.queue.shift(); if (obj) { store(this)._sendEvent(PlugAPI.events.CHAT, obj.msg); if (!Object.is(obj.timeout, undefined) && store(this)._isNumber(obj.timeout) && Number(obj.timeout) >= 0) { const specificChatDeleter = (data) => { if (Object.is(data.raw.uid, store(this)._room.getSelf().id) && Object.is(data.message.trim(), obj.msg.trim())) { setTimeout(() => { this.moderateDeleteChat(data.raw.cid); }, Number(obj.timeout) * 1E3); this.off(PlugAPI.events.CHAT, specificChatDeleter); } }; this.on(PlugAPI.events.CHAT, specificChatDeleter); } } delay(store(this)._floodProtectionDelay).then(() => store(this)._queueChatTicker()); } else { store(this)._chatQueue.running = false; store(this)._floodProtectionDelay = 0; } }; /** * Function to increment or decrement the playlist length count. * @type {Function} * @param {Array} playlist the playlist to modify * @param {Boolean} increment Whether to increment or decrement the playlist count. * @returns {Array} The playlist is returned with the modified count * @private */ store(this)._editPlaylistLength = (playlist, increment) => { if (Object.is(typeof increment, 'undefined') || Object.is(increment, true)) { playlist.count++; } else { playlist.count--; } return playlist; }; /** * Convert seconds to human readable format of HH:MM:SS * @param {Number} seconds The amount of time in seconds * @private * @returns {String} A string in the format of HH:MM:SS */ store(this)._convertSecondsToHMS = (seconds) => { if (!store(this)._isNumber(seconds)) return '00:00'; let secondsRemaining = parseInt(seconds, 10); const hoursRemaining = Math.floor(secondsRemaining / 3600); secondsRemaining %= 3600; const minsRemaining = Math.floor(secondsRemaining / 60); secondsRemaining %= 60; return `${hoursRemaining > 10 ? hoursRemaining : `0${hoursRemaining}`}:${minsRemaining > 10 ? minsRemaining : `0${minsRemaining}`}:${secondsRemaining > 10 ? secondsRemaining : `0${secondsRemaining}`}`; }; /** * Check if an object contains a value * @param {Object} obj The object * @param {*} value The value * @param {Boolean} [strict] Whether to use strict mode check or not * @private * @returns {Boolean} if object contains value */ store(this)._objectContainsValue = (obj, value, strict) => { for (const i in obj) { if (!obj.hasOwnProperty(i)) continue; if ((!strict && obj[i] == value) || (strict && Object.is(obj[i], value))) return true; // eslint-disable-line eqeqeq } return false; }; /** * Common callback for all API calls * * @callback RESTCallback * @name RESTCallback * * @param {null|String} err Error message on error; otherwise null * @param {null|*} data Data on success; otherwise null */ /** * Queue REST request * @param {String} method REST method * @param {String} endpoint Endpoint on server * @param {Object|Undefined} [data] Data * @param {RESTCallback|Undefined} [restCallback] Callback function * @param {Boolean} [skipQueue] Skip queue and send the request immediately * @private */ store(this)._queueREST = (method, endpoint, data, restCallback, skipQueue) => { restCallback = Object.is(typeof restCallback, 'function') ? restCallback : () => { }; const opts = { method, headers: Object.assign(store(this)._headers, { cookie: this.jar.getCookieStringSync('https://plug.dj'), Referer: `https://plug.dj/${store(this)._room.getRoomMeta().slug == null ? '' : store(this)._room.getRoomMeta().slug}` }), url: `${this.baseUrl}/_/${endpoint}` }; if (data != null) { opts.body = data; } if (skipQueue && Object.is(skipQueue, true)) { store(this)._sendREST(opts, restCallback); } else { store(this)._serverRequests.queue.push({ type: 'rest', opts, callback: restCallback }); if (!store(this)._serverRequests.running) { store(this)._queueTicker(); } } }; /** * Send a REST request * @param {Object} opts An object of options to send in to the request module. * @param {RESTCallback} sendCallback Callback function * @private */ store(this)._sendREST = (opts, sendCallback) => { const request = (attempts) => { if (Object.is(attempts % 5, 0)) this.logger.debug('REST', `Route: ${opts.url} sendREST attempt ${attempts}/10`); return got(opts.url, Object.assign(opts, { headers: store(this)._headers, agent, json: true })).then((body) => { body = body.body; if (body && Object.is(body.status, 'ok')) { return sendCallback(null, body.data); } return sendCallback(body && body.status ? body.status : new Error("Can't connect to plug.dj"), null); }).catch((err) => { if (err && err.statusCode) { if (Object.is(err.url, 'https://plug.dj/_/rooms/join') && err.statusCode < 500) { process.nextTick(() => { const statusMessage = `${err.statusCode} - ${http.STATUS_CODES[err.statusCode]}`; let errMessage = ''; if (err.response && err.response.body) { const status = err.response.body.status; const data = err.response.body.data; if (Object.is(status, 'ban') && data && data[0]) { if (Object.is(data[0].e, -1)) { errMessage = `${statusMessage} Can't join room due to being permanently banned`; } else { errMessage = `${statusMessage} Can't join room due to being banned. Time remaining: ${store(this)._convertSecondsToHMS(data[0].e)}`; } } else if (Object.is(status, 'roomCapacity')) { errMessage = `${statusMessage} Can't join room due to room being over capacity.`; } else if (Object.is(status, 'requestError') && data) { errMessage = `${statusMessage} Can't join room due to a request error. Data: ${JSON.stringify(err.response.body, null, 2)}`; } else if (Object.is(status, 'notFound')) { errMessage = `${statusMessage} Can't join room. Invalid Room slug.`; } else { errMessage = `${statusMessage} Can't join room. Data: ${JSON.stringify(err.response.body, null, 2)}`; } if (!Object.is(errMessage, '')) { throw new Error(errMessage); } } throw new Error(`${statusMessage} Can't join room.`); }); } else if (Object.is(err.statusCode, 403)) { if (Object.is(err.url, 'https://plug.dj/_/booth') || Object.is(err.url, 'https://plug.dj/_/booth/add')) { return sendCallback(err, null); } process.nextTick(() => { throw new Error(`${opts.url} responded with ${err.statusCode} (${err.statusMessage}`); }); } throw err; } process.nextTick(() => { throw new Error(err); }); }); }; pRetry(request, { retries: 10, randomize: true, maxTimeout: 10000 }) .catch((err) => { this.logger.error('REST', `Route: ${opts.url} ${(err ? err : '')} Guest Mode: ${this.guest}`); return sendCallback(`Route: ${opts.url} ${(err ? err : '')} Guest Mode: ${this.guest}`, null); }); }; /** * Get the room state. * @param {RESTCallback} roomstateCallback Callback function * @private */ store(this)._getRoomState = (roomstateCallback) => { store(this)._queueREST('GET', 'rooms/state', undefined, (err, data) => { if (err) { throw new Error(`Error getting room state: ${err ? err : 'Unknown error'}`); } store(this)._connectingRoomSlug = null; store(this)._initRoom(data[0], () => { if (Object.is(typeof roomstateCallback, 'function')) { return roomstateCallback(null, data); } }); }); }; /** * Queue that the bot should join a room. * @param {String} slug Slug of room to join after connection * @param {RESTCallback} [joinCallback] Callback function * @private */ store(this)._joinRoom = (slug, joinCallback) => { store(this)._queueREST('POST', 'rooms/join', { slug }, (err) => { if (err) return; store(this)._getRoomState(joinCallback); }); }; /** * Perform the login process using credentials. * @param {Function} loginCallback Callback * @private */ store(this)._performLoginCredentials = (loginCallback) => { /** * Is the login process running. * Used for sync * @type {boolean} * @private */ let loggingIn = true; const setCookie = (res) => { if (Array.isArray(res.headers['set-cookie'])) { res.headers['set-cookie'].forEach((item) => { this.jar.setCookieSync(item, 'https://plug.dj'); }); } else if (Object.is(typeof res.headers['set-cookie'], 'string')) { this.jar.setCookieSync(res.headers['set-cookie'], 'https://plug.dj'); } }; const handleLogin = (body) => { if (body && body.body && !Object.is(body.body.status, 'ok')) { handleErrors.error(`Login Error: ${body && body.body && !Object.is(body.body.status, 'ok') ? `API Status: ${body.body.status}` : ''} ${body && !Object.is(body.statusCode, 200) ? `HTTP Status: ${body.body.statusCode} - ${http.STATUS_CODES[body.body.statusCode]}` : ''}`, loginCallback); } else { loggingIn = false; if (Object.is(typeof loginCallback, 'function')) { return loginCallback(null, this); } } }; const handleError = (err) => { process.nextTick(() => { handleErrors.error(`Login Error: \n${err || ''}`, loginCallback); }); }; if (this.guest) { const guestLogin = (guestAttempts) => { if (Object.is(guestAttempts % 5, 0)) this.logger.debug('PlugAPI', `Attempt #${guestAttempts} to connect to plug.dj as guest`); return got(`${this.baseUrl}/plugcubed`).then((data) => { if (data.body) { const serverTime = data.body.split('_st')[1].split('"')[1]; DateUtilities.setServerTime(serverTime); loggingIn = false; setCookie(data); if (Object.is(typeof loginCallback, 'function')) { return loginCallback(null, this); } } else if (Object.is(typeof loginCallback, 'function')) { return loginCallback(new Error('unable to connect to plug.dj'), null); } }); }; pRetry(guestLogin, { forever: true, randomize: true, maxTimeout: 10000 }) .catch(handleError); } else { const login = (attempts) => { if (Object.is(attempts % 5, 0)) this.logger.debug('PlugAPI', `Attempt #${attempts} to obtain server data from plug.dj`); return got(`${this.baseUrl}/_/mobile/init`, { headers: store(this)._headers, json: true }).then((indexBody) => { const info = indexBody.body; const csrfToken = info.data[0].c; const serverTime = info.data[0].t; DateUtilities.setServerTime(serverTime); setCookie(indexBody); if (this.fbLogin) { const fbLogin = (facebookAttempts) => { if (Object.is(facebookAttempts % 5, 0)) this.logger.debug('PlugAPI', `Attempt #${facebookAttempts} to login to plug.dj`); return got(`${this.baseUrl}/_/auth/facebook`, { json: true, method: 'POST', headers: Object.assign(store(this)._headers, { cookie: this.jar.getCookieStringSync('https://plug.dj') }), body: { csrf: csrfToken, accessToken: store(this)._auththenticationInfo.facebook.accessToken, userID: store(this)._auththenticationInfo.facebook.userID } }) .then(handleLogin) .catch((fbError) => { process.nextTick(() => { handleErrors.error(fbError.stack || fbError, callback); }); }); }; pRetry(fbLogin, { forever: true, randomize: true, maxTimeout: 10000 }) .catch(handleError); } else { const authLogin = (loginAttempts) => { if (Object.is(loginAttempts % 5, 0)) this.logger.debug('PlugAPI', `Attempt #${loginAttempts} to login to plug.dj`); return got(`${this.baseUrl}/_/auth/login`, { json: true, method: 'POST', headers: Object.assign(store(this)._headers, { cookie: this.jar.getCookieStringSync('https://plug.dj') }), body: { csrf: csrfToken, email: store(this)._auththenticationInfo.email, password: store(this)._auththenticationInfo.password } }) .then(handleLogin) .catch((err2) => { process.nextTick(() => { if (err2 && err2.statusCode) { if (err2.statusCode >= 400 && err2.statusCode < 500) { if (Object.is(err2.statusCode, 400)) { handleErrors.error(`Login Error: Missing email or password | HTTP Status: ${err2.message}`, loginCallback); } else if (Object.is(err2.statusCode, 401)) { handleErrors.error(`Login Error: Incorrect email or password | HTTP Status: ${err2.message}`, loginCallback); } else if (Object.is(err2.statusCode, 403)) { handleErrors.error(`Login Error: Incorrect CSRF Token. | HTTP Status: ${err2.message}`, loginCallback); } } else { handleErrors.error(err2.stack || err2, loginCallback); } } }); }); }; pRetry(authLogin, { forever: true, randomize: true, maxTimeout: 10000 }) .catch(handleError); } }); }; pRetry(login, { forever: true, randomize: true, maxTimeout: 10000 }) .catch(handleError); } if (!Object.is(typeof loginCallback, 'function')) { const deasync = require('deasync'); // Wait until the session is set while (loggingIn) { // eslint-disable-line no-unmodified-loop-condition deasync.sleep(100); } } }; /** * Get the auth code for the user * @param {Function} authCallback Callback function * @private */ store(this)._getAuthCode = (authCallback) => { const getAuthCode = () => { store(this)._headers.cookie = this.jar.getCookieStringSync('https://plug.dj'); return got(`${this.baseUrl}/_/auth/token`, { headers: store(this)._headers, json: true }).then((body) => { return authCallback(body.body.data[0]); }); }; pRetry(getAuthCode, { randomize: true, forever: true, maxTimeout: 10000 }) .catch(console.error); }; /** * Handling incoming messages. * Emitting the correct events depending on commands, mentions, etc. * @param {Object} messageData plug.DJ message event data * @private */ store(this)._receivedChatMessage = (messageData) => { if (!store(this)._initialized || messageData.from == null) return; let i; const mutedUser = store(this)._room.isMuted(messageData.from.id); const prefixChatEventType = (mutedUser && !this.mutedTriggerNormalEvents ? 'muted:' : ''); if (messageData.message.startsWith(this.commandPrefix) && (this.processOwnMessages || !Object.is(messageData.from.id, store(this)._room.getSelf().id))) { const cmd = messageData.message.substr(this.commandPrefix.length).split(' ')[0]; messageData.command = cmd; messageData.args = messageData.message.substr(this.commandPrefix.length + cmd.length + 1); const random = Math.ceil(Math.random() * 1E10); // Arguments if (Object.is(messageData.args, '')) { messageData.args = []; } else { // Mentions => Mention placeholder let lastIndex = -1; let allUsers = store(this)._room.getUsers(); if (allUsers.length > 0) { allUsers = allUsers.sort((a, b) => { if (Object.is(a.username.length, b.username.length)) { return 0; } return a.username.length < b.username.length ? -1 : 1; }); for (const user of allUsers) { lastIndex = messageData.args.toLowerCase().indexOf(user.username.toLowerCase()); if (lastIndex > -1) { messageData.args = `${messageData.args.substr(0, lastIndex).replace('@', '')}%MENTION-${random}-${messageData.mentions.length}% ${messageData.args.substr(lastIndex + user.username.length + 1)}`; messageData.mentions.push(user); } } } messageData.args = messageData.args.split(' ').filter((item) => item != null && !Object.is(item, '')); for (i = 0; i < messageData.args.length; i++) { if (store(this)._isNumber(messageData.args[i])) { messageData.args[i] = Number(messageData.args[i]); } } } // Mention placeholder => User object if (messageData.mentions.length > 0) { for (i = 0; i < messageData.mentions.length; i++) { const atIndex = messageData.args.indexOf(`@%MENTION-${random}-${i}%`); const normalIndex = messageData.args.indexOf(`%MENTION-${random}-${i}%`); if (normalIndex > -1) { messageData.args[normalIndex] = messageData.mentions[i]; } if (atIndex > -1) { messageData.args[atIndex] = messageData.mentions[i]; } } } // Pre command handler if (Object.is(typeof this.preCommandHandler, 'function') && Object.is(this.preCommandHandler(messageData), false)) return; // Functions messageData.respond = function() { const message = Array.prototype.slice.call(arguments).join(' '); return this.sendChat(`@${messageData.from.username} ${message}`); }.bind(this); messageData.respondTimeout = function() { const args = Array.prototype.slice.call(arguments); const timeout = parseInt(args.splice(args.length - 1, 1), 10); const message = args.join(' '); return this.sendChat(`@${messageData.from.username} ${message}`, timeout); }.bind(this); messageData.havePermission = (permission, permissionCallback) => { if (permission == null) { permission = PlugAPI.ROOM_ROLE.NONE; } if (this.havePermission(messageData.from.id, permission)) { if (Object.is(typeof permissionCallback, 'function')) { return permissionCallback(true); } return true; } if (Object.is(typeof permissionCallback, 'function')) { return permissionCallback(false); } return false; }; messageData.isFrom = (ids, success, failure) => { if (Object.is(typeof ids, 'string') || Object.is(typeof ids, 'number')) { ids = [ids]; } if (ids == null) { if (Object.is(typeof failure, 'function')) { return failure(); } return false; } const isFrom = ids.indexOf(messageData.from.id) > -1; if (isFrom && Object.is(typeof success, 'function')) { return success(); } else if (!isFrom && Object.is(typeof failure, 'function')) { return failure(); } return isFrom; }; if (!mutedUser) { this.emit(prefixChatEventType + PlugAPI.events.CHAT_COMMAND, messageData); this.emit(`${prefixChatEventType}${PlugAPI.events.CHAT_COMMAND}:${cmd}`, messageData); if (this.deleteCommands) { if (!this.deleteMessageBlocks) { if (!(Object.is(store(this)._lastMessageUID, messageData.raw.uid) || Object.is(store(this)._lastMessageType, messageData.type))) { this.moderateDeleteChat(messageData.raw.cid); } } else { this.moderateDeleteChat(messageData.raw.cid); } } } } else if (Object.is(messageData.type, 'emote')) { this.emit(`${prefixChatEventType}${PlugAPI.events.CHAT}:emote`, messageData); } store(this)._lastMessageUID = messageData.raw.uid; store(this)._lastMessageType = messageData.type; this.emit(prefixChatEventType + PlugAPI.events.CHAT, messageData); this.emit(`${prefixChatEventType}${PlugAPI.events.CHAT}:${messageData.raw.type}`, messageData); if (!Object.is(store(this)._room.getSelf(), null) && messageData.message.indexOf(`@${store(this)._room.getSelf().username}`) > -1) { this.emit(`${prefixChatEventType}${PlugAPI.events.CHAT}:mention`, messageData); } }; /** * Sends in a websocket event to plug's servers * @param {String} type The Event type to send in * @param {String} data The data to send in. * @private */ store(this)._sendEvent = (type, data) => { if (store(this)._ws != null && Object.is(store(this)._ws.connection.readyState, WebSocket.OPEN)) { store(this)._ws.send(JSON.stringify({ a: type, p: data, t: DateUtilities.getServerEpoch() })); } }; /** * Initialize the room with the room state data * @param {Object} data the room state Data * @param {Function} roomCallback Callback function * @private */ store(this)._initRoom = (data, roomCallback) => { store(this)._room.reset(); store(this)._room.setRoomData(data); store(this)._queueREST('GET', endpoints.HISTORY, undefined, store(this)._room.setHistory.bind(store(this)._room)); this.emit(PlugAPI.events.ADVANCE, { currentDJ: store(this)._room.getDJ(), djs: store(this)._room.getDJs(), lastPlay: { dj: null, media: null, score: null }, media: store(this)._room.getMedia(), startTime: store(this)._room.getStartTime(), historyID: store(this)._room.getHistoryID() }); this.emit(PlugAPI.events.ROOM_JOIN, data.meta.name); store(this)._initialized = true; roomCallback(); }; /* * The handling of incoming messages from the Plug.DJ socket server. * If any cases are returned instead of breaking (stopping the emitting to the user code) it MUST be commented just before returning. * @param {Object} msg Socket events sent in from plug.dj * @private */ store(this)._messageHandler = (msg) => { /** * Event type * @private * @type {PlugAPI.events} */ const type = msg.a; /** * Data for the event * Will lookup in EventObjectTypes for possible converter function * @private * @type {*} */ let data = EventObjectTypes[msg.a] != null ? EventObjectTypes[msg.a](msg.p, store(this)._room) : msg.p; let i; const reconnect = (isAck) => { if (store(this)._room.getRoomMeta().slug) { this.logger.warn('PlugAPI', `${isAck ? 'Reconnecting' : 'Reconnecting because of KILL_SESSION (logging in more than once on plug.dj)'}`); this.close(true); if (isAck) { store(this)._performLoginCredentials(); } this.connect(store(this)._room.getRoomMeta().slug); } }; switch (type) { case 'ack': if (!Object.is(data, '1')) { reconnect(true); return; } const slug = store(this)._connectingRoomSlug ? store(this)._connectingRoomSlug : store(this)._room.getRoomMeta().slug; store(this)._queueREST('GET', endpoints.USER_INFO, null, (err, userData) => { if (err) throw new Error(`Error Obtaining user info. ${err}`); store(this)._room.setSelf(userData[0]); store(this)._joinRoom(slug, () => { this.logger.success('plug.dj Socket Server', chalk`{green Connected as a ${this.guest ? 'guest' : 'user'}}`); }); }); // This event should not be emitted to the user code. return; case PlugAPI.events.ADVANCE: // Add information about lastPlay to the data data.lastPlay = { dj: store(this)._room.getDJ(), media: store(this)._room.getMedia(), score: store(this)._room.getRoomScore() }; store(this)._room.advance(data); store(this)._queueREST('GET', endpoints.HISTORY, undefined, store(this)._room.setHistory.bind(store(this)._room)); // Override parts of the event data with actual User objects data.currentDJ = store(this)._room.getDJ(); data.djs = store(this)._room.getDJs(); break; case PlugAPI.events.CHAT: // If over limit, remove the first item if (store(this)._chatHistory.push(data) > 512) store(this)._chatHistory.shift(); store(this)._receivedChatMessage(data); // receivedChatMessage will emit the event with correct chat object and correct event types return; case PlugAPI.events.CHAT_DELETE: for (i = 0; i < store(this)._chatHistory.length; i++) { if (store(this)._chatHistory[i] && Object.is(store(this)._chatHistory[i].id, data.c)) { store(this)._chatHistory.splice(i, 1); } } break; case PlugAPI.events.ROOM_LONG_DESCRIPTION_UPDATE: store(this)._room.setRoomLongDescription(data.d); break; case PlugAPI.events.ROOM_SHORT_DESCRIPTION_UPDATE: store(this)._room.setRoomDescription(data.d); break; case PlugAPI.events.ROOM_DESCRIPTION_UPDATE: store(this)._room.setRoomDescription(data.description); break; case PlugAPI.events.ROOM_NAME_UPDATE: store(this)._room.setRoomName(data.name); break; case PlugAPI.events.ROOM_WELCOME_UPDATE: store(this)._room.setRoomWelcome(data.welcome); break; case PlugAPI.events.USER_JOIN: store(this)._room.addUser(data); break; case PlugAPI.events.USER_LEAVE: let userData = store(this)._room.getUser(data); if (userData == null || Object.is(data, 0)) { userData = { id: data, guest: Object.is(data, 0) }; } store(this)._room.removeUser(data); this.emit(type, userData); // This is getting emitted with the full user object instead of only the user ID return; case PlugAPI.events.USER_UPDATE: store(this)._room.updateUser(data); this.emit(type, this.getUser(data.i)); // This is getting emitted with the full user object instead of only the user ID return; case