<commit_before>/* jshint esversion: 6 */
/* jshint node: true */
/* jshint sub:true */
// This code uses ES2015 syntax that requires at least Node.js v4.
// For Node.js ES2015 support details, reference http://node.green/

"use strict";
var request = require('request-promise');
var OpenT2T = require('opent2t').OpenT2T;

/**
* This translator class implements the "Hub" interface.
*/
class Translator {
    constructor(accessToken) {
        this._accessToken = accessToken;

        this._baseUrl = "https://api.wink.com";
        this._devicesPath = '/users/me/wink_devices';

        this._name = "Wink Hub"; // TODO: Can be pulled from OpenT2T global constants. This information is not available, at least, on wink hub.
    }

    /**
     * Get the hub definition and devices
     */
    get(expand) {
        return getPlatforms(expand);
    }

    /**
     * Get the list of devices discovered through the hub.
     */
    getPlatforms(expand) {
        return this._makeRequest(this._devicesPath, 'GET').then((response) => {
            var devices = response.data;

            var platformPromises = [];
            devices.forEach((winkDevice) => {
                // get the opent2t schema and translator for the wink device
                var opent2tInfo = this._getOpent2tInfo(winkDevice);

                // Do not return the physical hub device, nor any devices for which there are not translators.
                // Additionally, do not return devices that have been marked as hidden by Wink (hidden_at is a number)
                // This state is used by third party devices (such as a Nest Thermostat) that were connected to a
                // Wink account and then removed.  Wink keeps the connection, but marks them as hidden.
                if ((winkDevice.model_name !== 'HUB')  &&
                    typeof opent2tInfo !== 'undefined' &&
                    (winkDevice.hidden_at == undefined || winkDevice.hidden_at == null))
                {
                    // set the opent2t info for the wink device
                    var deviceInfo = {};
                    deviceInfo.id = this._getDeviceId(winkDevice);

                    // Create a translator for this device and get the platform information, possibly expanded
                    platformPromises.push(OpenT2T.createTranslatorAsync(opent2tInfo.translator, {'deviceInfo': deviceInfo, 'hub': this})
                                    .then((translator) => {
                                        return OpenT2T.invokeMethodAsync(translator, opent2tInfo.schema, 'get', [expand])
                                            .then((platformResponse) => {
                                                return platformResponse;
                                            });
                                    }));
                }
            });

             return Promise.all(platformPromises)
                    .then((platforms) => {
                        var toReturn = {};
                        toReturn.schema = "opent2t.p.hub";
                        toReturn.platforms = platforms;
                        return toReturn;
                    });
        });
    }

    /**
     * Get the name of the hub.  Ties to the n property from oic.core
     */
    getN() {
        return this._name;
    }

    /**
     * Gets device details (all fields), response formatted per http://docs.winkapiv2.apiary.io/
     */
    getDeviceDetailsAsync(deviceType, deviceId) {

        // build request URI
        var requestPath = '/' + deviceType + '/' + deviceId;

        // Make the async request
        return this._makeRequest(requestPath, 'GET');
    }

    /**
     * Puts device details (all fields) payload formatted per http://docs.winkapiv2.apiary.io/
    */
    putDeviceDetailsAsync(deviceType, deviceId, putPayload) {

        // build request path and body
        var requestPath = '/' + deviceType + '/' + deviceId;
        var putPayloadString = JSON.stringify(putPayload);

        // Make the async request
        return this._makeRequest(requestPath, 'PUT', putPayloadString);
    }

    /**
     * Subscribes to a Wink pubsubhubbub feed.  This function is designed to be called twice.
     * The first call will contain just the callbackUrl, which will be subscribed for postbacks
     * from the Wink cloud service.
     * The second call will contain the request, and response for a GET to the callbackUrl that was
     * provided in order to complete the verification of the subscription.
     *
     * The expected sequence is as follows:
     * - Call subscribe with a callback url, get a response.
     * - Http server running at the callback url receives a GET request, this request and a response string
     *      are passed to a 2nd call to subscribe() (callbackUrl is ignored in this case)
     * - Http server responds to the GET with the verificationResponseContent that was populated by subscribe
     * - Future POST calls to the callback url are passed to the device translator get*Uri function
     *
     * All calls to subscribe will return the expiration time of the subscription.  For Wink, this is
     * currently 24 hours.  Subsequent calls to subscribe will refresh the expiration time, and in the
     * case of Wink/Pubsubhubbub will not require verification.
     *
     * @param {string} deviceType - Device Type (e.g. 'thermostats')
     * @param {string|number} deviceId - Id for the specific device
     * @param {string} callbackUrl - Callback url for feed postbacks
     * @param {HttpRequest} verificationRequest - GET request received by the server at a previously subscribed
     *      callback URL.  This completes the verification half of the subscription.
     * @param {HttpResponse} verificationResponseContent - Content that should be provided in the response to the
     *      verification request as {'Content-Type': 'text/plain'}.  All responses should use code 200 unless an
     *      error is caught.
     * @returns {number} Object containing the subscription expiration time, and any content that
     *      needs to be included in a response to the original request.
     */
    _subscribe(deviceType, deviceId, callbackUrl, verificationRequest) {

        if (callbackUrl) {
            // Subscribe to notifications

            var requestPath = '/' + deviceType + '/' + deviceId + '/subscriptions';

            // Winks implementation of PubSubHubbub differs from the standard in that we do not need to provide
            // the topic, or mode on this request.  Topic is implicit from the URL (deviceType/deviceId), and
            // separate requests exist for mode (subscribe and unsubscribe vis POST/DELETE).

            // Additionally, subscriptions will expire after 24 hours (for now), and need to be refreshed
            // with another call to this function.

            var postPayload = {
                callback: callbackUrl,
            }

            var postPayloadString = JSON.stringify(postPayload);

            return this._makeRequest(requestPath, 'POST', postPayloadString).then((response) => {
                // Return the expiration time for the subscription
                return {
                    expiration: response.data.expires_at,
                    response: ""
                };
            })

        } else if (verificationRequest) {
            // Verify a subscription request by constructing the appropriate response

            var params = require('url').parse(verificationRequest.url, true, true);
            // This is a subscription validation, populate the response
            switch(params.query['hub.mode']) {
                case "denied":
                    // The subscription cannot be completed, access was denied.
                    // This is likely due to a bad access token.
                    throw new Error("Access denied");
                case "unsubscribe": // Could remove this, see below.
                case "subscribe":
                    // Successful subscription/unsubscription requires a response to contain the same
                    // challenge that was included in the request.
                    // Wink doesn't actually use hub.mode unsubscribe, and instead uses DELETE to perform the action
                    // with no verification.

                    // Verify that this subscription is for the correct topic
                    if (params.query['hub.topic'].endsWith(deviceType + '/' + deviceId)) {
                        return {
                            expiration: params.query['hub.lease_seconds'],
                            response: params.query['hub.challenge']
                        }
                    } else {
                        // There is a mistmatch.  This subscription doesn't match this device.
                        throw new Error("Subscription cannot be verified");
                    }
                default:
                    // Hub mode is unknown.
                    throw new Error("Unknown request");
            }
        }
    }

    /**
     * Unsubscribes from an existing Wink pubsubhubbub feed.  The subscription id for a
     * topic is not cached, so this results in 2 web calls:
     * - GET to return a list of subscriptions
     * - DELETE to remove the existing subscriptions.
     *
     * Wink's pubsubhubbub is slightly different than standard as it uses DELETE along
     * with a subscriptionID rather than another GET with a hub.mode of 'unsubscribe'
     *
     * @param {string} deviceType - Device Type (e.g. 'thermostats')
     * @param {string|number} deviceId - Id for the specific device
     * @param {string} callbackUrl - URL that will be unsubscribed
     * @returns {request} Promise that supplies the server response
     *
     */
    _unsubscribe(deviceType, deviceId, callbackUrl)
    {
        // Find the subscription ID for this callback URL
        return this._getSubscriptions(deviceType, deviceId).then((subscriptions) => {
            subscriptions.forEach((sub) => {
                // Find the subscription id for this callback URL and device
                if (sub.callback == callbackUrl &&
                    sub.topic.endsWith(deviceType + '/' + deviceId)) {
                    var requestPath = '/' + deviceType + '/' + deviceId + '/subscriptions/' + sub.subscription_id;

                    // Do the actual unsubscribe
                    return this._makeRequest(requestPath, 'DELETE').then(() => {
                        return {};
                    })
                }
            });

            // Subscription wasn't found so there's nothing to do
            return {};
        });
    }

