Gielert/NoodleJS

View on GitHub
src/Client.js

Summary

Maintainability
C
1 day
Test Coverage
const EventEmitter = require('events').EventEmitter
const Promise = require('bluebird')
const Connection = require('./Connection')
const Util = require('./Util')
const Constants = require('./Constants')
const ServerSync = require('./handlers/ServerSync')
const UserState = require('./handlers/UserState')
const UserRemove = require('./handlers/UserRemove')
const ChannelState = require('./handlers/ChannelState')
const ChannelRemove = require('./handlers/ChannelRemove')
const TextMessage = require('./handlers/TextMessage')
const Collection = require('./structures/Collection')
const Dispatcher = require('./voice/Dispatcher')

/**
 * The main class for interacting with the Mumble server
 * @extends EventEmitter
 */
class Client extends EventEmitter {

    /**
     * @param  {ClientOptions} [options] Options for the client
     */
    constructor(options = {}) {
        super()

        /**
         * The options the client is instantiated with
         * @type {ClientOptions}
         */
        this.options = Util.mergeDefault(Constants.DefaultOptions, options)

        /**
         * The connection to the Mumble server
         * @type {Connection}
         * @private
         */
        this.connection = new Connection(this.options)
        this.connection.on('error', (error) => {
            this.emit('error', error)
        })

        this.connection.on('connected', () => {
            this.connection.writeProto('Version', {
                version: Util.encodeVersion(1, 3, 0),
                release: 'NoodleJS Client',
                os: 'NodeJS',
                os_version: process.version
            })
            this.connection.writeProto('Authenticate', {
                username: this.options.name,
                password: this.options.password,
                opus: true,
                tokens: this.options.tokens
            })
            this._pingRoutine()
        })

        /**
         * All of the {@link Channel} objects that are synced with the server,
         * mapped by their IDs
         * @type {Collection<id, Channel>}
         */
        this.channels = new Collection()

        /**
         * All of the {@link User} objects that are synced with the server,
         * mapped by their sessions
         * @type {Collection<session, User>}
         */
        this.users = new Collection()

        /**
         * The {@link Dispatcher} for the voiceConnection
         * @type {Dispatcher}
         */
        this.voiceConnection = new Dispatcher(this)

        const serverSync = new ServerSync(this)
        const userState = new UserState(this)
        const userRemove = new UserRemove(this)
        const channelState = new ChannelState(this)
        const channelRemove = new ChannelRemove(this)
        const textMessage = new TextMessage(this)

        this.connection.on('ServerSync', data => serverSync.handle(data))
        this.connection.on('UserState', data => userState.handle(data))
        this.connection.on('UserRemove', data => userRemove.handle(data));
        this.connection.on('ChannelRemove', data => channelRemove.handle(data));
        this.connection.on('ChannelState', data => channelState.handle(data))
        // this.connection.on('CryptSetup', data => console.log(data))
        this.connection.on('TextMessage', data => textMessage.handle(data));

        this.connection.on('voiceData', (voiceData) => {
            this.emit('voiceData', voiceData)
        })
    }

    /**
     * The ping routine for the client to keep the connection alive
     * @private
     */
    _pingRoutine() {
        this.ping = setInterval(() => {
            this.connection.writeProto('Ping', {timestamp: Date.now()})
        }, 15000)
    }

    connect() {
        this.connection.connect()
    }

    /**
     * Sends a message to the {@link Channel} where the client is currently
     * connected
     * @param  {String} message   The message to be sent
     * @param  {Boolean} recursive If the message should be sent down the tree
     * @return {Promise<TextMessage>}
     */
    sendMessage(message, recursive) {
        return this.user.channel.sendMessage(message, recursive)
    }

    /**
     * Switches to another channel
     * @param  {Number} id         The id of the channel to switch to
     * @return {Promise<any>}
     */
    switchChannel(id) {
        if (this.channels.has(id)) {
            return this.connection.writeProto('UserState', {session: this.user.session, actor: this.user.session , channelId: id})
        } else {
            return Promise.reject('ChannelId unknown')
        }
    }

    /**
     * Starts listening to another channel without joining it
     * @param  {Number} id         The id of the channel to start listening to
     * @return {Promise<any>}
     */
    startListeningToChannel(id) {
        if (this.channels.has(id)) {
            return this.connection.writeProto('UserState', {session: this.user.session, listeningChannelAdd: [id]})
        } else {
            return Promise.reject('ChannelId unknown')
        }
    }

    /**
     * Stops listening to another channel
     * @param  {Number} id         The id of the channel to stop listening to
     * @return {Promise<any>}
     */
    stopListeningToChannel(id) {
        if (this.channels.has(id)) {
            return this.connection.writeProto('UserState', {session: this.user.session, listeningChannelRemove: [id]})
        } else {
            return Promise.reject('ChannelId unknown')
        }
    }

    /**
     * Set up a voiceTarget to be optionally used when sending voice data
     * @param  {Number} targetId       The id for this voiceTarget. Must be between 4 and 30
     * @param  {Array.<Number>} userIds  Array of user sessions to send to. Can be empty.
     * @param  {Number} channelId      ChannelId to send to. Ignored when userIds is not empty.
     * @return {Promise<any>}
     */
    setupVoiceTarget(targetId, userIds, channelId) {
        if (targetId < 4 || targetId > 30) {
            return Promise.reject('targetId must be between 3 and 30')
        }

        if (userIds.length) {
            for (var idx in userIds) {
                if (!this.users.has(userIds[idx])) {
                    return Promise.reject('userId ' + userIds[idx] + ' unknown')
                }
            }
            return this.connection.writeProto('VoiceTarget', {id: targetId, targets: [{session: userIds}]})
        } else {
            if (!this.channels.has(channelId)) {
                return Promise.reject('ChannelId unknown')
            }
            return this.connection.writeProto('VoiceTarget', {id: targetId, targets: [{channelId: channelId}]})
        }
    }

    mute() {
        this.connection.writeProto('UserState', {session: this.user.session, actor: this.user.session , selfMute: true})
    }

    unmute() {
        this.connection.writeProto('UserState', {session: this.user.session, actor: this.user.session, selfMute: false})
    }

    destroy() {
        try {
            clearInterval(this.ping)
            this.connection.close()
            return Promise.resolve()
        } catch(e) {
            return Promise.reject(e)
        }
    }

}

module.exports = Client