Source: helpers/SocketConnection.js

/* eslint-disable no-case-declarations */

let WebSocket = require('ws'),
    EventEmitter = require('events'),
    Limiter = require('rolling-rate-limiter'),
    Constants = require('../Constants'),
    Message = require('../structures/Message'),
    User = require('../structures/User'),
    Playback = require('../structures/Playback'),
    Ban = require('../structures/Ban');

class SocketConnection extends EventEmitter {
    constructor(client) {
        super();
        this.status = 'disconnected';
        this.ws = null;
        this.client = client;
        this.chatQueue = [];
        this.ticking = false;
        this._reconnectTries = 1;
        this.limiter = Limiter({
            interval: 1000,
            maxInInterval: 3,
            minDifference: 100
        });

        this.freeze = 0;
    }

    connect() {
        return new Promise((resolve, reject) => {
            if (['connecting', 'connected', 'authenticating'].includes(this.status))return reject(new Error('Connection is established or in the process of beeing established.'));
            this.status = 'connecting';
            this.client._requestAgent.request('get', Constants.endpoints.token).then((body) => this.initializeWS(body.data[0])).catch(err => {
                this.status = 'disconnected';
                reject(err);
            });
        });
    }

    initializeWS(token) {
        this.ws = new WebSocket(Constants.socket, {origin: "https://plug.dj"});
        this.ws.on('open', () => {
            this.status = 'authenticating';
            this.sendWS(Constants.packetTypes.auth, token);
        });
        this.ws.on('error', (err) => {
            /**
             * Emitted when the socket errors
             * @event Client#socketError
             */
            this.client.emit('socketError', err);
            if (this.client._options.autoReconnect) setTimeout(() => this.initializeWS(token), (this._reconnectTries * 1000 + (Math.random() * 100)));
        });
        this.ws.on('close', (code, reason) => {
            /**
             * Emitted when the socket is closed
             * @event Client#socketClose
             * @property {Number} [data.code] The close code.
             * @property {String} [data.reason] A human-readable close reason.
             */
            this.client.emit('socketClose', {code, reason});
            this.status = 'disconnected';
        });
        this.ws.on('message', (data => {
            if (data === 'h')return;
            try {
                data = JSON.parse(data);
            } catch (e) {
                return this.client.emit('error', new Error('Received invalid data from plug.dj: ' + data));
            }
            for (let e of data) {
                /**
                 * Emits the raw packages.
                 * @event Client#rawWS
                 * @property {Object} data The data received from plug.dj
                 */
                this.client.emit('rawWS', e);
                if (!this.client.ready)break;
                if (e.s !== this.client.room)break;
                switch (e.a) {
                    default:
                        /**
                         * Emitted when an unknown package is received from plug. This can indicate an outdated version of plugging-you-in
                         * @event Client#unknown
                         */
                        this.client.emit('unknown', e);
                        if (this.client._options.updateNotification) this.client.emit('warn', 'You are probably using an outdated version of plugging-you-in. Please consider updating this module.');
                        break;
                    case 'ack':
                        /**
                         * Emitted when the socket connection is up.
                         * @event Client#connect
                         * @property {String} room The room the client is currently in
                         */
                        this.client.emit('connect', e.s);
                        this.status = 'connected';
                        break;

                    case 'chat':
                        /**
                         * Emitted when a chat message is received
                         * @event Client#chat
                         * @property {Message} message The received message
                         */
                        this.client.emit('chat', this.client.messages.add(new Message(e.p, this.client)));
                        break;
                    case 'chatDelete':
                        /**
                         * Emitted when a moderator deletes a chat message
                         * @event Client#chatDelete
                         * @property {Message} message The deleted message (can be null if message is not cached)
                         * @property {User} The user who deleted the message
                         */
                        this.client.emit('chatDelete', this.client.deletedMessages.add(this.client.messages.remove({id: e.p.c}) || {id: e.p.c}) || null, this.client.users.get(e.p.mi));
                        break;

                    case 'userJoin':
                        /**
                         * Emitted when a guest joins the room
                         * @event Client#guestJoin
                         */
                        if (!e.p)return this.client.emit('guestJoin');
                        /**
                         * Emitted when an users joins the room
                         * @event Client#userJoin
                         * @property {User} user The joined user
                         */
                        this.client.emit('userJoin', (this.client.offlineUsers.get(e.p.id) || this.client.users.add(new User(e.p))));
                        break;
                    case 'friendJoin':
                        /**
                         * Emitted when an users joins the room
                         * @event Client#friendJoin
                         * @property {User} user The joined user
                         */
                        this.client.emit(`${this.client._options.useFriends ? 'friend' : 'user'}Join`, (this.client.offlineUsers.get(e.p.id) || this.client.users.add(new User(e.p))));
                        break;
                    case 'userLeave':
                        /**
                         * Emitted when a guest leaves the room
                         * @event Client#guestLeave
                         */
                        if (!e.p)return this.client.emit('guestLeave');
                        /**
                         * Emitted when an user leaves the room.
                         * @event Client#userLeave
                         * @property {User} user The left user
                         */
                        this.client.emit('userLeave', this.client.offlineUsers.add(this.client.users.remove({id: e.p})));
                        break;

                    case 'advance':
                        let oldPlayback = null;
                        if (this.client.playback) {
                            this.client.history.push(this.client.playback);
                            oldPlayback = this.client.playback;
                        }
                        /**
                         * Emitted when a new song is played
                         * @event Client#advance
                         * @property {Playback} playback The current playback
                         * @property {Playback} oldPlayback The playback before.
                         */
                        this.client.emit('advance', this.client.playback = new Playback(e.p, this.client), oldPlayback);
                        let update = this.client.room.queue.update(e.p);
                        this.client.emit('queueUpdate', update.newQueue, update.oldQueue);
                        break;

                    case 'djListUpdate':
                        /**
                         * Emitted when the waitlist changes
                         * @event Client#queueUpdate
                         * @property {Array<User>} queue The new queue as an array of users
                         * @property {Array<User>} oldQueue The old queue
                         */
                        let {oldQueue, newQueue} = this.client.room.queue.update(e.p);
                        this.client.emit('queueUpdate', newQueue, oldQueue);
                        break;
                    case 'djListCycle':
                        /**
                         * Emitted when the cycle-mode gets changed
                         * @event Client#cycleChange
                         * @property {User} user The user who changed the cycle mode
                         * @property {Boolean} state The new cycle-mode
                         */
                        this.client.emit('cycleChange', this.client.users.get(e.p.mi), this.client.room.booth._setCycle(e.p.c));
                        break;
                    case 'djListLocked':
                        /**
                         * Emitted when the queue gets locked/unlocked/cleared
                         * @event Client#lockChange
                         * @property {User} user The user who took the action
                         * @property {Boolean} lock The new lock-state of the queue
                         * @property {Boolean} clear Wheter the queue got cleared or not
                         */
                        this.client.emit('lockChange', this.client.users.get(e.p.mi), this.client.booth._setLock(e.p.f), e.p.c);
                        break;

                    case 'vote':
                        /**
                         * Emitted when someone votes on a song
                         * @event Client#vote
                         * @property {User} user The user who voted
                         * @property {Number} vote The vote, 1 for woot, -1 for meh
                         */
                        this.client.emit('vote', this.client.users.get(e.p.i), e.p.v);
                        if (this.client.playback) this.client.playback._addVote(e.p.i, e.p.v);
                        else this.client.emit('debug', 'Couldn\'t add vote to playback since playback did not exist.');
                        break;
                    case 'grab':
                        /**
                         * Emitted when an user grabs a song
                         * @event Client#grab
                         * @property {User} user The user who grabbed the song
                         */
                        this.client.emit('grab', this.client.users.get(e.p));
                        break;

                    case 'floodAPI':
                        /**
                         * Emitted when too many api requests are fired. You don't need to handle it if you leave `options.requestFreeze` at the default value, however all actions will be stopped for 10 seconds
                         * @event Client#floodApi
                         */
                        this.client.emit('floodApi');
                        if (!this.client._options.ignoreRateLimits) this.client._requestAgent.freezeRequests();
                        break;
                    case 'floodChat':
                        /**
                         * Emitted when you are sending too many chat messages and plugging-you-in isn't good enough at limiting you. Plugging-you-in will handle this by stopping to send messages entirely for a few seconds
                         * @event Client#floodChat
                         */
                        this.client.emit('floodChat');
                        if (!this.client._options.ignoreRateLimits) this.freezeRequests();
                        break;
                    case 'rateLimit':
                        /**
                         * Emitted when chat enters slow mode.
                         * @event Client#slowMode
                         */
                        this.client.emit('slowMode');
                        break;

                    case 'earn':
                        this.client.user.plugPoints = e.p.pp || this.client.user.plugPoints;
                        this.client.user.xp = e.p.xp || this.client.user.xp;
                        this.client.user.level = e.p.level || this.client.user.level;
                        /**
                         * Emitted when the bot earns experience/plug points or levels up
                         * @event Client#earn
                         * @property {Object} earn
                         * @property {Number} earn.xp The new amount of xp
                         * @property {Number} earn.pp The new amount of plug points
                         * @property {Number} earn.level The new level
                         */
                        this.client.emit('earn', e.p);
                        break;
                    case 'gifted':
                        /**
                         * Emitted when someone gifts plug points to someone
                         * @event Client#gifted
                         * @property {User} sender The user who sent the gift
                         * @property {User} receiver The user who received the gift
                         */
                        this.client.emit('gifted', this.client.users.find(user => user.username === e.p.s) || null, this.client.users.find(user => user.username === e.p.r) || null);
                        break;

                    case 'ban':
                        /**
                         * Emitted when you are banned from a community
                         * @event Client#ban
                         * @property {Ban} ban The ban object
                         */
                        this.client.emit('ban', new Ban(e.p, this.client));
                        break;

                    case 'modAddDJ':
                        /**
                         * Emitted when a moderator adds someone to the queue. This also fires a queueUpdate-event
                         * @event Client#addDj
                         * @property {User} user The user who got added
                         * @property {User} moderator The Moderator
                         */
                        this.client.emit('addDj', this.client.users.find(user => user.username === e.p.t) || null, this.client.users.get(e.p.mi));
                        break;
                    case 'modBan':
                        /**
                         * Emitted when a moderator bans someone from the room
                         * @event Client#userBan
                         * @property {Ban} ban The ban
                         * @property {User} moderator
                         */
                        this.client.emit('userBan', new Ban(e.p), this.client.users.get(e.p.mi));
                        break;
                    case 'modMoveDJ':
                        /**
                         * Emitted when a moderator moves someone in the queue. This also fires a queueUpdate-event
                         * @event Client#moveUser
                         * @property {User} user The moved user
                         * @property {User} moderator
                         * @property {Number} newPosition
                         * @property {Number} oldPosition
                         */
                        this.client.emit('moveUser', this.client.users.find(user => user.username === e.p.u) || null, this.client.users.get(e.p.mi), e.p.n, e.p.o);
                        break;
                    case 'modMute':
                        //todo
                        break;
                    case 'modRemoveDJ':
                        //todo
                        break;
                    case 'modSkip':
                        /**
                         * Emitted when a moderator skips a song
                         * @event Client#skip
                         * @property {User} moderator
                         */
                        this.client.emit('skip', this.client.users.get(e.p.mi));
                        break;
                    case 'modStaff':
                        //todo
                        break;

                    case 'friendAccept':
                        //todo
                        break;
                    case 'friendRequest':
                        //todo
                        break;

                    case 'killSession':
                        /**
                         * Emitted when the socket server kills the session
                         * @event Client#killSession
                         */
                        this.client.emit('killSession');
                        /**
                         * Emitted when the client encounters an error which it cannot handle itself
                         * @event Client#error
                         * @property {String|Error} error The error or an error message.
                         * @property {Error} [err] An error object
                         */
                        if (this.client._options.autoReconnect) this.connect().catch(err => this.client.emit('error', 'Unable to reconnect to the socket after the session was killed.', err));
                        break;

                    case 'levelUp':
                        this.client.user.level = e.p || this.client.user.level;
                        /**
                         * Emitted when the logged in account levels up
                         * @event Client#levelUp
                         * @property {Number} newLevel
                         */
                        this.client.emit('levelUp', e.p);
                        break;

                    case 'playlistCycle':
                        //todo
                        break;

                    case 'plugMaintenance':
                        /**
                         * Emitted when plug.dj goes into maintenance mode
                         * @event Client#maintenance
                         */
                        this.client.emit('maintenance');
                        //todo add handler for autoreconnect
                        break;

                    case 'plugMaintenanceAlert':
                        /**
                         * Emitted when plug.dj is about to go into maintenance mode
                         * @event Client#maintenanceAlert
                         * @property {Number} time time til maintenance mode in minutes
                         */
                        this.client.emit('maintenanceAlert', e.p);
                        break;

                    case 'plugMessage':
                        /**
                         * Emitted when plug.dj sends a broadcast
                         * @event Client#plugMessage
                         * @property {String} message
                         */
                        this.client.emit('plugMessage', e.p);
                        break;

                    case 'plugUpdate':
                        /**
                         * Emitted when plig.dj gets updated.
                         * @event Client#plugUpdate
                         */
                        this.client.emit('plugUpdate');
                        break;

                    case 'roomDescription':
                        //todo
                        break;

                    case 'roomMinChatLevelUpdate':
                        //todo
                        break;

                    case 'roomNameUpdate':
                        //todo
                        break;

                    case 'roomWelcomeUpdate':
                        //todo
                        break;

                    case 'skip':
                        /**
                         * Emitted when someone skips himself
                         * @event Client#selfSkip
                         * @property {User} user
                         */
                        this.client.emit('selfSkip', this.client.users.get(e.p));
                        break;
                }
            }
        }));
    }

    sendChat(input) {
        this.chatQueue.push(input);
        this._chatTick();
    }

    _chatTick() {
        if (this.ticking)return;
        if (!this.chatQueue.length)return;
        this.ticking = true;
        this.processChatQueue();
    }

    processChatQueue() {
        setTimeout(() => {
            this.processChatMessage(this.chatQueue.shift());
            if (this.chatQueue.length) this.processChatQueue();
            else this.ticking = false;
        }, this.limiter() + this.freeze);
        this.freeze = 0;
    }

    processChatMessage(msg) {
        this.sendWS(Constants.packetTypes.chat, msg.message);
        msg.resolve();
    }

    sendWS(type, data) {
        this.ws.send(JSON.stringify({a: type, p: data, t: Math.floor(Date.now() / 1000)}));
    }

    freezeRequests() {
        return this.freeze = this.client._options.chatFreeze;
    }
}

module.exports = SocketConnection;