    /**
     * Gets all subscriptions for a device.
     *
     * @param {string} deviceType - Device Type (e.g. 'thermostats')
     * @param {string|number} deviceId - Id for the specific device
     * @returns {request} Promise that supplies the list of subscriptions
     *
     * This is just a helper for tests and verification.
     */
    _getSubscriptions(deviceType, deviceId) {
        // GET /<deviceType>/<device Id>/subscriptions
        var requestPath = '/' + deviceType + '/' + deviceId + '/subscriptions';

        return this._makeRequest(requestPath, 'GET').then((response) => {
            return response.data;
        });
    }

    /**
     * Given the hub specific device, returns the opent2t schema and translator
    */
    _getOpent2tInfo(winkDevice) {
        // if (winkDevice.thermostat_id) {
        //     return {
        //         "schema": 'opent2t.p.thermostat',
        //         "translator": "opent2t-translator-com-wink-thermostat"
        //     };
        // }
        if (winkDevice.binary_switch_id) {
            return {
                "schema": 'opent2t.p.switch.binary',
                "translator": "opent2t-translator-com-wink-binaryswitch"
            };
        }
        else if (winkDevice.light_bulb_id) {
            return {
                "schema": 'opent2t.p.light',
                "translator": "opent2t-translator-com-wink-lightbulb"
            };
        }

        return undefined;
    }

    /**
     * Internal helper method which makes the actual request to the wink service
     */
    _makeRequest(path, method, content) {
        // build request URI
        var requestUri = this._baseUrl + path;

        // Set the headers
        var headers = {
            'Authorization': 'Bearer ' + this._accessToken.accessToken,
            'Accept': 'application/json'
        }

        if (content) {
            headers['Content-Type'] = 'application/json';
            headers['Content-length'] = content.length;
        }

        var options = {
            url: requestUri,
            method: method,
            headers: headers,
            followAllRedirects: true
        };

        if (content) {
            options.body = content;
        }

        // Start the async request
        return request(options)
            .then(function (body) {
                return JSON.parse(body);
            })
            .catch(function (err) {
                console.log("Request failed to: " + options.method + " - " + options.url);
                console.log("Error            : " + err.statusCode + " - " + err.response.statusMessage);
                // todo auto refresh in specific cases, issue 74
                throw err;
            });
    }

    /**
     * Wink has a semi-complicated way of getting the actual device id of a device.
     */
    _getDeviceId(winkDevice) {
        // get the device id
        var deviceId = winkDevice.light_bulb_id ||
            winkDevice.air_conditioner_id ||
            winkDevice.binary_switch_id ||
            winkDevice.shade_id ||
            winkDevice.camera_id ||
            winkDevice.doorbell_id ||
            winkDevice.eggtray_id ||
            winkDevice.garage_door_id ||
            winkDevice.cloud_clock_id ||
            winkDevice.lock_id ||
            winkDevice.dial_id ||
            winkDevice.alarm_id ||
            winkDevice.power_strip_id ||
            winkDevice.outlet_id ||
            winkDevice.piggy_bank_id ||
            winkDevice.deposit_id ||
            winkDevice.refrigerator_id ||
            winkDevice.propane_tank_id ||
            winkDevice.remote_id ||
            winkDevice.sensor_pod_id ||
            winkDevice.siren_id ||
            winkDevice.smoke_detector_id ||
            winkDevice.sprinkler_id ||
            winkDevice.thermostat_id ||
            winkDevice.water_heater_id ||
            winkDevice.scene_id ||
            winkDevice.condition_id ||
            winkDevice.robot_id;

        return deviceId;
    }
}

module.exports = Translator;<commit_msg>Fixing hub translator<commit_after>/* jshint esversion: 6 */
/* jshint node: true */
/* jshint sub:true */
// This code uses ES2015 syntax that requires at least Node.js v4.
// For Node.js ES2015 support details, reference http://node.green/

"use strict";
var request = require('request-promise');
var OpenT2T = require('opent2t').OpenT2T;

/**
* This translator class implements the "Hub" interface.
*/
class Translator {
    constructor(accessToken) {
        this._accessToken = accessToken;

        this._baseUrl = "https://api.wink.com";
        this._devicesPath = '/users/me/wink_devices';

        this._name = "Wink Hub"; // TODO: Can be pulled from OpenT2T global constants. This information is not available, at least, on wink hub.
    }

    /**
     * Get the hub definition and devices
     */
    get(expand) {
        return this.getPlatforms(expand);
    }

    /**
     * Get the list of devices discovered through the hub.
     */
    getPlatforms(expand) {
        return this._makeRequest(this._devicesPath, 'GET').then((response) => {
            var devices = response.data;

            var platformPromises = [];
            devices.forEach((winkDevice) => {
                // get the opent2t schema and translator for the wink device
                var opent2tInfo = this._getOpent2tInfo(winkDevice);

                // Do not return the physical hub device, nor any devices for which there are not translators.
                // Additionally, do not return devices that have been marked as hidden by Wink (hidden_at is a number)
                // This state is used by third party devices (such as a Nest Thermostat) that were connected to a
                // Wink account and then removed.  Wink keeps the connection, but marks them as hidden.
                if ((winkDevice.model_name !== 'HUB')  &&
                    typeof opent2tInfo !== 'undefined' &&
                    (winkDevice.hidden_at == undefined || winkDevice.hidden_at == null))
                {
                    // set the opent2t info for the wink device
                    var deviceInfo = {};
                    deviceInfo.id = this._getDeviceId(winkDevice);

                    // Create a translator for this device and get the platform information, possibly expanded
                    platformPromises.push(OpenT2T.createTranslatorAsync(opent2tInfo.translator, {'deviceInfo': deviceInfo, 'hub': this})
                                    .then((translator) => {
                                        return OpenT2T.invokeMethodAsync(translator, opent2tInfo.schema, 'get', [expand])
                                            .then((platformResponse) => {
                                                return platformResponse;
                                            });
                                    }));
                }
            });

             return Promise.all(platformPromises)
                    .then((platforms) => {
                        var toReturn = {};
                        toReturn.schema = "opent2t.p.hub";
                        toReturn.platforms = platforms;
                        return toReturn;
                    });
        });
    }

    /**
     * Get the name of the hub.  Ties to the n property from oic.core
     */
    getN() {
        return this._name;
    }

    /**
     * Gets device details (all fields), response formatted per http://docs.winkapiv2.apiary.io/
     */
    getDeviceDetailsAsync(deviceType, deviceId) {

        // build request URI
        var requestPath = '/' + deviceType + '/' + deviceId;

        // Make the async request
        return this._makeRequest(requestPath, 'GET');
    }

    /**
     * Puts device details (all fields) payload formatted per http://docs.winkapiv2.apiary.io/
    */
    putDeviceDetailsAsync(deviceType, deviceId, putPayload) {

        // build request path and body
        var requestPath = '/' + deviceType + '/' + deviceId;
        var putPayloadString = JSON.stringify(putPayload);

        // Make the async request
        return this._makeRequest(requestPath, 'PUT', putPayloadString);
    }

    /**
     * Subscribes to a Wink pubsubhubbub feed.  This function is designed to be called twice.
     * The first call will contain just the callbackUrl, which will be subscribed for postbacks
     * from the Wink cloud service.
     * The second call will contain the request, and response for a GET to the callbackUrl that was
     * provided in order to complete the verification of the subscription.
     *
     * The expected sequence is as follows:
     * - Call subscribe with a callback url, get a response.
     * - Http server running at the callback url receives a GET request, this request and a response string
     *      are passed to a 2nd call to subscribe() (callbackUrl is ignored in this case)
     * - Http server responds to the GET with the verificationResponseContent that was populated by subscribe
     * - Future POST calls to the callback url are passed to the device translator get*Uri function
     *
     * All calls to subscribe will return the expiration time of the subscription.  For Wink, this is
     * currently 24 hours.  Subsequent calls to subscribe will refresh the expiration time, and in the
     * case of Wink/Pubsubhubbub will not require verification.
     *
     * @param {string} deviceType - Device Type (e.g. 'thermostats')
     * @param {string|number} deviceId - Id for the specific device
     * @param {string} callbackUrl - Callback url for feed postbacks
     * @param {HttpRequest} verificationRequest - GET request received by the server at a previously subscribed
     *      callback URL.  This completes the verification half of the subscription.
     * @param {HttpResponse} verificationResponseContent - Content that should be provided in the response to the
     *      verification request as {'Content-Type': 'text/plain'}.  All responses should use code 200 unless an
     *      error is caught.
     * @returns {number} Object containing the subscription expiration time, and any content that
     *      needs to be included in a response to the original request.
     */
    _subscribe(deviceType, deviceId, callbackUrl, verificationRequest) {

        if (callbackUrl) {
            // Subscribe to notifications

            var requestPath = '/' + deviceType + '/' + deviceId + '/subscriptions';

            // Winks implementation of PubSubHubbub differs from the standard in that we do not need to provide
            // the topic, or mode on this request.  Topic is implicit from the URL (deviceType/deviceId), and
            // separate requests exist for mode (subscribe and unsubscribe vis POST/DELETE).

            // Additionally, subscriptions will expire after 24 hours (for now), and need to be refreshed
            // with another call to this function.

            var postPayload = {
                callback: callbackUrl,
            }

            var postPayloadString = JSON.stringify(postPayload);

            return this._makeRequest(requestPath, 'POST', postPayloadString).then((response) => {
                // Return the expiration time for the subscription
                return {
                    expiration: response.data.expires_at,
                    response: ""
                };
            })

        } else if (verificationRequest) {
            // Verify a subscription request by constructing the appropriate response

            var params = require('url').parse(verificationRequest.url, true, true);
            // This is a subscription validation, populate the response
            switch(params.query['hub.mode']) {
                case "denied":
                    // The subscription cannot be completed, access was denied.
                    // This is likely due to a bad access token.
                    throw new Error("Access denied");
                case "unsubscribe": // Could remove this, see below.
                case "subscribe":
                    // Successful subscription/unsubscription requires a response to contain the same
                    // challenge that was included in the request.
                    // Wink doesn't actually use hub.mode unsubscribe, and instead uses DELETE to perform the action
                    // with no verification.

                    // Verify that this subscription is for the correct topic
                    if (params.query['hub.topic'].endsWith(deviceType + '/' + deviceId)) {
                        return {
                            expiration: params.query['hub.lease_seconds'],
                            response: params.query['hub.challenge']
                        }
                    } else {
                        // There is a mistmatch.  This subscription doesn't match this device.
                        throw new Error("Subscription cannot be verified");
                    }
                default:
                    // Hub mode is unknown.
                    throw new Error("Unknown request");
            }
        }
    }

    /**
     * Unsubscribes from an existing Wink pubsubhubbub feed.  The subscription id for a
     * topic is not cached, so this results in 2 web calls:
     * - GET to return a list of subscriptions
     * - DELETE to remove the existing subscriptions.
     *
     * Wink's pubsubhubbub is slightly different than standard as it uses DELETE along
     * with a subscriptionID rather than another GET with a hub.mode of 'unsubscribe'
     *
     * @param {string} deviceType - Device Type (e.g. 'thermostats')
     * @param {string|number} deviceId - Id for the specific device
     * @param {string} callbackUrl - URL that will be unsubscribed
     * @returns {request} Promise that supplies the server response
     *
     */
    _unsubscribe(deviceType, deviceId, callbackUrl)
    {
        // Find the subscription ID for this callback URL
        return this._getSubscriptions(deviceType, deviceId).then((subscriptions) => {
            subscriptions.forEach((sub) => {
                // Find the subscription id for this callback URL and device
                if (sub.callback == callbackUrl &&
                    sub.topic.endsWith(deviceType + '/' + deviceId)) {
                    var requestPath = '/' + deviceType + '/' + deviceId + '/subscriptions/' + sub.subscription_id;

                    // Do the actual unsubscribe
                    return this._makeRequest(requestPath, 'DELETE').then(() => {
                        return {};
                    })
                }
            });

            // Subscription wasn't found so there's nothing to do
            return {};
        });
    }

    /**
     * Gets all subscriptions for a device.
     *
     * @param {string} deviceType - Device Type (e.g. 'thermostats')
     * @param {string|number} deviceId - Id for the specific device
     * @returns {request} Promise that supplies the list of subscriptions
     *
     * This is just a helper for tests and verification.
     */
    _getSubscriptions(deviceType, deviceId) {
        // GET /<deviceType>/<device Id>/subscriptions
        var requestPath = '/' + deviceType + '/' + deviceId + '/subscriptions';

        return this._makeRequest(requestPath, 'GET').then((response) => {
            return response.data;
        });
    }

    /**
     * Given the hub specific device, returns the opent2t schema and translator
    */
    _getOpent2tInfo(winkDevice) {
        // if (winkDevice.thermostat_id) {
        //     return {
        //         "schema": 'opent2t.p.thermostat',
        //         "translator": "opent2t-translator-com-wink-thermostat"
        //     };
        // }
        if (winkDevice.binary_switch_id) {
            return {
                "schema": 'opent2t.p.switch.binary',
                "translator": "opent2t-translator-com-wink-binaryswitch"
            };
        }
        else if (winkDevice.light_bulb_id) {
            return {
                "schema": 'opent2t.p.light',
                "translator": "opent2t-translator-com-wink-lightbulb"
            };
        }

        return undefined;
    }

    /**
     * Internal helper method which makes the actual request to the wink service
     */
    _makeRequest(path, method, content) {
        // build request URI
        var requestUri = this._baseUrl + path;

        // Set the headers
        var headers = {
            'Authorization': 'Bearer ' + this._accessToken.accessToken,
            'Accept': 'application/json'
        }

        if (content) {
            headers['Content-Type'] = 'application/json';
            headers['Content-length'] = content.length;
        }

        var options = {
            url: requestUri,
            method: method,
            headers: headers,
            followAllRedirects: true
        };

        if (content) {
            options.body = content;
        }

        // Start the async request
        return request(options)
            .then(function (body) {
                return JSON.parse(body);
            })
            .catch(function (err) {
                console.log("Request failed to: " + options.method + " - " + options.url);
                console.log("Error            : " + err.statusCode + " - " + err.response.statusMessage);
                // todo auto refresh in specific cases, issue 74
                throw err;
            });
    }

    /**
     * Wink has a semi-complicated way of getting the actual device id of a device.
     */
    _getDeviceId(winkDevice) {
        // get the device id
        var deviceId = winkDevice.light_bulb_id ||
            winkDevice.air_conditioner_id ||
            winkDevice.binary_switch_id ||
            winkDevice.shade_id ||
            winkDevice.camera_id ||
            winkDevice.doorbell_id ||
            winkDevice.eggtray_id ||
            winkDevice.garage_door_id ||
            winkDevice.cloud_clock_id ||
            winkDevice.lock_id ||
            winkDevice.dial_id ||
            winkDevice.alarm_id ||
            winkDevice.power_strip_id ||
            winkDevice.outlet_id ||
            winkDevice.piggy_bank_id ||
            winkDevice.deposit_id ||
            winkDevice.refrigerator_id ||
            winkDevice.propane_tank_id ||
            winkDevice.remote_id ||
            winkDevice.sensor_pod_id ||
            winkDevice.siren_id ||
            winkDevice.smoke_detector_id ||
            winkDevice.sprinkler_id ||
            winkDevice.thermostat_id ||
            winkDevice.water_heater_id ||
            winkDevice.scene_id ||
            winkDevice.condition_id ||
            winkDevice.robot_id;

        return deviceId;
    }
}

module.exports = Translator;<|endoftext|>


<commit_before>jIRCs.prototype.irc_PING = function(prefix, args) {
    this.send('PONG',args);
};

jIRCs.prototype.irc_NICK = function(prefix, args) {
    var oldNick = this.getNick(prefix),
        newNick = args.pop();
    if(oldNick == this.nickname) {
        this.nickname = newNick;
    }
    this.forEach(this.channels, function(c, channel) {
        if(c.names && oldNick in c.names) {
            this.renderLine(channel, channel, oldNick + ' is now known as ' + newNick);
            c.names[newNick] = c.names[oldNick];
            delete(c.names[oldNick]);
        }
    }, this);
    this.forEach(this.displays, function(disobj) {
        this.render(disobj);
    }, this);
};

jIRCs.prototype.irc_JOIN = function(prefix, args) {
    var channel = args.pop().toLowerCase();
    if(prefix != this.nickname) {
        this.renderLine(channel, channel, prefix + " joined " + channel);
    } else {
        this.renderLine(channel, channel, "You have joined " + channel);
    }
    if(!this.channels[channel].names) {
        this.channels[channel].names = {};
    }
    this.channels[channel].names[prefix] = "";
    if (!this.channels[channel].modes) {
        this.channels[channel].modes = {};
        this.send('MODE', [channel]); // Get the initial modes because the server doesn't send them by default
    }
    this.forEach(this.displays, function(disobj) {
        if(document.activeElement == disobj.messagebox && prefix == this.nickname) {
            this.activateChan(channel, disobj);
        }
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_PART = function(prefix, args) {
    var channel = args.shift().toLowerCase();
    var reason = args.pop();
    if(this.getNick(prefix) == this.nickname) {
        this.destroyChan(channel);
    } else {
        this.renderLine(channel, channel, prefix + " left " + channel + " [" + reason + "]");
        delete(this.channels[channel].names[prefix]);
        this.forEach(this.displays, function(disobj) {
            if(disobj.viewing == channel) {
                this.render(disobj);
            }
        }, this);
    }
};

jIRCs.prototype.irc_QUIT = function(prefix, args) {
    var reason = args.pop();
    this.forEach(this.channels, function(c, channel) {
        if(channel == 'Status') {
            return;
        }
        if(c.names && prefix in c.names) {
            this.renderLine(channel, channel, prefix + ' quit (' + reason + ')');
            delete(c.names[prefix]);
        }
    }, this);
    if(this.getNick(prefix) == this.nickname) {
        // Let ondisconnect handle cleanup
    } else {
        this.forEach(this.displays, function(disobj) {
            this.render(disobj);
        }, this);
    }
};

jIRCs.prototype.irc_PRIVMSG = function(prefix, args) {
    var channel = args.shift().toLowerCase();
    var message = args.pop();
    //account for private messages
    if (channel == this.nickname.toLowerCase()) {
        channel = prefix.toLowerCase();
    }
    if (channel.charAt(0) in this.statusSymbols) {
        prefix += ":" + channel; // give a visible indication that the message isn't for the whole channel
        channel = channel.substr(1); // trim the status char off so the message gets displayed to the correct channel
    }
    if(message.charAt(0) == '\u0001') {
        message = message.split('\u0001')[1];
        if(message.substr(0,6).toUpperCase() == 'ACTION') {
            message = prefix + message.substr(6);
            prefix = '';
            this.renderLine(channel, prefix, message);
        } else {
            args = message.split(' ');
            var method = 'ctcp_' + args.shift().toUpperCase();
            console.log("<<<<<< " + method + "('" + channel + "'," + JSON.stringify(args) + ")");
            if(method in this) {
                this[method](channel, args);
            }
        }
    } else {
        this.renderLine(channel, "<"+prefix+">", message);
    }
    if(prefix == "BidServ") {
        var cleaned = message.replace("\u0001","").replace("\u0002","").replace("\u00034","").replace("\u000F","").replace("\u0016","").replace("\u001D","").replace("\u001F","");
        var parts = cleaned.split(" ");
        console.debug(
            cleaned,
            parts,
            parts.slice(0,1).join(" "),
            parts.slice(0,2).join(" "),
            ~cleaned.indexOf("has the high bid of"),
            ~cleaned.indexOf("New highest bid is by"),
            parts.slice(0,1).join(" ") == "Auction for" && ~cleaned.indexOf("cancelled"),
            parts[0] == "Sold!"
        );
        if(parts.slice(0,1).join(" ") == "Starting Auction") {
            var id = parts[4].slice(1,-1);
            var name = parts.slice(5,-4);
            this.auctionStart(id,name);
        } else if(parts.slice(0,2).join(" ") == "Beginning bidding at") {
            var starting = parts[3];
            this.auctionBid(starting,"Nobody");
        } else if(~cleaned.indexOf("has the high bid of")) { // ~ abuses two's complement notation to make -1 false, and everything else true
            var index = 0;
            while(index < parts.length - 5) {
                if(parts[index+1] == "has" && parts[index+2] == "the" && parts[index+3] == "high" && parts[index+4] == "bid" && parts[index+5] == "of")
                    break;
                index++;
            }
            var bidder = parts[index];
            var bid = parts[index + 6];
            this.auctionBid(bid,bidder);
        } else if(~cleaned.indexOf("New highest bid is by")) {
            parts = cleaned.substr(cleaned.indexOf("New highest bid is by")).split(" ");
            var bidder = parts[5];
            var bid = parts[7];
            this.auctionBid(bid,bidder);
        } else if(parts.slice(0,1).join(" ") == "Auction for" && ~cleaned.indexOf("cancelled")) {
            this.auctionStop();
        } else if(parts[0] == "Sold!") {
            this.auctionStop();
        }
    }
};

jIRCs.prototype.irc_NOTICE = function(prefix, args) {
    var nick = '\u2013 ' + this.getNick(prefix);
    var message = args.pop();
    var dest = args.shift().toLowerCase(); // It'll only be used if it's a channel name, anyway
    if (this.chantypes.indexOf(dest.charAt(0)) !== -1 || this.chantypes.indexOf(dest.charAt(1)) !== -1) { // There may or may not be a channel status in the parameter
        nick += ":" + dest + ' \u2013'; // Give a visible indication that the message is a channel notice
        if (dest.charAt(0) in this.statusSymbols) { // it's not going directly to all of a channel
            dest = dest.substr(1); // display it in the correct window
        }
        this.renderLine(dest, nick, message);
    } else {
        nick += ' \u2013'; // \u2013 is an en-dash
        this.forEach(this.displays, function(disobj) {
            this.renderLine(disobj.viewing, nick, message, disobj);
        }, this);
    }
};

jIRCs.prototype.irc_MODE = function(prefix, args) {
    var channel = args.shift().toLowerCase();
    var modes = args.shift().split('');
    if (channel == this.nickname.toLowerCase()) { // handle user modes here
        var adding = true;
        this.forEach(modes, function(mode) {
            switch (mode) {
                case '+':
                    adding = true;
                    break;
                case '-':
                    adding = false;
                    break;
                default:
                    if (adding) {
                        this.userModes.push(mode);
                    } else {
                        var modePos = this.userModes.indexOf(mode);
                        if (modePos != -1) {
                            this.userModes.splice(modePos, 1);
                        }
                    }
            }
        }, this);
    } else {
        // At this point, what's left in args is the parameter list
        this.parseModes(channel, modes, args); // handle the channel modes
    }
};

jIRCs.prototype.irc_CAP = function(prefix, args) {
    // :server CAP dest subcommand :capability list
    if (args[1] == "LS") {
        var supportedCaps = args[2].split(' ');
        if (supportedCaps.indexOf("multi-prefix") != -1) {
            this.send("CAP", ["REQ", ":multi-prefix"]);
        } else if (!this.registered) {
            this.send("CAP", ["END"]);
        }
    } else if (args[1] == "ACK" && !this.registered) {
        this.send("CAP", ["END"]);
    }
}

jIRCs.prototype.irc_001 = function(prefix, args) {
    this.registered = true;
}

jIRCs.prototype.irc_005 = function(prefix, args) {
    var server = args.shift();
    var message = args.pop();
    this.forEach(args, function(arg) {
        if(arg.substr(0,7).toUpperCase() == 'PREFIX=') {
            var modes = arg.substr(8).split(')'); // exclude the open paren, split close paren
            var symbols = modes[1].split('');
            var letters = modes[0].split('');
            this.statuses = this.zip(symbols.concat(letters), letters.concat(symbols));
            this.statusOrder = letters;
            this.statusSymbols = this.zip(symbols, symbols);
            this.statuses[''] = '';
            this.statusOrder.push('');
        }
        if (arg.substr(0, 10) == 'CHANMODES=') {
            var modes = arg.substr(10); // get all the channel modes
            var groups = modes.split(',');
            this.forEach(groups, function(group, groupNum) {
                this.forEach(group, function(mode) {
                    this.chanModes[mode] = groupNum; // map modes to their group index for easy lookup
                }, this);
            }, this);
        }
        if (arg.substr(0, 10) == 'CHANTYPES=') {
            this.chantypes = arg.substr(10).split('');
        }
    }, this);
};

jIRCs.prototype.irc_324 = function(prefix, args) {
    args.shift(); // discard our nick; it's not useful
    var channel = args.shift().toLowerCase();
    var modes = args.shift().split('');
    // At this point, what's left in args (if anything) is the mode parameter list
    this.channels[channel].modes = {}; // Reset the channel modes list as 324 is all of the channel's current modes
    this.parseModes(channel, modes, args);
};

jIRCs.prototype.irc_353 = function(prefix, args) {
    var channel = args[2].toLowerCase();
    if (!this.channels[channel].moreNames) {
        this.channels[channel].names = {};
        this.channels[channel].moreNames = true;
    }
    var names = args[3].split(' '); // Strip the colon and split the names out
    this.forEach(names, function(name) {
        var statusList = '';
        while (name.charAt(0) in this.statusSymbols) {
            statusList += name.charAt(0);
            name = name.substr(1);
        }
        this.channels[channel].names[name] = statusList;
    }, this);
};

jIRCs.prototype.irc_366 = function(prefix, args) {
    var channel = args[1].toLowerCase();
    this.channels[channel].moreNames = false;
    this.forEach(this.displays, function(disobj) {
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_332 = function(prefix, args) {
    var channel = args[1].toLowerCase();
    if(!this.channels[channel].topic) {
        this.channels[channel].topic = {};
    }
    this.channels[channel].topic.message = args[2];
    this.forEach(this.displays, function(disobj) {
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_333 = function(prefix, args) {
    var channel = args[1].toLowerCase();
    if(!this.channels[channel].topic) {
        this.channels[channel].topic = {};
    }
    this.channels[channel].topic.creator = args[2];
    this.channels[channel].topic.time = new Date(args[3] * 1000);
    this.forEach(this.displays, function(disobj) {
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_TOPIC = function(prefix, args) {
    var channel = args[0].toLowerCase();
    if(!this.channels[channel].topic) {
        this.channels[channel].topic = {};
    }
    this.channels[channel].topic.creator = prefix;
    this.channels[channel].topic.time = new Date();
    this.channels[channel].topic.message = args[1];
    this.forEach(this.displays, function(disobj) {
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};
<commit_msg>Yay fixes<commit_after>jIRCs.prototype.irc_PING = function(prefix, args) {
    this.send('PONG',args);
};

jIRCs.prototype.irc_NICK = function(prefix, args) {
    var oldNick = this.getNick(prefix),
        newNick = args.pop();
    if(oldNick == this.nickname) {
        this.nickname = newNick;
    }
    this.forEach(this.channels, function(c, channel) {
        if(c.names && oldNick in c.names) {
            this.renderLine(channel, channel, oldNick + ' is now known as ' + newNick);
            c.names[newNick] = c.names[oldNick];
            delete(c.names[oldNick]);
        }
    }, this);
    this.forEach(this.displays, function(disobj) {
        this.render(disobj);
    }, this);
};

jIRCs.prototype.irc_JOIN = function(prefix, args) {
    var channel = args.pop().toLowerCase();
    if(prefix != this.nickname) {
        this.renderLine(channel, channel, prefix + " joined " + channel);
    } else {
        this.renderLine(channel, channel, "You have joined " + channel);
    }
    if(!this.channels[channel].names) {
        this.channels[channel].names = {};
    }
    this.channels[channel].names[prefix] = "";
    if (!this.channels[channel].modes) {
        this.channels[channel].modes = {};
        this.send('MODE', [channel]); // Get the initial modes because the server doesn't send them by default
    }
    this.forEach(this.displays, function(disobj) {
        if(document.activeElement == disobj.messagebox && prefix == this.nickname) {
            this.activateChan(channel, disobj);
        }
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_PART = function(prefix, args) {
    var channel = args.shift().toLowerCase();
    var reason = args.pop();
    if(this.getNick(prefix) == this.nickname) {
        this.destroyChan(channel);
    } else {
        this.renderLine(channel, channel, prefix + " left " + channel + " [" + reason + "]");
        delete(this.channels[channel].names[prefix]);
        this.forEach(this.displays, function(disobj) {
            if(disobj.viewing == channel) {
                this.render(disobj);
            }
        }, this);
    }
};

jIRCs.prototype.irc_QUIT = function(prefix, args) {
    var reason = args.pop();
    this.forEach(this.channels, function(c, channel) {
        if(channel == 'Status') {
            return;
        }
        if(c.names && prefix in c.names) {
            this.renderLine(channel, channel, prefix + ' quit (' + reason + ')');
            delete(c.names[prefix]);
        }
    }, this);
    if(this.getNick(prefix) == this.nickname) {
        // Let ondisconnect handle cleanup
    } else {
        this.forEach(this.displays, function(disobj) {
            this.render(disobj);
        }, this);
    }
};

jIRCs.prototype.irc_PRIVMSG = function(prefix, args) {
    var channel = args.shift().toLowerCase();
    var message = args.pop();
    //account for private messages
    if (channel == this.nickname.toLowerCase()) {
        channel = prefix.toLowerCase();
    }
    if (channel.charAt(0) in this.statusSymbols) {
        prefix += ":" + channel; // give a visible indication that the message isn't for the whole channel
        channel = channel.substr(1); // trim the status char off so the message gets displayed to the correct channel
    }
    if(message.charAt(0) == '\u0001') {
        message = message.split('\u0001')[1];
        if(message.substr(0,6).toUpperCase() == 'ACTION') {
            message = prefix + message.substr(6);
            prefix = '';
            this.renderLine(channel, prefix, message);
        } else {
            args = message.split(' ');
            var method = 'ctcp_' + args.shift().toUpperCase();
            console.log("<<<<<< " + method + "('" + channel + "'," + JSON.stringify(args) + ")");
            if(method in this) {
                this[method](channel, args);
            }
        }
    } else {
        this.renderLine(channel, "<"+prefix+">", message);
    }
    if(prefix == "BidServ") {
        var cleaned = message.replace("\u0001","").replace("\u0002","").replace("\u00034","").replace("\u000F","").replace("\u0016","").replace("\u001D","").replace("\u001F","");
        var parts = cleaned.split(" ");
        console.debug(
            cleaned,
            parts,
            parts.slice(0,2).join(" "),
            parts.slice(0,3).join(" "),
            ~cleaned.indexOf("has the high bid of"),
            ~cleaned.indexOf("New highest bid is by"),
            parts.slice(0,1).join(" ") == "Auction for" && ~cleaned.indexOf("cancelled"),
            parts[0] == "Sold!"
        );
        if(parts.slice(0,2).join(" ") == "Starting Auction") {
            var id = parts[4].slice(1,-1);
            var name = parts.slice(5,-4);
            this.auctionStart(id,name);
        } else if(parts.slice(0,3).join(" ") == "Beginning bidding at") {
            var starting = parts[3];
            this.auctionBid(starting,"Nobody");
        } else if(~cleaned.indexOf("has the high bid of")) { // ~ abuses two's complement notation to make -1 false, and everything else true
            var index = 0;
            while(index < parts.length - 5) {
                if(parts[index+1] == "has" && parts[index+2] == "the" && parts[index+3] == "high" && parts[index+4] == "bid" && parts[index+5] == "of")
                    break;
                index++;
            }
            var bidder = parts[index];
            var bid = parts[index + 6];
            this.auctionBid(bid,bidder);
        } else if(~cleaned.indexOf("New highest bid is by")) {
            parts = cleaned.substr(cleaned.indexOf("New highest bid is by")).split(" ");
            var bidder = parts[5];
            var bid = parts[7];
            this.auctionBid(bid,bidder);
        } else if(parts.slice(0,2).join(" ") == "Auction for" && ~cleaned.indexOf("cancelled")) {
            this.auctionStop();
        } else if(parts[0] == "Sold!") {
            this.auctionStop();
        }
    }
};

jIRCs.prototype.irc_NOTICE = function(prefix, args) {
    var nick = '\u2013 ' + this.getNick(prefix);
    var message = args.pop();
    var dest = args.shift().toLowerCase(); // It'll only be used if it's a channel name, anyway
    if (this.chantypes.indexOf(dest.charAt(0)) !== -1 || this.chantypes.indexOf(dest.charAt(1)) !== -1) { // There may or may not be a channel status in the parameter
        nick += ":" + dest + ' \u2013'; // Give a visible indication that the message is a channel notice
        if (dest.charAt(0) in this.statusSymbols) { // it's not going directly to all of a channel
            dest = dest.substr(1); // display it in the correct window
        }
        this.renderLine(dest, nick, message);
    } else {
        nick += ' \u2013'; // \u2013 is an en-dash
        this.forEach(this.displays, function(disobj) {
            this.renderLine(disobj.viewing, nick, message, disobj);
        }, this);
    }
};

jIRCs.prototype.irc_MODE = function(prefix, args) {
    var channel = args.shift().toLowerCase();
    var modes = args.shift().split('');
    if (channel == this.nickname.toLowerCase()) { // handle user modes here
        var adding = true;
        this.forEach(modes, function(mode) {
            switch (mode) {
                case '+':
                    adding = true;
                    break;
                case '-':
                    adding = false;
                    break;
                default:
                    if (adding) {
                        this.userModes.push(mode);
                    } else {
                        var modePos = this.userModes.indexOf(mode);
                        if (modePos != -1) {
                            this.userModes.splice(modePos, 1);
                        }
                    }
            }
        }, this);
    } else {
        // At this point, what's left in args is the parameter list
        this.parseModes(channel, modes, args); // handle the channel modes
    }
};

jIRCs.prototype.irc_CAP = function(prefix, args) {
    // :server CAP dest subcommand :capability list
    if (args[1] == "LS") {
        var supportedCaps = args[2].split(' ');
        if (supportedCaps.indexOf("multi-prefix") != -1) {
            this.send("CAP", ["REQ", ":multi-prefix"]);
        } else if (!this.registered) {
            this.send("CAP", ["END"]);
        }
    } else if (args[1] == "ACK" && !this.registered) {
        this.send("CAP", ["END"]);
    }
}

jIRCs.prototype.irc_001 = function(prefix, args) {
    this.registered = true;
}

jIRCs.prototype.irc_005 = function(prefix, args) {
    var server = args.shift();
    var message = args.pop();
    this.forEach(args, function(arg) {
        if(arg.substr(0,7).toUpperCase() == 'PREFIX=') {
            var modes = arg.substr(8).split(')'); // exclude the open paren, split close paren
            var symbols = modes[1].split('');
            var letters = modes[0].split('');
            this.statuses = this.zip(symbols.concat(letters), letters.concat(symbols));
            this.statusOrder = letters;
            this.statusSymbols = this.zip(symbols, symbols);
            this.statuses[''] = '';
            this.statusOrder.push('');
        }
        if (arg.substr(0, 10) == 'CHANMODES=') {
            var modes = arg.substr(10); // get all the channel modes
            var groups = modes.split(',');
            this.forEach(groups, function(group, groupNum) {
                this.forEach(group, function(mode) {
                    this.chanModes[mode] = groupNum; // map modes to their group index for easy lookup
                }, this);
            }, this);
        }
        if (arg.substr(0, 10) == 'CHANTYPES=') {
            this.chantypes = arg.substr(10).split('');
        }
    }, this);
};

jIRCs.prototype.irc_324 = function(prefix, args) {
    args.shift(); // discard our nick; it's not useful
    var channel = args.shift().toLowerCase();
    var modes = args.shift().split('');
    // At this point, what's left in args (if anything) is the mode parameter list
    this.channels[channel].modes = {}; // Reset the channel modes list as 324 is all of the channel's current modes
    this.parseModes(channel, modes, args);
};

jIRCs.prototype.irc_353 = function(prefix, args) {
    var channel = args[2].toLowerCase();
    if (!this.channels[channel].moreNames) {
        this.channels[channel].names = {};
        this.channels[channel].moreNames = true;
    }
    var names = args[3].split(' '); // Strip the colon and split the names out
    this.forEach(names, function(name) {
        var statusList = '';
        while (name.charAt(0) in this.statusSymbols) {
            statusList += name.charAt(0);
            name = name.substr(1);
        }
        this.channels[channel].names[name] = statusList;
    }, this);
};

jIRCs.prototype.irc_366 = function(prefix, args) {
    var channel = args[1].toLowerCase();
    this.channels[channel].moreNames = false;
    this.forEach(this.displays, function(disobj) {
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_332 = function(prefix, args) {
    var channel = args[1].toLowerCase();
    if(!this.channels[channel].topic) {
        this.channels[channel].topic = {};
    }
    this.channels[channel].topic.message = args[2];
    this.forEach(this.displays, function(disobj) {
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_333 = function(prefix, args) {
    var channel = args[1].toLowerCase();
    if(!this.channels[channel].topic) {
        this.channels[channel].topic = {};
    }
    this.channels[channel].topic.creator = args[2];
    this.channels[channel].topic.time = new Date(args[3] * 1000);
    this.forEach(this.displays, function(disobj) {
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_TOPIC = function(prefix, args) {
    var channel = args[0].toLowerCase();
    if(!this.channels[channel].topic) {
        this.channels[channel].topic = {};
    }
    this.channels[channel].topic.creator = prefix;
    this.channels[channel].topic.time = new Date();
    this.channels[channel].topic.message = args[1];
    this.forEach(this.displays, function(disobj) {
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};
<|endoftext|>


<commit_before>jIRCs.prototype.irc_PING = function(prefix, args) {
    this.send('PONG',args);
};

jIRCs.prototype.irc_NICK = function(prefix, args) {
    var oldNick = this.getNick(prefix),
        newNick = args.pop();
    if(oldNick == this.nickname) {
        this.nickname = newNick;
    }
    this.forEach(this.channels, function(c, channel) {
        if(c.names && oldNick in c.names) {
            this.renderLine(channel, channel, oldNick + ' is now known as ' + newNick);
            c.names[newNick] = c.names[oldNick];
            delete(c.names[oldNick]);
        }
    }, this);
    this.forEach(this.displays, function(disobj) {
        this.render(disobj);
    }, this);
};

jIRCs.prototype.irc_JOIN = function(prefix, args) {
    var channel = args.pop().toLowerCase();
    if(prefix != this.nickname) {
        //this.renderLine(channel, channel, prefix + " joined " + channel);
    } else {
        //this.renderLine(channel, channel, "You have joined " + channel);
    }
    if(!this.channels[channel].names) {
        this.channels[channel].names = {};
    }
    this.channels[channel].names[prefix] = "";
    if (!this.channels[channel].modes) {
        this.channels[channel].modes = {};
        this.send('MODE', [channel]); // Get the initial modes because the server doesn't send them by default
    }
    this.forEach(this.displays, function(disobj) {
        if(channel.charAt(0) == "#" && prefix == this.nickname) {
            this.activateChan(channel, disobj);
        }
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_PART = function(prefix, args) {
    var channel = args.shift().toLowerCase();
    var reason = args.pop();
    if(this.getNick(prefix) == this.nickname) {
        this.destroyChan(channel);
    } else {
        //this.renderLine(channel, channel, prefix + " left " + channel + " [" + reason + "]");
        delete(this.channels[channel].names[prefix]);
        this.forEach(this.displays, function(disobj) {
            if(disobj.viewing == channel) {
                this.render(disobj);
            }
        }, this);
    }
};

jIRCs.prototype.irc_QUIT = function(prefix, args) {
    var reason = args.pop();
    this.forEach(this.channels, function(c, channel) {
        if(channel == 'Status') {
            return;
        }
        if(c.names && prefix in c.names) {
            //this.renderLine(channel, channel, prefix + ' quit (' + reason + ')');
            delete(c.names[prefix]);
        }
    }, this);
    if(this.getNick(prefix) == this.nickname) {
        // Let ondisconnect handle cleanup
    } else {
        this.forEach(this.displays, function(disobj) {
            this.render(disobj);
        }, this);
    }
};

jIRCs.prototype.irc_PRIVMSG = function(prefix, args) {
    var channel = args.shift().toLowerCase();
    var message = args.pop();
    //account for private messages
    if (channel == this.nickname.toLowerCase()) {
        channel = prefix.toLowerCase();
    }
    if (channel.charAt(0) in this.statusSymbols) {
        prefix += ":" + channel; // give a visible indication that the message isn't for the whole channel
        channel = channel.substr(1); // trim the status char off so the message gets displayed to the correct channel
    }
    if(message.charAt(0) == '\u0001') {
        message = message.split('\u0001')[1];
        if(message.substr(0,6).toUpperCase() == 'ACTION') {
            message = prefix + message.substr(6);
            prefix = '';
            this.renderLine(channel, prefix, message);
        } else {
            args = message.split(' ');
            var method = 'ctcp_' + args.shift().toUpperCase();
            console.log("<<<<<< " + method + "('" + channel + "'," + JSON.stringify(args) + ")");
            if(method in this) {
                this[method](channel, args);
            }
        }
    } else {
        this.renderLine(channel, "<"+prefix+">", message);
    }
    if(prefix == "BidServ") {
        var cleaned = message.replace("\u0001","").replace("\u0002","").replace("\u00034","").replace("\u000F","").replace("\u0016","").replace("\u001D","").replace("\u001F","");
        var parts = cleaned.split(" ");
        console.debug(
            cleaned,
            parts,
            parts.slice(0,2).join(" "),
            parts.slice(0,3).join(" "),
            ~cleaned.indexOf("has the high bid of"),
            ~cleaned.indexOf("New highest bid is by"),
            parts.slice(0,1).join(" ") == "Auction for" && ~cleaned.indexOf("cancelled"),
            parts[0] == "Sold!"
        );
        if(parts.slice(0,2).join(" ") == "Starting Auction") {
            var id = parts[4].slice(1,-1);
            var name = parts.slice(5,-4).join(" ").slice(1,-2);
            this.auctionStart(id,name);
        } else if(parts.slice(0,3).join(" ") == "Beginning bidding at") {
            var starting = parts[3];
            this.auctionBid(starting,"Nobody");
        } else if(~cleaned.indexOf("has the high bid of")) { // ~ abuses two's complement notation to make -1 false, and everything else true
            var index = 0;
            while(index < parts.length - 5) {
                if(parts[index+1] == "has" && parts[index+2] == "the" && parts[index+3] == "high" && parts[index+4] == "bid" && parts[index+5] == "of")
                    break;
                index++;
            }
            var bidder = parts[index];
            var bid = parts[index + 6];
            this.auctionBid(bid,bidder);
        } else if(~cleaned.indexOf("New highest bid is by")) {
            parts = cleaned.substr(cleaned.indexOf("New highest bid is by")).split(" ");
            var bidder = parts[5];
            var bid = parts[7];
            this.auctionBid(bid,bidder);
        } else if(parts.slice(0,2).join(" ") == "Auction for" && ~cleaned.indexOf("cancelled")) {
            this.auctionStop();
        } else if(parts[0] == "Sold!") {
            this.auctionStop();
        }
    }
};

jIRCs.prototype.irc_NOTICE = function(prefix, args) {
    var nick = '\u2013 ' + this.getNick(prefix);
    var message = args.pop();
    var dest = args.shift().toLowerCase(); // It'll only be used if it's a channel name, anyway
    if (this.chantypes.indexOf(dest.charAt(0)) !== -1 || this.chantypes.indexOf(dest.charAt(1)) !== -1) { // There may or may not be a channel status in the parameter
        nick += ":" + dest + ' \u2013'; // Give a visible indication that the message is a channel notice
        if (dest.charAt(0) in this.statusSymbols) { // it's not going directly to all of a channel
            dest = dest.substr(1); // display it in the correct window
        }
        this.renderLine(dest, nick, message);
    } else {
        nick += ' \u2013'; // \u2013 is an en-dash
        this.forEach(this.displays, function(disobj) {
            this.renderLine(disobj.viewing, nick, message, disobj);
        }, this);
    }
};

jIRCs.prototype.irc_MODE = function(prefix, args) {
    var channel = args.shift().toLowerCase();
    var modes = args.shift().split('');
    if (channel == this.nickname.toLowerCase()) { // handle user modes here
        var adding = true;
        this.forEach(modes, function(mode) {
            switch (mode) {
                case '+':
                    adding = true;
                    break;
                case '-':
                    adding = false;
                    break;
                default:
                    if (adding) {
                        this.userModes.push(mode);
                    } else {
                        var modePos = this.userModes.indexOf(mode);
                        if (modePos != -1) {
                            this.userModes.splice(modePos, 1);
                        }
                    }
            }
        }, this);
    } else {
        // At this point, what's left in args is the parameter list
        this.parseModes(channel, modes, args); // handle the channel modes
    }
};

jIRCs.prototype.irc_CAP = function(prefix, args) {
    // :server CAP dest subcommand :capability list
    if (args[1] == "LS") {
        var supportedCaps = args[2].split(' ');
        if (supportedCaps.indexOf("multi-prefix") != -1) {
            this.send("CAP", ["REQ", ":multi-prefix"]);
        } else if (!this.registered) {
            this.send("CAP", ["END"]);
        }
    } else if (args[1] == "ACK" && !this.registered) {
        this.send("CAP", ["END"]);
    }
}

jIRCs.prototype.irc_001 = function(prefix, args) {
    this.registered = true;
}

jIRCs.prototype.irc_005 = function(prefix, args) {
    var server = args.shift();
    var message = args.pop();
    this.forEach(args, function(arg) {
        if(arg.substr(0,7).toUpperCase() == 'PREFIX=') {
            var modes = arg.substr(8).split(')'); // exclude the open paren, split close paren
            var symbols = modes[1].split('');
            var letters = modes[0].split('');
            this.statuses = this.zip(symbols.concat(letters), letters.concat(symbols));
            this.statusOrder = letters;
            this.statusSymbols = this.zip(symbols, symbols);
            this.statuses[''] = '';
            this.statusOrder.push('');
        }
        if (arg.substr(0, 10) == 'CHANMODES=') {
            var modes = arg.substr(10); // get all the channel modes
            var groups = modes.split(',');
            this.forEach(groups, function(group, groupNum) {
                this.forEach(group, function(mode) {
                    this.chanModes[mode] = groupNum; // map modes to their group index for easy lookup
                }, this);
            }, this);
        }
        if (arg.substr(0, 10) == 'CHANTYPES=') {
            this.chantypes = arg.substr(10).split('');
        }
    }, this);
};

jIRCs.prototype.irc_324 = function(prefix, args) {
    args.shift(); // discard our nick; it's not useful
    var channel = args.shift().toLowerCase();
    var modes = args.shift().split('');
    // At this point, what's left in args (if anything) is the mode parameter list
    this.channels[channel].modes = {}; // Reset the channel modes list as 324 is all of the channel's current modes
    this.parseModes(channel, modes, args);
};

jIRCs.prototype.irc_353 = function(prefix, args) {
    var channel = args[2].toLowerCase();
    if (!this.channels[channel].moreNames) {
        this.channels[channel].names = {};
        this.channels[channel].moreNames = true;
    }
    var names = args[3].split(' '); // Strip the colon and split the names out
    this.forEach(names, function(name) {
        var statusList = '';
        while (name.charAt(0) in this.statusSymbols) {
            statusList += name.charAt(0);
            name = name.substr(1);
        }
        this.channels[channel].names[name] = statusList;
    }, this);
};

jIRCs.prototype.irc_366 = function(prefix, args) {
    var channel = args[1].toLowerCase();
    this.channels[channel].moreNames = false;
    this.forEach(this.displays, function(disobj) {
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_332 = function(prefix, args) {
    var channel = args[1].toLowerCase();
    if(!this.channels[channel].topic) {
        this.channels[channel].topic = {};
    }
    this.channels[channel].topic.message = args[2];
    this.forEach(this.displays, function(disobj) {
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_333 = function(prefix, args) {
    var channel = args[1].toLowerCase();
    if(!this.channels[channel].topic) {
        this.channels[channel].topic = {};
    }
    this.channels[channel].topic.creator = args[2];
    this.channels[channel].topic.time = new Date(args[3] * 1000);
    this.forEach(this.displays, function(disobj) {
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_TOPIC = function(prefix, args) {
    var channel = args[0].toLowerCase();
    if(!this.channels[channel].topic) {
        this.channels[channel].topic = {};
    }
    this.channels[channel].topic.creator = prefix;
    this.channels[channel].topic.time = new Date();
    this.channels[channel].topic.message = args[1];
    this.forEach(this.displays, function(disobj) {
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};
<commit_msg>We needed that line<commit_after>jIRCs.prototype.irc_PING = function(prefix, args) {
    this.send('PONG',args);
};

jIRCs.prototype.irc_NICK = function(prefix, args) {
    var oldNick = this.getNick(prefix),
        newNick = args.pop();
    if(oldNick == this.nickname) {
        this.nickname = newNick;
    }
    this.forEach(this.channels, function(c, channel) {
        if(c.names && oldNick in c.names) {
            this.renderLine(channel, channel, oldNick + ' is now known as ' + newNick);
            c.names[newNick] = c.names[oldNick];
            delete(c.names[oldNick]);
        }
    }, this);
    this.forEach(this.displays, function(disobj) {
        this.render(disobj);
    }, this);
};

jIRCs.prototype.irc_JOIN = function(prefix, args) {
    var channel = args.pop().toLowerCase();
    if(prefix != this.nickname) {
        //this.renderLine(channel, channel, prefix + " joined " + channel);
    } else {
        this.renderLine(channel, channel, "You have joined " + channel);
    }
    if(!this.channels[channel].names) {
        this.channels[channel].names = {};
    }
    this.channels[channel].names[prefix] = "";
    if (!this.channels[channel].modes) {
        this.channels[channel].modes = {};
        this.send('MODE', [channel]); // Get the initial modes because the server doesn't send them by default
    }
    this.forEach(this.displays, function(disobj) {
        if(channel.charAt(0) == "#" && prefix == this.nickname) {
            this.activateChan(channel, disobj);
        }
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_PART = function(prefix, args) {
    var channel = args.shift().toLowerCase();
    var reason = args.pop();
    if(this.getNick(prefix) == this.nickname) {
        this.destroyChan(channel);
    } else {
        //this.renderLine(channel, channel, prefix + " left " + channel + " [" + reason + "]");
        delete(this.channels[channel].names[prefix]);
        this.forEach(this.displays, function(disobj) {
            if(disobj.viewing == channel) {
                this.render(disobj);
            }
        }, this);
    }
};

jIRCs.prototype.irc_QUIT = function(prefix, args) {
    var reason = args.pop();
    this.forEach(this.channels, function(c, channel) {
        if(channel == 'Status') {
            return;
        }
        if(c.names && prefix in c.names) {
            //this.renderLine(channel, channel, prefix + ' quit (' + reason + ')');
            delete(c.names[prefix]);
        }
    }, this);
    if(this.getNick(prefix) == this.nickname) {
        // Let ondisconnect handle cleanup
    } else {
        this.forEach(this.displays, function(disobj) {
            this.render(disobj);
        }, this);
    }
};

jIRCs.prototype.irc_PRIVMSG = function(prefix, args) {
    var channel = args.shift().toLowerCase();
    var message = args.pop();
    //account for private messages
    if (channel == this.nickname.toLowerCase()) {
        channel = prefix.toLowerCase();
    }
    if (channel.charAt(0) in this.statusSymbols) {
        prefix += ":" + channel; // give a visible indication that the message isn't for the whole channel
        channel = channel.substr(1); // trim the status char off so the message gets displayed to the correct channel
    }
    if(message.charAt(0) == '\u0001') {
        message = message.split('\u0001')[1];
        if(message.substr(0,6).toUpperCase() == 'ACTION') {
            message = prefix + message.substr(6);
            prefix = '';
            this.renderLine(channel, prefix, message);
        } else {
            args = message.split(' ');
            var method = 'ctcp_' + args.shift().toUpperCase();
            console.log("<<<<<< " + method + "('" + channel + "'," + JSON.stringify(args) + ")");
            if(method in this) {
                this[method](channel, args);
            }
        }
    } else {
        this.renderLine(channel, "<"+prefix+">", message);
    }
    if(prefix == "BidServ") {
        var cleaned = message.replace("\u0001","").replace("\u0002","").replace("\u00034","").replace("\u000F","").replace("\u0016","").replace("\u001D","").replace("\u001F","");
        var parts = cleaned.split(" ");
        console.debug(
            cleaned,
            parts,
            parts.slice(0,2).join(" "),
            parts.slice(0,3).join(" "),
            ~cleaned.indexOf("has the high bid of"),
            ~cleaned.indexOf("New highest bid is by"),
            parts.slice(0,1).join(" ") == "Auction for" && ~cleaned.indexOf("cancelled"),
            parts[0] == "Sold!"
        );
        if(parts.slice(0,2).join(" ") == "Starting Auction") {
            var id = parts[4].slice(1,-1);
            var name = parts.slice(5,-4).join(" ").slice(1,-2);
            this.auctionStart(id,name);
        } else if(parts.slice(0,3).join(" ") == "Beginning bidding at") {
            var starting = parts[3];
            this.auctionBid(starting,"Nobody");
        } else if(~cleaned.indexOf("has the high bid of")) { // ~ abuses two's complement notation to make -1 false, and everything else true
            var index = 0;
            while(index < parts.length - 5) {
                if(parts[index+1] == "has" && parts[index+2] == "the" && parts[index+3] == "high" && parts[index+4] == "bid" && parts[index+5] == "of")
                    break;
                index++;
            }
            var bidder = parts[index];
            var bid = parts[index + 6];
            this.auctionBid(bid,bidder);
        } else if(~cleaned.indexOf("New highest bid is by")) {
            parts = cleaned.substr(cleaned.indexOf("New highest bid is by")).split(" ");
            var bidder = parts[5];
            var bid = parts[7];
            this.auctionBid(bid,bidder);
        } else if(parts.slice(0,2).join(" ") == "Auction for" && ~cleaned.indexOf("cancelled")) {
            this.auctionStop();
        } else if(parts[0] == "Sold!") {
            this.auctionStop();
        }
    }
};

jIRCs.prototype.irc_NOTICE = function(prefix, args) {
    var nick = '\u2013 ' + this.getNick(prefix);
    var message = args.pop();
    var dest = args.shift().toLowerCase(); // It'll only be used if it's a channel name, anyway
    if (this.chantypes.indexOf(dest.charAt(0)) !== -1 || this.chantypes.indexOf(dest.charAt(1)) !== -1) { // There may or may not be a channel status in the parameter
        nick += ":" + dest + ' \u2013'; // Give a visible indication that the message is a channel notice
        if (dest.charAt(0) in this.statusSymbols) { // it's not going directly to all of a channel
            dest = dest.substr(1); // display it in the correct window
        }
        this.renderLine(dest, nick, message);
    } else {
        nick += ' \u2013'; // \u2013 is an en-dash
        this.forEach(this.displays, function(disobj) {
            this.renderLine(disobj.viewing, nick, message, disobj);
        }, this);
    }
};

jIRCs.prototype.irc_MODE = function(prefix, args) {
    var channel = args.shift().toLowerCase();
    var modes = args.shift().split('');
    if (channel == this.nickname.toLowerCase()) { // handle user modes here
        var adding = true;
        this.forEach(modes, function(mode) {
            switch (mode) {
                case '+':
                    adding = true;
                    break;
                case '-':
                    adding = false;
                    break;
                default:
                    if (adding) {
                        this.userModes.push(mode);
                    } else {
                        var modePos = this.userModes.indexOf(mode);
                        if (modePos != -1) {
                            this.userModes.splice(modePos, 1);
                        }
                    }
            }
        }, this);
    } else {
        // At this point, what's left in args is the parameter list
        this.parseModes(channel, modes, args); // handle the channel modes
    }
};

jIRCs.prototype.irc_CAP = function(prefix, args) {
    // :server CAP dest subcommand :capability list
    if (args[1] == "LS") {
        var supportedCaps = args[2].split(' ');
        if (supportedCaps.indexOf("multi-prefix") != -1) {
            this.send("CAP", ["REQ", ":multi-prefix"]);
        } else if (!this.registered) {
            this.send("CAP", ["END"]);
        }
    } else if (args[1] == "ACK" && !this.registered) {
        this.send("CAP", ["END"]);
    }
}

jIRCs.prototype.irc_001 = function(prefix, args) {
    this.registered = true;
}

jIRCs.prototype.irc_005 = function(prefix, args) {
    var server = args.shift();
    var message = args.pop();
    this.forEach(args, function(arg) {
        if(arg.substr(0,7).toUpperCase() == 'PREFIX=') {
            var modes = arg.substr(8).split(')'); // exclude the open paren, split close paren
            var symbols = modes[1].split('');
            var letters = modes[0].split('');
            this.statuses = this.zip(symbols.concat(letters), letters.concat(symbols));
            this.statusOrder = letters;
            this.statusSymbols = this.zip(symbols, symbols);
            this.statuses[''] = '';
            this.statusOrder.push('');
        }
        if (arg.substr(0, 10) == 'CHANMODES=') {
            var modes = arg.substr(10); // get all the channel modes
            var groups = modes.split(',');
            this.forEach(groups, function(group, groupNum) {
                this.forEach(group, function(mode) {
                    this.chanModes[mode] = groupNum; // map modes to their group index for easy lookup
                }, this);
            }, this);
        }
        if (arg.substr(0, 10) == 'CHANTYPES=') {
            this.chantypes = arg.substr(10).split('');
        }
    }, this);
};

jIRCs.prototype.irc_324 = function(prefix, args) {
    args.shift(); // discard our nick; it's not useful
    var channel = args.shift().toLowerCase();
    var modes = args.shift().split('');
    // At this point, what's left in args (if anything) is the mode parameter list
    this.channels[channel].modes = {}; // Reset the channel modes list as 324 is all of the channel's current modes
    this.parseModes(channel, modes, args);
};

jIRCs.prototype.irc_353 = function(prefix, args) {
    var channel = args[2].toLowerCase();
    if (!this.channels[channel].moreNames) {
        this.channels[channel].names = {};
        this.channels[channel].moreNames = true;
    }
    var names = args[3].split(' '); // Strip the colon and split the names out
    this.forEach(names, function(name) {
        var statusList = '';
        while (name.charAt(0) in this.statusSymbols) {
            statusList += name.charAt(0);
            name = name.substr(1);
        }
        this.channels[channel].names[name] = statusList;
    }, this);
};

jIRCs.prototype.irc_366 = function(prefix, args) {
    var channel = args[1].toLowerCase();
    this.channels[channel].moreNames = false;
    this.forEach(this.displays, function(disobj) {
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_332 = function(prefix, args) {
    var channel = args[1].toLowerCase();
    if(!this.channels[channel].topic) {
        this.channels[channel].topic = {};
    }
    this.channels[channel].topic.message = args[2];
    this.forEach(this.displays, function(disobj) {
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_333 = function(prefix, args) {
    var channel = args[1].toLowerCase();
    if(!this.channels[channel].topic) {
        this.channels[channel].topic = {};
    }
    this.channels[channel].topic.creator = args[2];
    this.channels[channel].topic.time = new Date(args[3] * 1000);
    this.forEach(this.displays, function(disobj) {
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_TOPIC = function(prefix, args) {
    var channel = args[0].toLowerCase();
    if(!this.channels[channel].topic) {
        this.channels[channel].topic = {};
    }
    this.channels[channel].topic.creator = prefix;
    this.channels[channel].topic.time = new Date();
    this.channels[channel].topic.message = args[1];
    this.forEach(this.displays, function(disobj) {
        if(disobj.viewing == channel) {
            this.render(disobj);
        }
    }, this);
};
<|endoftext|>