<commit_before>"use strict";

var substitutions = require("./substitutions");
var rp = require('request-promise');
var Promise = require('bluebird');
/**
 * @constructor
 * @param {request} requestLib request library, for sending HTTP requests.
 * @param {Object} config the configuration of the bridge.
 *     See ../config/slack-config-schema.yaml for the schema to which this must conform.
 * @param {Rooms} rooms mapping of all known slack channels to matrix rooms.
 * @param {Bridge} bridge the matrix-appservice-bridge bridge through which to
 *     communicate with matrix.
 */
function SlackHookHandler(requestLib, config, rooms, bridge) {
    this.requestLib = requestLib;
    this.config = config;
    this.rooms = rooms;
    this.bridge = bridge;
}

SlackHookHandler.prototype.getIntent = function(slackUser) {
    var username = "@" + this.config.username_prefix + slackUser +
        ":" + this.config.homeserver.server_name;
    return this.bridge.getIntent(username);
};

/**
 * Handles a slack webhook request.
 *
 * Sends a message to Matrix if it understands enough of the message to do so.
 * Attempts to make the message as native-matrix feeling as it can.
 *
 * @param {Object} params HTTP body of the webhook request, as a JSON-parsed dictionary.
 * @param {string} params.channel_id Slack channel ID receiving the message.
 * @param {string} params.channel_name Slack channel name receiving the message.
 * @param {string} params.user_id Slack user ID of user sending the message.
 * @param {string} params.user_name Slack user name of the user sending the message.
 * @param {?string} params.text Text contents of the message, if a text message.
 * @param {string} timestamp Timestamp when message was received, in seconds
 *     formatted as a float.
 */
SlackHookHandler.prototype.handle = function(params) {
    console.log("Received slack webhook request: " + JSON.stringify(params));
    if (params.user_id === "USLACKBOT") {
        return;
    }
    if (!this.rooms.knowsSlackChannel(params.channel_id)) {
        console.log("Ignoring message for slack channel with unknown matrix ID: %s (%s)",
            params.channel_id, params.channel_name
        );
        return;
    }
    var intent = this.getIntent(params.user_name);
    var roomID = this.rooms.matrixRoomID(params.channel_id);
    var team_domain = params.team_domain;

    // TODO: store this somewhere
    intent.setDisplayName(params.user_name);

    if (!this.config["slack_token_" + team_domain]) {
        // If we can't look up more details about the message
        // (because we don't have a master token), but it has text,
        // just send the message as text.
        console.log("no slack token for " + team_domain);

        if (params.text) {
            intent.sendText(roomID, substitutions.slackToMatrix(params.text));
        }
        return;
    }
    var token = this.config["slack_token_" + team_domain];

    var text = params.text;
    if (undefined == text) {
        lookupAndSendMessage(params.channel_id, params.timestamp, intent, roomID, token);
        return;
    }

    lookupMessage(params.channel_id, params.timestamp, token).then((msg) => {
        if(undefined == msg) {
            msg = params;
        }
        console.log("the msg:" + JSON.stringify(msg));
        return replaceChannelIdsWithNames(msg, token);
    }).then((msg) => {
        return replaceUserIdsWithNames(msg, token);
    }).then((msg) => {
        // we can't use .finally here as it does not get the final value, see https://github.com/kriskowal/q/issues/589
        return sendMessage(msg, intent, roomID, token);
    },
    (msg) => {
        return sendMessage(msg, intent, roomID, token);
    });
};

function replaceChannelIdsWithNames(message, token) {
    var testForName = message.text.match(/<#(\w+?)>/g);
    var iteration = 0;
    var matches = 0;
    if (testForName && testForName.length) {
        matches = testForName.length;
    }
    return promiseWhile(function() {
        // Do this until there are no more channel ID matches
        return iteration < matches;
    }, function() {
        var id = testForName[iteration].slice(2, -1);
        var channelsInfoApiParams = {
            uri: 'https://slack.com/api/channels.info',
            qs: {
                token: token,
                channel: id
            },
            json: true
        };
        return rp(channelsInfoApiParams).then((response) => {
            if (response && response.channel && response.channel.name) {
                console.log("channels.info: " + id + " mapped to " + response.channel.name);
                message.text = message.text.replace(/<#(\w+?)>/, "#" + response.channel.name);
            }
            else {
                console.log("channels.info returned no result for " + id);
            }
            iteration++;
            }).catch((err) => {
               console.log("Caught error " + err);
            });
    }).then(() => {
        // Notice we can chain it because it's a Promise,
        // this will run after completion of the promiseWhile Promise!
        return message;
    });
};

function replaceUserIdsWithNames(message, token) {
    var testForName = message.text.match(/<@(\w+?)>/g);
    var iteration = 0;
    var matches = 0;
    if (testForName && testForName.length) {
        matches = testForName.length;
    }
    return promiseWhile(() => {
        // Condition for stopping
        return iteration < matches;
    }, function() {
        var id = testForName[iteration].slice(2, -1);
        var channelsInfoApiParams = {
            uri: 'https://slack.com/api/users.info',
            qs: {
                token: token,
                user: id
            },
            json: true
        };
        return rp(channelsInfoApiParams).then((response) => {
            if (response && response.user && response.user.name) {
                console.log("users.info: " + id + " mapped to " + response.user.name);
                message.text = message.text.replace(/<@(\w+?)>/, response.user.name);
            }
            else {
                console.log("users.info returned no result for " + id);
            }
            iteration++;
            }).catch((err) => {
               console.log("Caught error " + err);
            });
    }).then(() => {
        // Notice we can chain it because it's a Promise,
        // this will run after completion of the promiseWhile Promise!
        return message;
    });
};

// promiseWhile code from http://blog.victorquinn.com/javascript-promise-while-loop
var promiseWhile = function(condition, action) {
    var resolver = Promise.defer();

    var loop = function() {
        if (!condition()) return resolver.resolve();
        return Promise.cast(action())
            .then(loop)
            .catch(resolver.reject);
    };

    process.nextTick(loop);

    return resolver.promise;
};

/**
 * Attempts to handle a message received from a slack webhook request.
 *
 * The webhook request that we receive doesn't have enough information to richly
 * represent the message in Matrix, so we look up more details.
 *
 * @param {string} channelID Slack channel ID.
 * @param {string} timestamp Timestamp when message was received, in seconds
 *     formatted as a float.
 * @param {Intent} intent Intent for sending messages as the relevant user.
 * @param {string} roomID Matrix room ID associated with channelID.
 */
//SlackHookHandler.prototype.lookupAndSendMessage =
var lookupMessage = function(channelID, timestamp, token) {
    // Look up all messages at the exact timestamp we received.
    // This has microsecond granularity, so should return the message we want.
    var params = {
        method: 'POST',
        form : {
            channel: channelID,
            latest: timestamp,
            oldest: timestamp,
            inclusive: "1",
            token: token,
        },
        uri: "https://slack.com/api/channels.history",
        json: true
    };
    return rp(params).then((response) => {
        if (!response || !response.messages || response.messages.length === 0) {
            console.log("Could not find history: " + response);
            return undefined;
        }
        if (response.messages.length != 1) {
            // Just laziness.
            // If we get unlucky and two messages were sent at exactly the
            // same microsecond, we could parse them all, filter by user,
            // filter by whether they have attachments, and such, and pick
            // the right message. But this is unlikely, and I'm lazy, so
            // we'll just drop the message...
            console.log("Really unlucky, got multiple messages at same" +
                " microsecond, dropping:" + response);
            return undefined;
        }
        var message = response.messages[0];
        console.log("Looked up message from history as " + JSON.stringify(message));
        return message;
    });
}

var lookupSharedPublicURL = function(file, token) {
    var params = {
        method: 'POST',
        form : {
            file: file.id,
            token: token,
        },
        uri: "https://slack.com/api/files.sharedPublicURL",
        json: true
    };
    return rp(params).then((response) => {
        if (!response || !response.file || !response.file.permalink_public) {
            console.log("Could not find sharedPublichURL: " + JSON.stringify(response));
            return undefined;
        }
        var file = response.file;
        console.log("Looked up sharedPublicURL as " + JSON.stringify(file));
        return file;
    });
}

var uploadContent = function(file, intent) {
    var params = {
        uri: file,
        resolveWithFullResponse: true,
        encoding: null
    };
    return rp(params).then((response) => {
        var content_type = response.headers["content-type"];

        return intent.getClient().uploadContent({
            stream: new Buffer(response.body, "binary"),
            name: file.title,
            type: content_type,
        });
    }).then((response) => {
        var content_uri = JSON.parse(response).content_uri;

        console.log("Media uploaded to " + content_uri);
        return content_uri;
    });

};

var sendMessage = function(message, intent, roomID, token) {
    if (!message.subtype) {
        intent.sendText(roomID, substitutions.slackToMatrix(message.text));
    }
    else if (message.subtype === "me_message") {
        intent.sendMessage(roomID, {
            msgtype: "m.emote",
            body: substitutions.slackToMatrix(message.text)
        });
    }
    else if (message.subtype === "file_share") {
        if (!message.file) {
            console.log("Ignoring non-text non-image message: " + res);
            return;
        }
        if (message.file.mimetype && message.file.mimetype.indexOf("image/") === 0) {
            // image uploaded in slack. we need to lookup its URL, make it public, and generate the direct public URL
            lookupSharedPublicURL(message.file, token).then((file) => {
                var pub_secret = file.permalink_public.match(/https?:\/\/slack-files.com\/[^-]*-[^-]*-(.*)/);
                var public_file_url = file.permalink_public;
                // try to get direct link to image
                if (pub_secret != undefined && pub_secret.length > 0) {
                    public_file_url = message.file.url_private + "?pub_secret=" + pub_secret[1];
                }
                // upload to media repo; get media repo URL back
                return uploadContent(public_file_url, intent).then((content_uri) => {
                    if(undefined == content_uri) {
                        // no URL returned from media repo; abort
                       return undefined;
                    }
                    var matrixMessage = slackImageToMatrixImage(message.file, content_uri);
                    intent.sendMessage(roomID, matrixMessage);
                });
            }).finally(() => {

            if (message.file.initial_comment) {
                var text = substitutions.slackToMatrix(
                    message.file.initial_comment.comment
                );
                intent.sendText(roomID, text);
            }
            });
        }
    }
    else {
        console.log("Ignoring message with subtype: " + message.subtype);
    }
};

/**
 * Converts a slack image attachment to a matrix image event.
 *
 * @param {Object} file The slack image attachment file object.
 * @param {string} file.url URL of the file.
 * @param {string} file.title alt-text for the file.
 * @param {string} file.mimetype mime-type of the file.
 * @param {?integer} file.size size of the file in bytes.
 * @param {?integer} file.original_w width of the file if an image, in pixels.
 * @param {?integer} file.original_h height of the file if an image, in pixels.
 * @param {?string} file.thumb_360 URL of a 360 pixel wide thumbnail of the
 *     file, if an image.
 * @param {?integer} file.thumb_360_w width of the thumbnail of the 360 pixel
 *     wide thumbnail of the file, if an image.
 * @param {?integer} file.thumb_360_h height of the thumbnail of the 36 pixel
 *     wide thumbnail of the file, if an image.
 * @return {Object} Matrix event content, as per https://matrix.org/docs/spec/#m-image
 */
var slackImageToMatrixImage = function(file, url) {
    var message = {
        msgtype: "m.image",
        url: url,
        body: file.title,
        info: {
            mimetype: file.mimetype
        }
    };
    if (file.original_w) {
        message.info.w = file.original_w;
    }
    if (file.original_h) {
        message.info.h = file.original_h;
    }
    if (file.size) {
        message.info.size = file.size;
    }
    if (false && file.thumb_360) {
        message.thumbnail_url = file.thumb_360;
        message.thumbnail_info = {};
        if (file.thumb_360_w) {
            message.thumbnail_info.w = file.thumb_360_w;
        }
        if (file.thumb_360_h) {
            message.thumbnail_info.h = file.thumb_360_h;
        }
    }
    return message;
};

SlackHookHandler.prototype.checkAuth = function(params) {
    return params.token &&
        params.token === this.rooms.tokenForSlackChannel(params.channel_id);
};

module.exports = SlackHookHandler;
<commit_msg>Fix an issue with mentions in emote msgs<commit_after>"use strict";

var substitutions = require("./substitutions");
var rp = require('request-promise');
var Promise = require('bluebird');
/**
 * @constructor
 * @param {request} requestLib request library, for sending HTTP requests.
 * @param {Object} config the configuration of the bridge.
 *     See ../config/slack-config-schema.yaml for the schema to which this must conform.
 * @param {Rooms} rooms mapping of all known slack channels to matrix rooms.
 * @param {Bridge} bridge the matrix-appservice-bridge bridge through which to
 *     communicate with matrix.
 */
function SlackHookHandler(requestLib, config, rooms, bridge) {
    this.requestLib = requestLib;
    this.config = config;
    this.rooms = rooms;
    this.bridge = bridge;
}

SlackHookHandler.prototype.getIntent = function(slackUser) {
    var username = "@" + this.config.username_prefix + slackUser +
        ":" + this.config.homeserver.server_name;
    return this.bridge.getIntent(username);
};

/**
 * Handles a slack webhook request.
 *
 * Sends a message to Matrix if it understands enough of the message to do so.
 * Attempts to make the message as native-matrix feeling as it can.
 *
 * @param {Object} params HTTP body of the webhook request, as a JSON-parsed dictionary.
 * @param {string} params.channel_id Slack channel ID receiving the message.
 * @param {string} params.channel_name Slack channel name receiving the message.
 * @param {string} params.user_id Slack user ID of user sending the message.
 * @param {string} params.user_name Slack user name of the user sending the message.
 * @param {?string} params.text Text contents of the message, if a text message.
 * @param {string} timestamp Timestamp when message was received, in seconds
 *     formatted as a float.
 */
SlackHookHandler.prototype.handle = function(params) {
    console.log("Received slack webhook request: " + JSON.stringify(params));
    if (params.user_id === "USLACKBOT") {
        return;
    }
    if (!this.rooms.knowsSlackChannel(params.channel_id)) {
        console.log("Ignoring message for slack channel with unknown matrix ID: %s (%s)",
            params.channel_id, params.channel_name
        );
        return;
    }
    var intent = this.getIntent(params.user_name);
    var roomID = this.rooms.matrixRoomID(params.channel_id);
    var team_domain = params.team_domain;

    // TODO: store this somewhere
    intent.setDisplayName(params.user_name);

    if (!this.config["slack_token_" + team_domain]) {
        // If we can't look up more details about the message
        // (because we don't have a master token), but it has text,
        // just send the message as text.
        console.log("no slack token for " + team_domain);

        if (params.text) {
            intent.sendText(roomID, substitutions.slackToMatrix(params.text));
        }
        return;
    }
    var token = this.config["slack_token_" + team_domain];

    var text = params.text;
    if (undefined == text) {
        lookupAndSendMessage(params.channel_id, params.timestamp, intent, roomID, token);
        return;
    }

    lookupMessage(params.channel_id, params.timestamp, token).then((msg) => {
        if(undefined == msg) {
            msg = params;
        }
        console.log("the msg:" + JSON.stringify(msg));
        return replaceChannelIdsWithNames(msg, token);
    }).then((msg) => {
        return replaceUserIdsWithNames(msg, token);
    }).then((msg) => {
        // we can't use .finally here as it does not get the final value, see https://github.com/kriskowal/q/issues/589
        return sendMessage(msg, intent, roomID, token);
    },
    (msg) => {
        return sendMessage(msg, intent, roomID, token);
    });
};

function replaceChannelIdsWithNames(message, token) {
    // match all channelIds
    var testForName = message.text.match(/<#(\w+)\|?\w*?>/g);
    var iteration = 0;
    var matches = 0;
    if (testForName && testForName.length) {
        matches = testForName.length;
    }
    return promiseWhile(function() {
        // Do this until there are no more channel ID matches
        return iteration < matches;
    }, function() {
        // foreach channelId, pull out the ID
        // (if this is an emote msg, the format is <#ID|name>, but in normal msgs it's just <#ID>
        var id = testForName[iteration].match(/<#(\w+)\|?\w*?>/)[1];
        var channelsInfoApiParams = {
            uri: 'https://slack.com/api/channels.info',
            qs: {
                token: token,
                channel: id
            },
            json: true
        };
        return rp(channelsInfoApiParams).then((response) => {
            if (response && response.channel && response.channel.name) {
                console.log("channels.info: " + id + " mapped to " + response.channel.name);
                message.text = message.text.replace(/<#(\w+)\|?\w*?>/, "#" + response.channel.name);
            }
            else {
                console.log("channels.info returned no result for " + id);
            }
            iteration++;
            }).catch((err) => {
               console.log("Caught error " + err);
            });
    }).then(() => {
        // Notice we can chain it because it's a Promise,
        // this will run after completion of the promiseWhile Promise!
        return message;
    });
};

function replaceUserIdsWithNames(message, token) {
    // match all userIds
    var testForName = message.text.match(/<@(\w+)\|?\w*?>/g);
    var iteration = 0;
    var matches = 0;
    if (testForName && testForName.length) {
        matches = testForName.length;
    }
    return promiseWhile(() => {
        // Condition for stopping
        return iteration < matches;
    }, function() {
        // foreach userId, pull out the ID
        // (if this is an emote msg, the format is <@ID|nick>, but in normal msgs it's just <@ID>
        var id = testForName[iteration].match(/<@(\w+)\|?\w*?>/)[1];
        var channelsInfoApiParams = {
            uri: 'https://slack.com/api/users.info',
            qs: {
                token: token,
                user: id
            },
            json: true
        };
        return rp(channelsInfoApiParams).then((response) => {
            if (response && response.user && response.user.name) {
                console.log("users.info: " + id + " mapped to " + response.user.name);
                message.text = message.text.replace(/<@(\w+)\|?\w*?>/, response.user.name);
            }
            else {
                console.log("users.info returned no result for " + id);
            }
            iteration++;
            }).catch((err) => {
               console.log("Caught error " + err);
            });
    }).then(() => {
        // Notice we can chain it because it's a Promise,
        // this will run after completion of the promiseWhile Promise!
        return message;
    });
};

// promiseWhile code from http://blog.victorquinn.com/javascript-promise-while-loop
var promiseWhile = function(condition, action) {
    var resolver = Promise.defer();

    var loop = function() {
        if (!condition()) return resolver.resolve();
        return Promise.cast(action())
            .then(loop)
            .catch(resolver.reject);
    };

    process.nextTick(loop);

    return resolver.promise;
};

/**
 * Attempts to handle a message received from a slack webhook request.
 *
 * The webhook request that we receive doesn't have enough information to richly
 * represent the message in Matrix, so we look up more details.
 *
 * @param {string} channelID Slack channel ID.
 * @param {string} timestamp Timestamp when message was received, in seconds
 *     formatted as a float.
 * @param {Intent} intent Intent for sending messages as the relevant user.
 * @param {string} roomID Matrix room ID associated with channelID.
 */
//SlackHookHandler.prototype.lookupAndSendMessage =
var lookupMessage = function(channelID, timestamp, token) {
    // Look up all messages at the exact timestamp we received.
    // This has microsecond granularity, so should return the message we want.
    var params = {
        method: 'POST',
        form : {
            channel: channelID,
            latest: timestamp,
            oldest: timestamp,
            inclusive: "1",
            token: token,
        },
        uri: "https://slack.com/api/channels.history",
        json: true
    };
    return rp(params).then((response) => {
        if (!response || !response.messages || response.messages.length === 0) {
            console.log("Could not find history: " + response);
            return undefined;
        }
        if (response.messages.length != 1) {
            // Just laziness.
            // If we get unlucky and two messages were sent at exactly the
            // same microsecond, we could parse them all, filter by user,
            // filter by whether they have attachments, and such, and pick
            // the right message. But this is unlikely, and I'm lazy, so
            // we'll just drop the message...
            console.log("Really unlucky, got multiple messages at same" +
                " microsecond, dropping:" + response);
            return undefined;
        }
        var message = response.messages[0];
        console.log("Looked up message from history as " + JSON.stringify(message));
        return message;
    });
}

var lookupSharedPublicURL = function(file, token) {
    var params = {
        method: 'POST',
        form : {
            file: file.id,
            token: token,
        },
        uri: "https://slack.com/api/files.sharedPublicURL",
        json: true
    };
    return rp(params).then((response) => {
        if (!response || !response.file || !response.file.permalink_public) {
            console.log("Could not find sharedPublichURL: " + JSON.stringify(response));
            return undefined;
        }
        var file = response.file;
        console.log("Looked up sharedPublicURL as " + JSON.stringify(file));
        return file;
    });
}

var uploadContent = function(file, intent) {
    var params = {
        uri: file,
        resolveWithFullResponse: true,
        encoding: null
    };
    return rp(params).then((response) => {
        var content_type = response.headers["content-type"];

        return intent.getClient().uploadContent({
            stream: new Buffer(response.body, "binary"),
            name: file.title,
            type: content_type,
        });
    }).then((response) => {
        var content_uri = JSON.parse(response).content_uri;

        console.log("Media uploaded to " + content_uri);
        return content_uri;
    });

};

var sendMessage = function(message, intent, roomID, token) {
    if (!message.subtype) {
        intent.sendText(roomID, substitutions.slackToMatrix(message.text));
    }
    else if (message.subtype === "me_message") {
        intent.sendMessage(roomID, {
            msgtype: "m.emote",
            body: substitutions.slackToMatrix(message.text)
        });
    }
    else if (message.subtype === "file_share") {
        if (!message.file) {
            console.log("Ignoring non-text non-image message: " + res);
            return;
        }
        if (message.file.mimetype && message.file.mimetype.indexOf("image/") === 0) {
            // image uploaded in slack. we need to lookup its URL, make it public, and generate the direct public URL
            lookupSharedPublicURL(message.file, token).then((file) => {
                var pub_secret = file.permalink_public.match(/https?:\/\/slack-files.com\/[^-]*-[^-]*-(.*)/);
                var public_file_url = file.permalink_public;
                // try to get direct link to image
                if (pub_secret != undefined && pub_secret.length > 0) {
                    public_file_url = message.file.url_private + "?pub_secret=" + pub_secret[1];
                }
                // upload to media repo; get media repo URL back
                return uploadContent(public_file_url, intent).then((content_uri) => {
                    if(undefined == content_uri) {
                        // no URL returned from media repo; abort
                       return undefined;
                    }
                    var matrixMessage = slackImageToMatrixImage(message.file, content_uri);
                    intent.sendMessage(roomID, matrixMessage);
                });
            }).finally(() => {

            if (message.file.initial_comment) {
                var text = substitutions.slackToMatrix(
                    message.file.initial_comment.comment
                );
                intent.sendText(roomID, text);
            }
            });
        }
    }
    else {
        console.log("Ignoring message with subtype: " + message.subtype);
    }
};

/**
 * Converts a slack image attachment to a matrix image event.
 *
 * @param {Object} file The slack image attachment file object.
 * @param {string} file.url URL of the file.
 * @param {string} file.title alt-text for the file.
 * @param {string} file.mimetype mime-type of the file.
 * @param {?integer} file.size size of the file in bytes.
 * @param {?integer} file.original_w width of the file if an image, in pixels.
 * @param {?integer} file.original_h height of the file if an image, in pixels.
 * @param {?string} file.thumb_360 URL of a 360 pixel wide thumbnail of the
 *     file, if an image.
 * @param {?integer} file.thumb_360_w width of the thumbnail of the 360 pixel
 *     wide thumbnail of the file, if an image.
 * @param {?integer} file.thumb_360_h height of the thumbnail of the 36 pixel
 *     wide thumbnail of the file, if an image.
 * @return {Object} Matrix event content, as per https://matrix.org/docs/spec/#m-image
 */
var slackImageToMatrixImage = function(file, url) {
    var message = {
        msgtype: "m.image",
        url: url,
        body: file.title,
        info: {
            mimetype: file.mimetype
        }
    };
    if (file.original_w) {
        message.info.w = file.original_w;
    }
    if (file.original_h) {
        message.info.h = file.original_h;
    }
    if (file.size) {
        message.info.size = file.size;
    }
    if (false && file.thumb_360) {
        message.thumbnail_url = file.thumb_360;
        message.thumbnail_info = {};
        if (file.thumb_360_w) {
            message.thumbnail_info.w = file.thumb_360_w;
        }
        if (file.thumb_360_h) {
            message.thumbnail_info.h = file.thumb_360_h;
        }
    }
    return message;
};

SlackHookHandler.prototype.checkAuth = function(params) {
    return params.token &&
        params.token === this.rooms.tokenForSlackChannel(params.channel_id);
};

module.exports = SlackHookHandler;
<|endoftext|>


<commit_before>var User = require('./../../models/user').User;
var Chat = require('./../../models/chat').Chat;
var async = require('async');

var socket_outgoing = {
  UPDATE_ONLINE: 'CHAT_IN_UPDATE_ONLINE',
  INCREMENT_ONLINE: 'CHAT_IN_INCREMENET_ONLINE',
  DECREMENT_ONLINE: 'CHAT_IN_DECREMENT_ONLINE',
  MUTE_USER: 'CHAT_IN_MUTE_USER',
  UNMUTE_USER: 'CHAT_IN_UNMUTE_USER',
  BAN_USER: 'CHAT_IN_BAN_USER',
  UNBAN_USER: 'CHAT_IN_UNBAN_USER',
  RECEIVE_MESSAGE: 'CHAT_IN_RECEIVE_MESSAGE',
  RELOAD_PAGE: 'CHAT_IN_RELOAD_PAGE',
  CLEAR_CHAT: 'CHAT_IN_CLEAR_CHAT',
  BOT_MESSAGE: 'CHAT_IN_BOT_MESSAGE',
  CHAT_MODE: 'CHAT_IN_CHAT_MODE',
  INIT_CHAT: 'CHAT_IN_INIT_CHAT',
  ALERT: 'ALERT'
};

var socket_incoming = {
  SEND_CHAT: 'CHAT_OUT_SEND_MESSAGE',
  MUTE_USER: 'CHAT_OUT_MUTE_USER',
  UNMUTE_USER: 'CHAT_OUT_UNMUTE_USER',
  BAN_USER: 'CHAT_OUT_BAN_USER',
  UNBAN_USER: 'CHAT_OUT_UNBAN_USER',
  RELOAD_PAGE: 'CHAT_OUT_RELOAD_PAGE',
  CLEAR_CHAT: 'CHAT_OUT_CLEAR_CHAT',
  BOT_MESSAGE: 'CHAT_OUT_BOT_MESSAGE',
  CHAT_MODE: 'CHAT_OUT_CHAT_MODE',
  INIT_CHAT: 'CHAT_OUT_INIT_CHAT'
};

var RECENT_MESSAGE_CAP =  50;

var recentMessages = []; //queue the 50 most recent messages for newcomers so we dont have to query the database

module.exports = function(socket, io) {

  socket.on(socket_incoming.INIT_CHAT, function() {
    var currentUsers = Object.keys(io.sockets.sockets).length;

    if (recentMessages.length < RECENT_MESSAGE_CAP) { //we need to query the database for messages if we dont have enough recent messages (very rarely)
      var previousMessages = [];

      var data = [];

      async.series([
        function(callback) {
          Chat.getRecentMessages(RECENT_MESSAGE_CAP, function(err, result) {
            if (!err && result) {
              data = result;
              callback();
            } else {
              callback(err);
            }
          });
        },
        function(callback) {
          var counter = 0;
          if (data.length > 0) {
            data.forEach(function(obj) {
              obj.formatChatMessage(function(err, msg) {
                if (!err && msg) {
                  previousMessages.push(msg);
                  counter++;
                  if (data.length == counter) {
                    callback();
                  }
                } else {
                  callback(err);
                }
              });
            });
          } else {
            callback(new Error('no data locally'));
          }
        }
      ], function(err, results) {
        if (!err) {
          socket.emit(socket_outgoing.INIT_CHAT, {
            previous_messages: previousMessages.reverse(),
            current_users: currentUsers
          });
          recentMessages = previousMessages.reverse();
        } else {
          socket.emit(socket_outgoing.ALERT, {
            header: 'Chat Error',
            type: 'error',
            message: 'Unable to load previous messages'
          });
        }
      });
    } else {
      var reversedMessages = [].concat(recentMessages).reverse();
      socket.emit(socket_outgoing.INIT_CHAT, {
        current_users: currentUsers,
        previous_messages: reversedMessages
      });
    }

    socket.broadcast.emit(socket_outgoing.INCREMENT_ONLINE);
  });

  socket.on(socket_incoming.SEND_CHAT, function(data) {
    if (socket.request.session.passport.user) {
      User.findById(socket.request.session.passport.user, function(err, user) {
        if (!err && user) {
          var chatData = {
            id: user._id,
            profile_name: user.name,
            profile_img: user.photo,
            rank: user.rank,
            text: data.text
          }
          io.emit(socket_outgoing.RECEIVE_MESSAGE, chatData);
          recentMessages.unshift(chatData);
          recentMessages.length = RECENT_MESSAGE_CAP;

          var chatModel = new Chat({
            sender_id: user._id,
            content: data.text
          });
          chatModel.save(function(err) {
            if (err) {
              socket.emit(socket_outgoing.ALERT, {
                type: 'error',
                message: 'Error while saving your chat to the database. Please contact the developers.',
                header: 'Chat Error'
              });
            }
          });
        } else {
          socket.emit(socket_outgoing.ALERT, {
            type: 'error',
            message: 'Error while processing. Please contact developers with this message: ' + err.message,
            header: 'Chat Error'
          });
        }
      });
    } else {
      socket.emit(socket_outgoing.ALERT, {
        type: 'error',
        message: 'You are not logged in.',
        header: 'Chat Error'
      });
    }
  });

  socket.on(socket_incoming.MUTE_USER, function(data) {

  });

  socket.on(socket_outgoing.UNMUTE_USER, function(data) {

  });

  socket.on(socket_outgoing.BAN_USER, function(data) {

  });

  socket.on(socket_outgoing.UNBAN_USER, function(data) {

  });

  socket.on(socket_outgoing.RELOAD_PAGE, function(data) {

  });

  socket.on(socket_outgoing.CLEAR_CHAT, function(data) {

  });

  socket.on(socket_outgoing.BOT_MESSAGE, function(data) {

  });

  socket.on(socket_outgoing.CHAT_MODE, function(data) {

  });

  socket.on('disconnect', function() {
    io.emit(socket_outgoing.DECREMENT_ONLINE);
  });
}
<commit_msg>show error messages<commit_after>var User = require('./../../models/user').User;
var Chat = require('./../../models/chat').Chat;
var async = require('async');

var socket_outgoing = {
  UPDATE_ONLINE: 'CHAT_IN_UPDATE_ONLINE',
  INCREMENT_ONLINE: 'CHAT_IN_INCREMENET_ONLINE',
  DECREMENT_ONLINE: 'CHAT_IN_DECREMENT_ONLINE',
  MUTE_USER: 'CHAT_IN_MUTE_USER',
  UNMUTE_USER: 'CHAT_IN_UNMUTE_USER',
  BAN_USER: 'CHAT_IN_BAN_USER',
  UNBAN_USER: 'CHAT_IN_UNBAN_USER',
  RECEIVE_MESSAGE: 'CHAT_IN_RECEIVE_MESSAGE',
  RELOAD_PAGE: 'CHAT_IN_RELOAD_PAGE',
  CLEAR_CHAT: 'CHAT_IN_CLEAR_CHAT',
  BOT_MESSAGE: 'CHAT_IN_BOT_MESSAGE',
  CHAT_MODE: 'CHAT_IN_CHAT_MODE',
  INIT_CHAT: 'CHAT_IN_INIT_CHAT',
  ALERT: 'ALERT'
};

var socket_incoming = {
  SEND_CHAT: 'CHAT_OUT_SEND_MESSAGE',
  MUTE_USER: 'CHAT_OUT_MUTE_USER',
  UNMUTE_USER: 'CHAT_OUT_UNMUTE_USER',
  BAN_USER: 'CHAT_OUT_BAN_USER',
  UNBAN_USER: 'CHAT_OUT_UNBAN_USER',
  RELOAD_PAGE: 'CHAT_OUT_RELOAD_PAGE',
  CLEAR_CHAT: 'CHAT_OUT_CLEAR_CHAT',
  BOT_MESSAGE: 'CHAT_OUT_BOT_MESSAGE',
  CHAT_MODE: 'CHAT_OUT_CHAT_MODE',
  INIT_CHAT: 'CHAT_OUT_INIT_CHAT'
};

var RECENT_MESSAGE_CAP =  50;

var recentMessages = []; //queue the 50 most recent messages for newcomers so we dont have to query the database

module.exports = function(socket, io) {

  socket.on(socket_incoming.INIT_CHAT, function() {
    var currentUsers = Object.keys(io.sockets.sockets).length;

    if (recentMessages.length < RECENT_MESSAGE_CAP) { //we need to query the database for messages if we dont have enough recent messages (very rarely)
      var previousMessages = [];

      var data = [];

      async.series([
        function(callback) {
          Chat.getRecentMessages(RECENT_MESSAGE_CAP, function(err, result) {
            if (!err && result) {
              data = result;
              callback();
            } else {
              callback(err);
            }
          });
        },
        function(callback) {
          var counter = 0;
          if (data.length > 0) {
            data.forEach(function(obj) {
              obj.formatChatMessage(function(err, msg) {
                if (!err && msg) {
                  previousMessages.push(msg);
                  counter++;
                  if (data.length == counter) {
                    callback();
                  }
                } else {
                  callback(err);
                }
              });
            });
          } else {
            callback(new Error('no data locally'));
          }
        }
      ], function(err, results) {
        if (!err) {
          socket.emit(socket_outgoing.INIT_CHAT, {
            previous_messages: previousMessages.reverse(),
            current_users: currentUsers
          });
          recentMessages = previousMessages.reverse();
        } else {
          socket.emit(socket_outgoing.ALERT, {
            header: 'Chat Error',
            type: 'error',
            message: 'Unable to load previous messages. Error: ' + err.message
          });
        }
      });
    } else {
      var reversedMessages = [].concat(recentMessages).reverse();
      socket.emit(socket_outgoing.INIT_CHAT, {
        current_users: currentUsers,
        previous_messages: reversedMessages
      });
    }

    socket.broadcast.emit(socket_outgoing.INCREMENT_ONLINE);
  });

  socket.on(socket_incoming.SEND_CHAT, function(data) {
    if (socket.request.session.passport.user) {
      User.findById(socket.request.session.passport.user, function(err, user) {
        if (!err && user) {
          var chatData = {
            id: user._id,
            profile_name: user.name,
            profile_img: user.photo,
            rank: user.rank,
            text: data.text
          }
          io.emit(socket_outgoing.RECEIVE_MESSAGE, chatData);
          recentMessages.unshift(chatData);
          recentMessages.length = RECENT_MESSAGE_CAP;

          var chatModel = new Chat({
            sender_id: user._id,
            content: data.text
          });
          chatModel.save(function(err) {
            if (err) {
              socket.emit(socket_outgoing.ALERT, {
                type: 'error',
                message: 'Error while saving your chat to the database. Please contact the developers.',
                header: 'Chat Error'
              });
            }
          });
        } else {
          socket.emit(socket_outgoing.ALERT, {
            type: 'error',
            message: 'Error while processing. Please contact developers with this message: ' + err.message,
            header: 'Chat Error'
          });
        }
      });
    } else {
      socket.emit(socket_outgoing.ALERT, {
        type: 'error',
        message: 'You are not logged in.',
        header: 'Chat Error'
      });
    }
  });

  socket.on(socket_incoming.MUTE_USER, function(data) {

  });

  socket.on(socket_outgoing.UNMUTE_USER, function(data) {

  });

  socket.on(socket_outgoing.BAN_USER, function(data) {

  });

  socket.on(socket_outgoing.UNBAN_USER, function(data) {

  });

  socket.on(socket_outgoing.RELOAD_PAGE, function(data) {

  });

  socket.on(socket_outgoing.CLEAR_CHAT, function(data) {

  });

  socket.on(socket_outgoing.BOT_MESSAGE, function(data) {

  });

  socket.on(socket_outgoing.CHAT_MODE, function(data) {

  });

  socket.on('disconnect', function() {
    io.emit(socket_outgoing.DECREMENT_ONLINE);
  });
}
<|endoftext|>


<commit_before>

"use strict";

var LinkColorHandler = function() { };

/**
 * Constants for link colors
 */
LinkColorHandler.prototype._CONSTANTS = {

		// default distances associated with distance-based coloring (in Angstroms)
		linkers : {
			dss : {
				shortDistance : 25,
				longDistance : 35
			},
			bs3 : {
				shortDistance : 25,
				longDistance : 35
			},
			dsg : {
				shortDistance : 20,
				longDistance : 30
			},
			bs2 : {
				shortDistance : 20,
				longDistance : 25
			},
			edc : {
				shortDistance : 10,
				longDistance : 20
			},
			generic : {
				shortDistance : 25,
				longDistance : 35
			},

		},

		searchColors_3searches : {
			1:		"#FF0000",			// red, for items belonging only to first search
			2:		"#0000FF",			// blue, for items belonging only to second search
			3:		"#dcd900",			// mustard yellow, for items belonging only to third search
			12:		"#8a51ff",			// purple, for items belonging to first and second search
			13:		"#FF6600",			// orange, for items belonging to first and third search
			23:		"#006600",			// green, for items belonging to second and third search
			123:	"#000000",			// black, for items belonging to all three searches
		},

		searchColors_2searches : {
			1:		"#FF0000",			// red, for items only in first search
			2:		"#0000FF",			// blue for items only in second search
			12:		"#00FF00",			// green for items in both searches
		}

};

<commit_msg>Further work on user-customizable distance-based coloring.<commit_after>

"use strict";

var LinkColorHandler = function() { };

/**
 * Constants for link colors
 */
LinkColorHandler.prototype._CONSTANTS = {

		// default distances associated with distance-based coloring (in Angstroms)
		linkers : {
			dss : {
				shortDistance : 25,
				longDistance : 35
			},
			bs3 : {
				shortDistance : 25,
				longDistance : 35
			},
			dsg : {
				shortDistance : 20,
				longDistance : 30
			},
			bs2 : {
				shortDistance : 20,
				longDistance : 25
			},
			edc : {
				shortDistance : 10,
				longDistance : 20
			},
			generic : {
				shortDistance : 25,
				longDistance : 35
			},

		},

		searchColors_3searches : {
			1:		"#FF0000",			// red, for items belonging only to first search
			2:		"#0000FF",			// blue, for items belonging only to second search
			3:		"#dcd900",			// mustard yellow, for items belonging only to third search
			12:		"#8a51ff",			// purple, for items belonging to first and second search
			13:		"#FF6600",			// orange, for items belonging to first and third search
			23:		"#006600",			// green, for items belonging to second and third search
			123:	"#000000",			// black, for items belonging to all three searches
		},

		searchColors_2searches : {
			1:		"#FF0000",			// red, for items only in first search
			2:		"#0000FF",			// blue for items only in second search
			12:		"#00FF00",			// green for items in both searches
		},

		// colors to use for the type of link
		typeColors : {
			crosslink : "#FF0000",
			looplink  : "#0000FF",
			monolink  : "#A243E4",
			unknown   : "#000000",
		},

		defaultOpacity : 0.9,			// default opacity to use for links
};

/**
 * Get the color for the specified link.
 * @param link The link for which we want a color
 * @param colorFormat The format for the color returned. Possibilities are:
 * 		'hex' : The color as a string hexadecimal (e.g., "#FF0000" for red)
 * 		'rgb' : The color as integers assigned to each channel (e.g., { r:255, g:0, b:0 } for red )
 * 		'pvrgba' : The color as an array of decimals [r, g, b, opacity] expected by the pv viewer: ( [ 1.0, 0.0, 0.0, 0.75 ] for red at 75% opacity )
 */
LinkColorHandler.prototype.getLinkColor = function( link, colorFormat ) {
	if( !link ) {
		console.log( "ERROR, got not link in getLinkColor" );
		return;
	}

	if( !colorFormat ) {
		console.log( "ERROR, got no colorFormat for getLinkColor" );
		return;
	}

};

/**
 * Return the supplied hex and opacity as an array of decimal rgba values as expected by the pv structure viewer
 * @param hex The hex code of the color (e.g., #FF0000 for red)
 * @param opacity The opacity (from 0 - 1)
 * @returns [ r (0-1), g (0-1), b (0-1), opacity ]
 */
LinkColorHandler.prototype.hexToRgbaDecimalArray = function( hex, opacity ) {
	var rgb = this.hexToRgb( hex );
	return rgb ? [ (rgb.r / 255), (rgb.g / 255 ),  (rgb.b / 255 ), opacity ] : null;
};

/**
 * Convert hex to rgb
 * @returns { r: redValue, g: greenValue, b: blueValue } (all values 0-255)
 */
LinkColorHandler.prototype.hexToRgb = function( hex ) {
    // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
    var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
    hex = hex.replace(shorthandRegex, function(m, r, g, b) {
        return r + r + g + g + b + b;
    });

    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
    } : null;
};

/**
 * Get the opacity to use for the given link's color
 * @param link The link we're testing
 * @param forReport true if for the distance report table, false if not
 */
LinkColorHandler.prototype.getOpacity = function( link ) {

	var shadeByCounts = false;
	if ( $( "input#shade-by-counts" ).is( ':checked' ) ) { shadeByCounts = true; }

	if( shadeByCounts && !('psmCount' in link)) {
		console.log( "ERROR: Was told to shade by counts, but link has no PSM count Using default opacity. Link:" );
		console.log( link );
	}

	var opacity = this._CONSTANTS.defaultOpacity;

	if( shadeByCounts && 'psmCount' in link ) {
		var psmCount = link[ 'psmCount' ];
		var min = 0.5;
		var max = opacity;
		var countForMaxOpacity = 10;

		if( psmCount > countForMaxOpacity ) { psmCount = countForMaxOpacity; }

		opacity = max - ( ( max - min ) * ( ( countForMaxOpacity - psmCount ) / countForMaxOpacity ) );
		if( opacity < min || opacity > max ) {
			console.log( "WARNING: Invalid opacity: " + opacity  + " (psmCount: " + psmCount + ")" );
		}

	}

	return opacity;
}

LinkColorHandler.prototype.setUserDistanceConstraints = function(short, long) {
	this.userDistanceConstraint = {
		shortDistance : short,
		longDistance : long
	};
}

LinkColorHandler.prototype.getUserDistanceConstraints = function() {
	if( !( 'userDistanceConstraint' in this ) ) { return undefined; }
	return this.userDistanceConstraint;
}

/**
 * Get the distance restraints to use for coloring, given the linkers used in the current
 * search(es).
 */
LinkColorHandler.prototype.getDistanceConstraints = function() {

	// if the user has set custom constraints, use them
	if( this.getUserDistanceConstraints() ) {
		return this.getUserDistanceConstraints();
	}

	var linkers = getLinkerStringsAsArray();

	if( !linkers || linkers.length < 1 ) {
		return this._CONSTANTS.linkers.generic;
	}

	if( linkers.length == 1 ) {
		if( linkers[ 0 ] in this._CONSTANTS.linkers ) {
			return this._CONSTANTS.linkers[ linkers[ 0 ] ];
		} else {
			this._CONSTANTS.linkers.generic;
		}
	}

	// more than one linker was used. color based on the "longer" of the two, as
	// determined by the "longDistance" property.
	var longLinker;
	for( var i = 0; i < linkers.length; i++ ) {
		if( linkers[ i ] in this._CONSTANTS.linkers ) {
			var tLinker = this._CONSTANTS.linkers[ linkers[ i ] ];

			if( !longLinker || longLinker.longDistance < tLinker.longDistance ) {
				longLinker = tLinker;
			}
		}
	}

	if( !longLinker ) { longLinker = this._CONSTANTS.linkers.generic; }

	return longLinker;
};<|endoftext|>


<commit_before>var stream = require('stream');
var util = require('util');
var serializedType = require('./stream-util').serializedType;
var StringDecoder = require('string_decoder').StringDecoder;

module.exports = Deserialize;
util.inherits(Deserialize, stream.Transform);

function Deserialize() {
    stream.Transform.call(this);
    this._writableState.objectMode = false;
    this._readableState.objectMode = true;
    this._buffer = '';
    this._decoder = new StringDecoder('utf8');
}

Deserialize.prototype._transform = function(chunk, enc, callback) {
    var push = this.push,
        emit = this.emit;

    this._buffer += this._decoder.write(chunk);
    var lines = this._buffer.split(/\r?\n/);
    this._buffer = lines.pop();

    var obj, line;
    for (var i = 0; i < lines.length; i++) {
        line = lines[i];

        // Determination of tile vs. info
        try { obj = serializedType(line); }
        catch(err) { return callback(err); }

        try { obj.deserialize(line); }
        catch (err) { return callback(err); }
        this.push(obj);
    }

    callback();
};

Deserialize.prototype._flush = function(callback) {
    var leftover = this._buffer.trim();
    if (leftover) {
        var obj;
        try { obj = serializedType(leftover); }
        catch(err) { return callback(err); }
        try { obj.deserialize(leftover); }
        catch (err) { return callback(err); }
        this.push(obj);
    }

    callback();
};
<commit_msg>add Info event emitter<commit_after>var stream = require('stream');
var util = require('util');
var Info = require('./stream-util').Info;
var serializedType = require('./stream-util').serializedType;
var StringDecoder = require('string_decoder').StringDecoder;

module.exports = Deserialize;
util.inherits(Deserialize, stream.Transform);

function Deserialize() {
    stream.Transform.call(this);
    this._writableState.objectMode = false;
    this._readableState.objectMode = true;
    this._buffer = '';
    this._decoder = new StringDecoder('utf8');
}

Deserialize.prototype._transform = function(chunk, enc, callback) {
    var push = this.push,
        emit = this.emit;

    this._buffer += this._decoder.write(chunk);
    var lines = this._buffer.split(/\r?\n/);
    this._buffer = lines.pop();

    var obj, line;
    for (var i = 0; i < lines.length; i++) {
        line = lines[i];

        // Determination of tile vs. info
        try { obj = serializedType(line); }
        catch(err) { return callback(err); }

        try { obj.deserialize(line); }
        catch (err) { return callback(err); }
        if(obj instanceof Info) emit('info', obj);
        this.push(obj);
    }

    callback();
};

Deserialize.prototype._flush = function(callback) {
    var leftover = this._buffer.trim();
    if (leftover) {
        var obj;
        try { obj = serializedType(leftover); }
        catch(err) { return callback(err); }
        try { obj.deserialize(leftover); }
        catch (err) { return callback(err); }
        this.push(obj);
    }

    callback();
};
<|endoftext|>


<commit_before>// Start server cluster
//
// In cluster mode it forks N http workers + N queue workers.
//
// In single mode it emits `init:server.worker-http` and
// `init:server.worker-queue` events instead
//
'use strict';

const _            = require('lodash');
const cluster      = require('cluster');
const co           = require('co');
const execFile     = require('mz/child_process').execFile;
const os           = require('os');
const inherits     = require('util').inherits;
const EventEmitter = require('events').EventEmitter;
const StateMachine = require('javascript-state-machine');

////////////////////////////////////////////////////////////////////////////////

function WorkerPool(size, worker) {
  if (!(this instanceof WorkerPool)) return new WorkerPool(size, worker);

  EventEmitter.call(this);

  this.__workers = [];
  this.__wClass  = worker;
  this.__size    = 0;

  this.update = _.debounce(() => {
    process.nextTick(() => {
      this.resize();
      this.gc();
    });
  }, 100, { maxWait: 200 });

  this.resize(size);
}

WorkerPool.prototype = {
  log() {
    /*eslint-disable no-console*/
    console.log.apply(null, arguments);
  },

  resize(count) {
    // Use current value if no params
    if (!count && count !== 0) {
      count = this.__size;
    }

    let alive = this.get([ 'INIT', 'START', 'RUN' ]),
        delta = count - alive.length,
        prev_size = this.__size;

    this.__size = count;

    if (delta === 0) return;

    if (delta > 0) {
      while (delta--) {
        let w = this.__wClass(this);
        this.__workers.push(w);
        // Force immediate start for the first run
        if (prev_size === 0 && w.can('start')) w.start();
      }
      return;
    }

    alive.slice(0, -delta).forEach(w => { w.shutdown(); });
  },

  terminate() {
    // prevent workers from being respawned
    this.__size = 0;

    this.__workers.forEach(w => { w.die(); });
  },

  reload() {
    this.__workers.forEach(w => {
      if (w.can('freeze')) w.freeze();
    });
  },

  get(state) {
    if (!Array.isArray(state)) {
      return this.__workers.filter(w => w.current === state);
    }
    return state.reduce((a, s) => a.concat(this.get(s)), []);
  },

  gc() {
    // Shutdown pending workers when not needed
    let excess = Math.max((this.get('PEND').length + this.get('RUN').length - this.__size), 0);

    this.get('PEND').slice(0, excess).forEach(w => { w.shutdown(); });

    // Drop dead workers
    this.__workers = this.__workers.filter(w => w.current !== 'DEAD');
  }
};

Object.setPrototypeOf(WorkerPool.prototype, EventEmitter.prototype);

////////////////////////////////////////////////////////////////////////////////

function Worker(pool) {
  if (!(this instanceof Worker)) return new Worker(pool);
  this.__pool          = pool;
  this.__w             = null;
  this.__start_timeout = 0;
  this.__exit_code     = null;

  this.begin();
}

Worker.prototype = {
  create() {
    throw new Error('This method should be overriden');
  },

  onchangestate() {
    this.__pool.update();
  },

  onINIT() {
    setTimeout(() => {
      if (this.can('start')) this.start();
    }, 2000);
  },

  onSTART() {
    let w = this.__w = this.fork();
    let rcvd_startup_message = false;

    this.__pool.log.info(`Worker ${w.process.pid} spawned`);
    this.__pool.emit('spawn', w.process.pid);

    w.on('error', err => {
      this.__pool.log.error(err);
      this.die();
    });

    w.on('exit', (code, signal) => {
      this.__exit_code = signal || code;
      this.die();
    });

    w.on('message', msg => {
      if (msg === 'worker.running') {
        rcvd_startup_message = true;
        if (this.can('run')) this.run();
      }
    });

    setTimeout(() => {
      if (!rcvd_startup_message) this.die();
    }, 30000);
  },

  onSTOP() {
    let w = this.__w;
    process.kill(w.process.pid, 'SIGTERM');
    setTimeout(() => this.die(), 15000);
  },

  onRUN() {
    let w = this.__w;
    this.__pool.log.info(`Worker ${w.process.pid} is running`);
    this.__pool.emit('running', w.process.pid);
  },

  onDEAD() {
    let w = this.__w;

    if (w && !w.isDead()) {
      // use SIGKILL instead of default SIGTERM
      // to guarantee that worker process will stop
      process.kill(w.process.pid, 'SIGKILL');
      w.disconnect();
      this.__exit_code = 'SIGKILL';
    }

    if (w) {
      this.__pool.log.info(`Worker ${w.process.pid} exited with status ${this.__exit_code}`);
      this.__pool.emit('exit', w.process.pid);
    }
  }
};

StateMachine.create({
  events: [
    { name: 'begin',     from: 'none',   to: 'INIT'  },
    { name: 'start',     from: 'INIT',   to: 'START' },
    { name: 'run',       from: 'START',  to: 'RUN'   },

    { name: 'freeze',    from: 'RUN',    to: 'PEND'  },
    { name: 'freeze',    from: 'START',  to: 'DEAD'  },

    { name: 'shutdown',  from: 'RUN',    to: 'STOP'  },
    { name: 'shutdown',  from: 'PEND',   to: 'STOP'  },

    { name: 'die',       from: '*',      to: 'DEAD'  }
  ]
}, Worker.prototype);

////////////////////////////////////////////////////////////////////////////////

function WorkerQueue(pool) {
  if (!(this instanceof WorkerQueue)) return new WorkerQueue(pool);
  Worker.call(this, pool);
}

inherits(WorkerQueue, Worker);

WorkerQueue.prototype.fork = function () {
  cluster.setupMaster({ args: [ 'worker-queue' ] });

  let worker = cluster.fork(),
      log = this.__pool.log;

  worker.on('online', co.wrap(function* () {
    try {
      // Set scheduling policy to SCHED_IDLE (`-i` flag);
      // `0` is only possible value for priority ('cause this policy doesn't allow to set it)
      yield execFile('chrt', [ '-i', '-p', '0', worker.process.pid ]);
    } catch (__) {
      // If `chrt` not exists, try fallback to `renice`.
      try {
        yield execFile('renice', [ '-n', '19', '-p', worker.process.pid ]);
        log.warn('Cannot set scheduling policy for queue using `chrt`, falling back to `renice`');
      } catch (___) {
        log.error('Cannot lower priority for queue ' +
          '(both `renice` and `chrt` have failed), continuing with default priority');
      }
    }
  }));

  return worker;
};

////////////////////////////////////////////////////////////////////////////////

function WorkerHttp(pool) {
  if (!(this instanceof WorkerHttp)) return new WorkerHttp(pool);
  Worker.call(this, pool);
}

inherits(WorkerHttp, Worker);

WorkerHttp.prototype.fork = function () {
  cluster.setupMaster({ args: [ 'worker-http' ] });
  return cluster.fork();
};

////////////////////////////////////////////////////////////////////////////////

module.exports = function (N) {

  N.wire.on('init:server', function* cluster_init(N) {
    let fork = N.config.fork;

    if (fork === 'auto' || fork === true || _.isUndefined(fork)) {
      fork = os.cpus().length;
    } else {
      fork = +fork || 0;
    }

    if (cluster.isMaster && (fork >= 1)) {
      WorkerPool.prototype.log = N.logger;

      /*eslint-disable new-cap*/
      let pools = [
        WorkerPool(fork, WorkerQueue),
        WorkerPool(fork, WorkerHttp)
      ];

      let shutting_down = false;

      N.wire.on('shutdown', function* shutdown_workers() {
        shutting_down = true;

        yield pools.map(pool => new Promise(resolve => {
          function on_exit() {
            // check workers in every state except DEAD
            if (pool.get([ 'INIT', 'START', 'RUN', 'PEND', 'STOP' ]).length === 0) {
              pool.removeListener('exit', on_exit);
              resolve();
            }
          }

          pool.on('exit', on_exit);
          pool.terminate();
        }));
      });

      N.wire.on('reload', function reload_workers() {
        pools.forEach(pool => pool.reload());
      });

      yield pools.map(pool => new Promise((resolve, reject) => {
        let on_exit, on_running;

        on_running = function () {
          if (pool.get('RUN').length >= fork) {
            pool.removeListener('running', on_running);
            pool.removeListener('exit', on_exit);
            resolve();
          }
        };

        on_exit = function () {
          pool.removeListener('running', on_running);
          pool.removeListener('exit', on_exit);

          if (!shutting_down) {
            reject('Cannot start workers');
          } else {
            // don't throw if user pressed ctrl+c when workers start
            resolve();
          }
        };

        pool.on('running', on_running);
        pool.on('exit', on_exit);
      }));

      if (!shutting_down) {
        N.logger.info('All workers started successfully');
      }
      return;
    }

    // If we are here - need worker deals
    yield N.wire.emit('init:server.worker-http', N);
    yield N.wire.emit('init:server.worker-queue', N);
  });
};
<commit_msg>Die properly: wait child exit after sigkill<commit_after>// Start server cluster
//
// In cluster mode it forks N http workers + N queue workers.
//
// In single mode it emits `init:server.worker-http` and
// `init:server.worker-queue` events instead
//
'use strict';

const _            = require('lodash');
const cluster      = require('cluster');
const co           = require('co');
const execFile     = require('mz/child_process').execFile;
const os           = require('os');
const inherits     = require('util').inherits;
const EventEmitter = require('events').EventEmitter;
const StateMachine = require('javascript-state-machine');

////////////////////////////////////////////////////////////////////////////////

function WorkerPool(size, worker) {
  if (!(this instanceof WorkerPool)) return new WorkerPool(size, worker);

  EventEmitter.call(this);

  this.__workers = [];
  this.__wClass  = worker;
  this.__size    = 0;

  this.update = _.debounce(() => {
    process.nextTick(() => {
      this.resize();
      this.gc();
    });
  }, 100, { maxWait: 200 });

  this.resize(size);
}

WorkerPool.prototype = {
  log() {
    /*eslint-disable no-console*/
    console.log.apply(null, arguments);
  },

  resize(count) {
    // Use current value if no params
    if (!count && count !== 0) {
      count = this.__size;
    }

    let alive = this.get([ 'INIT', 'START', 'RUN' ]),
        delta = count - alive.length,
        prev_size = this.__size;

    this.__size = count;

    if (delta === 0) return;

    if (delta > 0) {
      while (delta--) {
        let w = this.__wClass(this);
        this.__workers.push(w);
        // Force immediate start for the first run
        if (prev_size === 0 && w.can('start')) w.start();
      }
      return;
    }

    alive.slice(0, -delta).forEach(w => { w.shutdown(); });
  },

  terminate() {
    // prevent workers from being respawned
    this.__size = 0;

    this.__workers.forEach(w => { w.terminate(); });
  },

  reload() {
    this.__workers.forEach(w => {
      if (w.can('freeze')) w.freeze();
    });
  },

  get(state) {
    if (!Array.isArray(state)) {
      return this.__workers.filter(w => w.current === state);
    }
    return state.reduce((a, s) => a.concat(this.get(s)), []);
  },

  gc() {
    // Shutdown pending workers when not needed
    let excess = Math.max((this.get('PEND').length + this.get('RUN').length - this.__size), 0);

    this.get('PEND').slice(0, excess).forEach(w => { w.shutdown(); });

    // Drop dead workers
    this.__workers = this.__workers.filter(w => w.current !== 'DEAD');
  }
};

Object.setPrototypeOf(WorkerPool.prototype, EventEmitter.prototype);

////////////////////////////////////////////////////////////////////////////////

function Worker(pool) {
  if (!(this instanceof Worker)) return new Worker(pool);
  this.__pool          = pool;
  this.__w             = null;
  this.__start_timeout = 0;
  this.__exit_code     = null;

  this.begin();
}

Worker.prototype = {
  create() {
    throw new Error('This method should be overriden');
  },

  terminate() {
    if (this.__w) this.__w.kill('SIGKILL');
    else this.die();
  },

  onchangestate() {
    this.__pool.update();
  },

  onINIT() {
    setTimeout(() => {
      if (this.can('start')) this.start();
    }, 2000);
  },

  onSTART() {
    let w = this.__w = this.fork();
    let rcvd_startup_message = false;

    this.__pool.log.info(`Worker ${w.process.pid} spawned`);
    this.__pool.emit('spawn', w.process.pid);

    w.on('error', err => {
      this.__pool.log.error(err);
      this.die();
    });

    w.on('exit', (code, signal) => {
      this.__exit_code = signal || code;
      this.die();
    });

    w.on('message', msg => {
      if (msg === 'worker.running') {
        rcvd_startup_message = true;
        if (this.can('run')) this.run();
      }
    });

    setTimeout(() => {
      if (!rcvd_startup_message) this.terminate();
    }, 30000);
  },

  onSTOP() {
    this.__w.kill('SIGTERM');
    setTimeout(() => this.terminate(), 15000);
  },

  onRUN() {
    let w = this.__w;
    this.__pool.log.info(`Worker ${w.process.pid} is running`);
    this.__pool.emit('running', w.process.pid);
  },

  onDEAD() {
    let w = this.__w;

    // We can come here if process did not started.
    if (w) {
      this.__pool.log.info(`Worker ${w.process.pid} exited with status ${this.__exit_code}`);
      this.__pool.emit('exit', w.process.pid);
    }

    w.removeAllListeners();
    this.__w = null;
  }
};

StateMachine.create({
  events: [
    { name: 'begin',     from: 'none',   to: 'INIT'  },
    { name: 'start',     from: 'INIT',   to: 'START' },
    { name: 'run',       from: 'START',  to: 'RUN'   },

    { name: 'freeze',    from: 'RUN',    to: 'PEND'  },
    { name: 'freeze',    from: 'START',  to: 'DEAD'  },

    { name: 'shutdown',  from: 'RUN',    to: 'STOP'  },
    { name: 'shutdown',  from: 'PEND',   to: 'STOP'  },

    { name: 'die',       from: '*',      to: 'DEAD'  }
  ]
}, Worker.prototype);

////////////////////////////////////////////////////////////////////////////////

function WorkerQueue(pool) {
  if (!(this instanceof WorkerQueue)) return new WorkerQueue(pool);
  Worker.call(this, pool);
}

inherits(WorkerQueue, Worker);

WorkerQueue.prototype.fork = function () {
  cluster.setupMaster({ args: [ 'worker-queue' ] });

  let worker = cluster.fork(),
      log = this.__pool.log;

  worker.on('online', co.wrap(function* () {
    try {
      // Set scheduling policy to SCHED_IDLE (`-i` flag);
      // `0` is only possible value for priority ('cause this policy doesn't allow to set it)
      yield execFile('chrt', [ '-i', '-p', '0', worker.process.pid ]);
    } catch (__) {
      // If `chrt` not exists, try fallback to `renice`.
      try {
        yield execFile('renice', [ '-n', '19', '-p', worker.process.pid ]);
        log.warn('Cannot set scheduling policy for queue using `chrt`, falling back to `renice`');
      } catch (___) {
        log.error('Cannot lower priority for queue ' +
          '(both `renice` and `chrt` have failed), continuing with default priority');
      }
    }
  }));

  return worker;
};

////////////////////////////////////////////////////////////////////////////////

function WorkerHttp(pool) {
  if (!(this instanceof WorkerHttp)) return new WorkerHttp(pool);
  Worker.call(this, pool);
}

inherits(WorkerHttp, Worker);

WorkerHttp.prototype.fork = function () {
  cluster.setupMaster({ args: [ 'worker-http' ] });
  return cluster.fork();
};

////////////////////////////////////////////////////////////////////////////////

module.exports = function (N) {

  N.wire.on('init:server', function* cluster_init(N) {
    let fork = N.config.fork;

    if (fork === 'auto' || fork === true || _.isUndefined(fork)) {
      fork = os.cpus().length;
    } else {
      fork = +fork || 0;
    }

    if (cluster.isMaster && (fork >= 1)) {
      WorkerPool.prototype.log = N.logger;

      /*eslint-disable new-cap*/
      let pools = [
        WorkerPool(fork, WorkerQueue),
        WorkerPool(fork, WorkerHttp)
      ];

      let shutting_down = false;

      N.wire.on('shutdown', function* shutdown_workers() {
        shutting_down = true;

        yield pools.map(pool => new Promise(resolve => {
          function on_exit() {
            // check workers in every state except DEAD
            if (pool.get([ 'INIT', 'START', 'RUN', 'PEND', 'STOP' ]).length === 0) {
              pool.removeListener('exit', on_exit);
              resolve();
            }
          }

          pool.on('exit', on_exit);
          pool.terminate();
        }));
      });

      N.wire.on('reload', function reload_workers() {
        pools.forEach(pool => pool.reload());
      });

      yield pools.map(pool => new Promise((resolve, reject) => {
        let on_exit, on_running;

        on_running = function () {
          if (pool.get('RUN').length >= fork) {
            pool.removeListener('running', on_running);
            pool.removeListener('exit', on_exit);
            resolve();
          }
        };

        on_exit = function () {
          pool.removeListener('running', on_running);
          pool.removeListener('exit', on_exit);

          if (!shutting_down) {
            reject('Cannot start workers');
          } else {
            // don't throw if user pressed ctrl+c when workers start
            resolve();
          }
        };

        pool.on('running', on_running);
        pool.on('exit', on_exit);
      }));

      if (!shutting_down) {
        N.logger.info('All workers started successfully');
      }
      return;
    }

    // If we are here - need worker deals
    yield N.wire.emit('init:server.worker-http', N);
    yield N.wire.emit('init:server.worker-queue', N);
  });
};
<|endoftext|>


<commit_before>(function ( mw, $ ) {
	"use strict";
	mw.dualScreen = mw.dualScreen || {};

	mw.dualScreen.imagePlayer = mw.KBaseComponent.extend({
		defaultConfig: {
			cuePointType: [{
				"main": mw.KCuePoints.TYPE.THUMB,
				"sub": [mw.KCuePoints.THUMB_SUB_TYPE.SLIDE]
			}],
			prefetch: {
				'durationPercentageUntilNextSequence': 60,
				'minimumSequenceDuration': 2
			}
		},
		cuePoints: [],
		syncEnabled: true,
		setup: function(){
			this.addBinding();
		},
		canRender: function () {
			var cuePoints = this.getCuePoints();
			var cuePointsExist = (cuePoints.length > 0);
			return (!this.getPlayer().useNativePlayerControls() &&
				(
					( this.getPlayer().isLive() && this.getPlayer().isDvrSupported() && mw.getConfig("EmbedPlayer.LiveCuepoints") ) ||
					( !this.getPlayer().isLive() && cuePointsExist )
				)
			);
		},
		addBinding: function(){
			var _this = this;
			this.bind( 'onplay', function () {
				_this.loadAdditionalAssets();
			} );
			//In live mode wait for first updatetime that is bigger then 0 for syncing initial slide
			if (mw.getConfig("EmbedPlayer.LiveCuepoints") && this.getPlayer().isLive()) {
				this.bind( 'timeupdate', function ( ) {
					if (_this.getPlayer().currentTime > 0) {
						_this.unbind('timeupdate');
					}
					var cuePoint = _this.getCurrentCuePoint();
					_this.sync( cuePoint );
				} );
			}

			this.bind( 'KalturaSupport_ThumbCuePointsReady', function () {
				var currentCuepoint = _this.getCurrentCuePoint() || _this.getCuePoints()[0];
				_this.sync(currentCuepoint);
			} );
			this.bind( 'KalturaSupport_CuePointReached', function ( e, cuePointObj ) {
				var cuePoint;
				$.each(_this.getConfig("cuePointType"), function(i, cuePointType){
					var main = $.isArray(cuePointType.main) ? cuePointType.main : [cuePointType.main];
					var sub = $.isArray(cuePointType.sub) ? cuePointType.sub : [cuePointType.sub];
					if ( ( $.inArray( cuePointObj.cuePoint.cuePointType, main ) > -1 ) &&
						( $.inArray( cuePointObj.cuePoint.subType, sub ) > -1 ) ) {
						cuePoint = cuePointObj.cuePoint;
						return false;
					}
				});

				if (!cuePoint){
					// TODO [es]: TBD with Oren - I removed this since reached event will also trigger for code cue points and if I understood you correctly this workaround is redundant
					// cuePoint = _this.getCurrentCuePoint();
					return;
				}
				_this.sync( cuePoint );
			} );
			this.bind("onChangeMedia", function(){
				//Clear the current slide before loading the new media
				_this.getComponent().attr("src", "");
			});
			this.bind("onChangeStream", function(){
				_this.syncEnabled = false;
			});
			this.bind("onChangeStreamDone", function(){
				_this.syncEnabled = true;
				var cuePoint = _this.getCurrentCuePoint();
				_this.sync( cuePoint );
			});
		},
		getComponent: function() {
			if (!this.$el) {
				this.$el =
					$( '<img>' )
						.attr( 'id', 'SynchImg' )
						.addClass( "imagePlayer" );
			}
			return this.$el;
		},
		applyIntrinsicAspect: function(){
			// Check if a image thumbnail is present:
			var $img = this.getComponent();
			var imgContainer = this.getComponent().parent();
			//Make sure both image player and display are initialized
			if( $img.length && imgContainer.length/*&& this.viewInitialized*/){
				var pHeight = imgContainer.height();
				// Check for intrinsic width and maintain aspect ratio
				var pWidth = parseInt( $img.naturalWidth() / $img.naturalHeight() * pHeight, 10);
				var pClass = 'fill-height';
				if( pWidth > imgContainer.width() ){
					pClass = 'fill-width';
				}
				$img.removeClass('fill-width fill-height').addClass(pClass);
			}
		},
		getCuePoints: function(){
			var cuePoints = [];
			var _this = this;
			if ( this.getPlayer().kCuePoints ) {
				$.each( _this.getConfig("cuePointType"), function ( i, cuePointType ) {
					$.each( cuePointType.sub, function ( j, cuePointSubType ) {
						var filteredCuePoints = _this.getPlayer().kCuePoints.getCuePointsByType( cuePointType.main, cuePointSubType );
						cuePoints = cuePoints.concat( filteredCuePoints );
					} );
				} );
			}

			cuePoints.sort(function (a, b) {
				return a.startTime - b.startTime;
			});
			return cuePoints;
		},
		sync: function(cuePoint, callback){
			if (this.syncEnabled) {
				this.loadAdditionalAssets();
				var _this = this;
				var callCallback = function () {
					_this.applyIntrinsicAspect();
					if ( callback && typeof(callback) === "function" ) {
						callback();
					}
				};
				if ( cuePoint ) {
					var myImg = this.getComponent();
					if ( cuePoint.thumbnailUrl ) {
						myImg.attr( 'src', cuePoint.thumbnailUrl );
						callCallback();
					} else {
						this.loadNext( cuePoint, function ( url ) {
							myImg.attr( 'src', url );
							callCallback();
						} );
					}
				}
			}
		},
		//Prefetch
		loadAdditionalAssets: function () {
			if ( this.cuePoints ) {
				this.cancelPrefetch();
				var currentTime = this.getPlayer().currentTime;
				var nextCuePoint = this.getNextCuePoint( currentTime * 1000 );
				if ( nextCuePoint ) {
					if (!nextCuePoint.loaded) {
						var nextCuePointTime = nextCuePoint.startTime / 1000;
						var prefetch = this.getConfig("prefetch");
						var delta = nextCuePointTime - currentTime;

						var _this = this;

						if ( nextCuePointTime > currentTime && prefetch.minimumSequenceDuration <= delta ) {

							var timeOutDuration = delta * (prefetch.durationPercentageUntilNextSequence / 100) * 1000;
							this.prefetchTimeoutId = setTimeout( function () {
									_this.loadNext( nextCuePoint );
									_this.prefetchTimeoutId = null;
								}, timeOutDuration
							);
						} else if ( prefetch.minimumSequenceDuration > delta ){
							this.loadNext( nextCuePoint );
						} else {
							mw.log('Dual screen::: Too late, bail out!!!');
						}
					} else {
						mw.log('Dual screen:: Asset already loaded, aborting...');
					}
				} else {
					mw.log( 'Dual screen:: No more cuepoints!' );
				}
			}
		},
		cancelPrefetch: function () {
			if ( typeof( this.prefetchTimeoutId ) === 'number' ) {
				mw.log( 'Dual screen:: Cancel pending prefetch(' + this.prefetchTimeoutId + ')' );
				window.clearTimeout( this.prefetchTimeoutId );
				this.prefetchTimeoutId = null;
			}
		},
		loadNext: function (nextCuePoint, callback) {
			if (nextCuePoint.thumbnailUrl){
				if (!nextCuePoint.loaded){
					this.loadImage(nextCuePoint.thumbnailUrl, nextCuePoint, callback);
				}
			} else if (callback || (!nextCuePoint.loading && !nextCuePoint.loaded)) {
				nextCuePoint.loading = true;
				var assetId = nextCuePoint.assetId;

				var _this = this;
				// do the api request
				this.getKalturaClient().doRequest( {
					'service': 'thumbAsset',
					'action': 'getUrl',
					'id': assetId
				}, function ( data ) {
					// Validate result
					if ( !_this.isValidResult( data ) ) {
						return;
					}
					// Preload the next image
					_this.loadImage(data, nextCuePoint, callback);
				} );
			}
		},
		loadImage: function(src, cuePoint, callback){
			var _this = this;
			var img = new Image();
			img.onload = function () {
				cuePoint.loaded = true;
				cuePoint.loading = false;
				cuePoint.thumbnailUrl = src;
				if ( callback && typeof(callback) === "function" ) {
					callback.apply( _this, [src] );
				}
			};
			img.onerror = function () {
				cuePoint.loaded = false;
				cuePoint.loading = false;
				cuePoint.thumbnailUrl = null;
			};
			img.src = src;
		},
		isValidResult: function( data ){
			// Check if we got error
			if( !data
				||
				( data.code && data.message )
			){
				//this.log('Error getting related items: ' + data.message);
				//this.getBtn().hide();
				this.error = true;
				return false;
			}
			this.error = false;
			return true;
		},
		getNextCuePoint: function ( time ) {
			var cuePoints = this.getCuePoints();

			// Start looking for the cue point via time, return first match:
			for ( var i = 0; i < cuePoints.length; i++ ) {

				if ( cuePoints[i].startTime >= time ) {
					return cuePoints[i];
				}
			}
			// No cue point found in range return false:
			return false;
		},
		getCurrentCuePoint: function ( ) {
			var currentTime = this.getPlayer().currentTime *1000;
			var cuePoints = this.getCuePoints();
			var cuePoint;
			var duration=this.getPlayer().isLive() ? 0 : this.getPlayer().getDuration() * 1000;

			//assume sortedCuePoints array
			for ( var i = 0; i < cuePoints.length; i++ ) {

				var startTime = cuePoints[i].startTime;

				if ( (startTime > currentTime) ||  //stop once we found a future slide (or out of range slide)
					(duration>0 && startTime>duration)) {
					break;
				}

				cuePoint=cuePoints[i];

			}
			return cuePoint;
		}
	} );
}

)( window.mw, window.jQuery );<commit_msg>modified image player to use new events<commit_after>(function ( mw, $ ) {
	"use strict";
	mw.dualScreen = mw.dualScreen || {};

	mw.dualScreen.imagePlayer = mw.KBaseComponent.extend({
		defaultConfig: {

			cuePointType: [{
				"main": mw.KCuePoints.TYPE.THUMB,
				"sub": [mw.KCuePoints.THUMB_SUB_TYPE.SLIDE]
			}],
			prefetch: {
				'durationPercentageUntilNextSequence': 60,
				'minimumSequenceDuration': 2
			}
		},
		cuePoints: [],
		syncEnabled: true,
		setup: function(){
			this.addBinding();
		},
		canRender: function () {
			var cuePoints = this.getCuePoints();
			var cuePointsExist = (cuePoints.length > 0);
			return (!this.getPlayer().useNativePlayerControls() &&
				(
					( this.getPlayer().isLive() && this.getPlayer().isDvrSupported() && mw.getConfig("EmbedPlayer.LiveCuepoints") ) ||
					( !this.getPlayer().isLive() && cuePointsExist )
				)
			);
		},
		addBinding: function(){
			var _this = this;
			this.bind( 'onplay', function () {
				_this.loadAdditionalAssets();
			} );
			//In live mode wait for first updatetime that is bigger then 0 for syncing initial slide
			if (mw.getConfig("EmbedPlayer.LiveCuepoints") && this.getPlayer().isLive()) {
				this.bind( 'timeupdate', function ( ) {
					if (_this.getPlayer().currentTime > 0) {
						_this.unbind('timeupdate');
					}
					var cuePoint = _this.getCurrentCuePoint();
					_this.sync( cuePoint );
				} );
			}

			this.bind( 'KalturaSupport_ThumbCuePointsReady', function () {
				var currentCuepoint = _this.getCurrentCuePoint() || _this.getCuePoints()[0];
				_this.sync(currentCuepoint);
			} );

			function filterCuePointsByType(cuePoints, sortDesc)
			{
				var result = [];

				$.each(_this.getConfig("cuePointType"), function(i, cuePointType){
					var main = $.isArray(cuePointType.main) ? cuePointType.main : [cuePointType.main];
					var sub = $.isArray(cuePointType.sub) ? cuePointType.sub : [cuePointType.sub];

					$.each(cuePoints,function(index,cuePoint)
					{
						if ( ( $.inArray( cuePoint.cuePointType, main ) > -1 ) &&
							( $.inArray( cuePoint.subType, sub ) > -1 ) ) {
							result.push(cuePoint);
						}
					});

				});

				result.sort(function (a, b) {
					return sortDesc ? (b.startTime - a.startTime) : (a.startTime - b.startTime);
				});

				return result;
			}
			this.bind( 'KalturaSupport_CuePointsPassedDueToSeekAggregated KalturaSupport_CuePointsReachedAggregated', function ( e, args ) {

				// TODO [es] -> [om] check if configuration of cuePointType value can/is modified or do we always use the same values. if it is always this values can use the improved code
				//var relevantCuePoints = args.filter({types : _this.getConfig("cuePointType"),sortDesc : true});
				var relevantCuePoints = filterCuePointsByType(args.cuePoints,true);
				var mostUpdatedCuePointToHandle = relevantCuePoints.length> 0 ? relevantCuePoints[0] : null; // since we ordered the relevant cue points descending - the first cue point is the most updated

				if (mostUpdatedCuePointToHandle)
				{
					mw.log("imagePlayer.addBinding(cuePointEvents): syncing thumb with id '" + mostUpdatedCuePointToHandle.id);
					_this.sync(mostUpdatedCuePointToHandle);
				}
			});

			this.bind("onChangeMedia", function(){
				//Clear the current slide before loading the new media
				_this.getComponent().attr("src", "");
			});
			this.bind("onChangeStream", function(){
				_this.syncEnabled = false;
			});
			this.bind("onChangeStreamDone", function(){
				_this.syncEnabled = true;
				var cuePoint = _this.getCurrentCuePoint();
				_this.sync( cuePoint );
			});
		},
		getComponent: function() {
			if (!this.$el) {
				this.$el =
					$( '<img>' )
						.attr( 'id', 'SynchImg' )
						.addClass( "imagePlayer" );
			}
			return this.$el;
		},
		applyIntrinsicAspect: function(){
			// Check if a image thumbnail is present:
			var $img = this.getComponent();
			var imgContainer = this.getComponent().parent();
			//Make sure both image player and display are initialized
			if( $img.length && imgContainer.length/*&& this.viewInitialized*/){
				var pHeight = imgContainer.height();
				// Check for intrinsic width and maintain aspect ratio
				var pWidth = parseInt( $img.naturalWidth() / $img.naturalHeight() * pHeight, 10);
				var pClass = 'fill-height';
				if( pWidth > imgContainer.width() ){
					pClass = 'fill-width';
				}
				$img.removeClass('fill-width fill-height').addClass(pClass);
			}
		},
		getCuePoints: function(){
			var cuePoints = [];
			var _this = this;
			if ( this.getPlayer().kCuePoints ) {
				$.each( _this.getConfig("cuePointType"), function ( i, cuePointType ) {
					$.each( cuePointType.sub, function ( j, cuePointSubType ) {
						var filteredCuePoints = _this.getPlayer().kCuePoints.getCuePointsByType( cuePointType.main, cuePointSubType );
						cuePoints = cuePoints.concat( filteredCuePoints );
					} );
				} );
			}

			cuePoints.sort(function (a, b) {
				return a.startTime - b.startTime;
			});
			return cuePoints;
		},
		sync: function(cuePoint, callback){
			if (this.syncEnabled) {
				this.loadAdditionalAssets();
				var _this = this;
				var callCallback = function () {
					_this.applyIntrinsicAspect();
					if ( callback && typeof(callback) === "function" ) {
						callback();
					}
				};
				if ( cuePoint ) {
					var myImg = this.getComponent();
					if ( cuePoint.thumbnailUrl ) {
						myImg.attr( 'src', cuePoint.thumbnailUrl );
						callCallback();
					} else {
						this.loadNext( cuePoint, function ( url ) {
							myImg.attr( 'src', url );
							callCallback();
						} );
					}
				}
			}
		},
		//Prefetch
		loadAdditionalAssets: function () {
			if ( this.cuePoints ) {
				this.cancelPrefetch();
				var currentTime = this.getPlayer().currentTime;
				var nextCuePoint = this.getNextCuePoint( currentTime * 1000 );
				if ( nextCuePoint ) {
					if (!nextCuePoint.loaded) {
						var nextCuePointTime = nextCuePoint.startTime / 1000;
						var prefetch = this.getConfig("prefetch");
						var delta = nextCuePointTime - currentTime;

						var _this = this;

						if ( nextCuePointTime > currentTime && prefetch.minimumSequenceDuration <= delta ) {

							var timeOutDuration = delta * (prefetch.durationPercentageUntilNextSequence / 100) * 1000;
							this.prefetchTimeoutId = setTimeout( function () {
									_this.loadNext( nextCuePoint );
									_this.prefetchTimeoutId = null;
								}, timeOutDuration
							);
						} else if ( prefetch.minimumSequenceDuration > delta ){
							this.loadNext( nextCuePoint );
						} else {
							mw.log('Dual screen::: Too late, bail out!!!');
						}
					} else {
						mw.log('Dual screen:: Asset already loaded, aborting...');
					}
				} else {
					mw.log( 'Dual screen:: No more cuepoints!' );
				}
			}
		},
		cancelPrefetch: function () {
			if ( typeof( this.prefetchTimeoutId ) === 'number' ) {
				mw.log( 'Dual screen:: Cancel pending prefetch(' + this.prefetchTimeoutId + ')' );
				window.clearTimeout( this.prefetchTimeoutId );
				this.prefetchTimeoutId = null;
			}
		},
		loadNext: function (nextCuePoint, callback) {
			if (nextCuePoint.thumbnailUrl){
				if (!nextCuePoint.loaded){
					this.loadImage(nextCuePoint.thumbnailUrl, nextCuePoint, callback);
				}
			} else if (callback || (!nextCuePoint.loading && !nextCuePoint.loaded)) {
				nextCuePoint.loading = true;
				var assetId = nextCuePoint.assetId;

				var _this = this;
				// do the api request
				this.getKalturaClient().doRequest( {
					'service': 'thumbAsset',
					'action': 'getUrl',
					'id': assetId
				}, function ( data ) {
					// Validate result
					if ( !_this.isValidResult( data ) ) {
						return;
					}
					// Preload the next image
					_this.loadImage(data, nextCuePoint, callback);
				} );
			}
		},
		loadImage: function(src, cuePoint, callback){
			var _this = this;
			var img = new Image();
			img.onload = function () {
				cuePoint.loaded = true;
				cuePoint.loading = false;
				cuePoint.thumbnailUrl = src;
				if ( callback && typeof(callback) === "function" ) {
					callback.apply( _this, [src] );
				}
			};
			img.onerror = function () {
				cuePoint.loaded = false;
				cuePoint.loading = false;
				cuePoint.thumbnailUrl = null;
			};
			img.src = src;
		},
		isValidResult: function( data ){
			// Check if we got error
			if( !data
				||
				( data.code && data.message )
			){
				//this.log('Error getting related items: ' + data.message);
				//this.getBtn().hide();
				this.error = true;
				return false;
			}
			this.error = false;
			return true;
		},
		getNextCuePoint: function ( time ) {
			var cuePoints = this.getCuePoints();

			// Start looking for the cue point via time, return first match:
			for ( var i = 0; i < cuePoints.length; i++ ) {

				if ( cuePoints[i].startTime >= time ) {
					return cuePoints[i];
				}
			}
			// No cue point found in range return false:
			return false;
		},
		getCurrentCuePoint: function ( ) {
			var currentTime = this.getPlayer().currentTime *1000;
			var cuePoints = this.getCuePoints();
			var cuePoint;
			var duration=this.getPlayer().isLive() ? 0 : this.getPlayer().getDuration() * 1000;

			//assume sortedCuePoints array
			for ( var i = 0; i < cuePoints.length; i++ ) {

				var startTime = cuePoints[i].startTime;

				if ( (startTime > currentTime) ||  //stop once we found a future slide (or out of range slide)
					(duration>0 && startTime>duration)) {
					break;
				}

				cuePoint=cuePoints[i];

			}
			return cuePoint;
		}
	} );
}

)( window.mw, window.jQuery );<|endoftext|>


<commit_before>'use strict';

angular.module('core').controller('HeaderController', [
  '$rootScope',
  '$scope',
  '$state',
  'Authentication',
  'Menus',
  'Socket',
  'NotifsService',
  'NotifsApi',
  'Storages',
  'Constants',
  function ($rootScope, $scope, $state, Authentication, Menus, Socket, Notifs, NotifsApi, Storages, Constants) {
    // Expose view variables
    $scope.$state = $state;
    $scope.authentication = Authentication;

    // Get the topbar menu
    $scope.menu = Menus.getMenu('topbar');

    // Toggle the menu items
    $scope.isCollapsed = false;
    $scope.toggleCollapsibleMenu = function () {
      $scope.isCollapsed = !$scope.isCollapsed;
    };
    // Nghe sự kiện login thành công để load menu
    $rootScope.$on('loginSuccess', () => {
      init();
    });
    // Nghe sự kiện update Notif để load notifs
    $rootScope.$on('changeNotif', () => {
      if ($scope.authentication.user) {
        loadNotifs(10);
        loadUncheckNotifs();
      }
    });
    // Nghe sự kiện chuyển state để đóng menu collapse
    $scope.$on('$stateChangeSuccess', function () {
      $scope.isCollapsed = false;
    });

    init();

    function init() {
      if ($scope.authentication.user) {
        loadNotifs(10);
        loadUncheckNotifs();
        initSocket();
      }
    }

    function initSocket() {
      if (!Socket.socket) {
        Socket.connect();
      }
      Socket.on('notifs', res => {
        loadNotifs(10);
        loadUncheckNotifs();
      });
      Socket.on('notifs', res => {
        loadNotifs(10);
        loadUncheckNotifs();
      });
      Socket.on('activity', res => {
        res.time = moment().format();
        let activitys = JSON.parse(Storages.get_session(Constants.storages.activitys, JSON.stringify([])));
        activitys.push(res);
        Storages.set_session(Constants.storages.activitys, JSON.stringify(activitys));
        $rootScope.$emit('activity');
      });
    }

    function loadNotifs(limit) {
      return new Promise((resolve, reject) => {
        NotifsApi.findNotifs(limit)
          .then(res => {
            $scope.notifs = res.data || [];
            return resolve(res.data);
          })
          .catch(err => {
            alert(err + '');
            return reject(err);
          });
      });
    }

    function loadUncheckNotifs() {
      return new Promise((resolve, reject) => {
        NotifsApi.countUnchecks()
          .then(res => {
            $scope.uncheckNotifs = res.data || 0;
            $rootScope.$emit('updateNotif', $scope.uncheckNotifs);
            return resolve(res.data);
          })
          .catch(err => {
            console.log(err + '');
            return reject();
          });
      });
    }
    $scope.mark_all_read = () => {
      NotifsApi.markAllRead()
        .then(res => {
          loadNotifs(10);
          loadUncheckNotifs();
        });
    };
    $scope.search_key = '';
    $scope.search = () => {
      if ($scope.search_key !== '') {
        $state.go('search', { key: $scope.search_key, in: 'title' });
      }
    };

  }
]);
<commit_msg>Commited from lenh at Tue, Aug 15, 2017  1:44:01 PM<commit_after>'use strict';

angular.module('core').controller('HeaderController', [
  '$rootScope',
  '$scope',
  '$state',
  'Authentication',
  'Menus',
  'Socket',
  'NotifsService',
  'NotifsApi',
  'Storages',
  'Constants',
  function ($rootScope, $scope, $state, Authentication, Menus, Socket, Notifs, NotifsApi, Storages, Constants) {
    // Expose view variables
    $scope.$state = $state;
    $scope.authentication = Authentication;

    // Get the topbar menu
    $scope.menu = Menus.getMenu('topbar');

    // Toggle the menu items
    $scope.isCollapsed = false;
    $scope.toggleCollapsibleMenu = function () {
      $scope.isCollapsed = !$scope.isCollapsed;
    };
    // Nghe sự kiện login thành công để load menu
    $rootScope.$on('loginSuccess', () => {
      init();
    });
    // Nghe sự kiện update Notif để load notifs
    $rootScope.$on('changeNotif', () => {
      if ($scope.authentication.user) {
        loadNotifs(10);
        loadUncheckNotifs();
      }
    });
    // Nghe sự kiện chuyển state để đóng menu collapse
    $scope.$on('$stateChangeSuccess', function () {
      $scope.isCollapsed = false;
    });

    init();

    function init() {
      if ($scope.authentication.user) {
        loadNotifs(10);
        loadUncheckNotifs();
        initSocket();
      }
    }

    function initSocket() {
      if (!Socket.socket) {
        Socket.connect();
      }
      Socket.on('notifs', res => {
        loadNotifs(10);
        loadUncheckNotifs();
      });
      Socket.on('notifs', res => {
        loadNotifs(10);
        loadUncheckNotifs();
      });
      Socket.on('activity', res => {
        console.log('Has new activity from server');
        res.time = moment().format();
        let activitys = JSON.parse(Storages.get_session(Constants.storages.activitys, JSON.stringify([])));
        activitys.push(res);
        Storages.set_session(Constants.storages.activitys, JSON.stringify(activitys));
        $rootScope.$emit('activity');
      });
    }

    function loadNotifs(limit) {
      return new Promise((resolve, reject) => {
        NotifsApi.findNotifs(limit)
          .then(res => {
            $scope.notifs = res.data || [];
            return resolve(res.data);
          })
          .catch(err => {
            alert(err + '');
            return reject(err);
          });
      });
    }

    function loadUncheckNotifs() {
      return new Promise((resolve, reject) => {
        NotifsApi.countUnchecks()
          .then(res => {
            $scope.uncheckNotifs = res.data || 0;
            $rootScope.$emit('updateNotif', $scope.uncheckNotifs);
            return resolve(res.data);
          })
          .catch(err => {
            console.log(err + '');
            return reject();
          });
      });
    }
    $scope.mark_all_read = () => {
      NotifsApi.markAllRead()
        .then(res => {
          loadNotifs(10);
          loadUncheckNotifs();
        });
    };
    $scope.search_key = '';
    $scope.search = () => {
      if ($scope.search_key !== '') {
        $state.go('search', { key: $scope.search_key, in: 'title' });
      }
    };

  }
]);
<|endoftext|>


<commit_before>'use strict';

// files controller
angular.module('files').controller('FilesController', ['$scope', '$stateParams', '$location', 'Authentication', 'Files', 'FileUploader', '$timeout', '$window', '$http',
  function($scope, $stateParams, $location, Authentication, Files, FileUploader, $timeout, $window, $http) {
    $scope.authentication = Authentication;

    // Create new File
    $scope.create = function(isValid) {
      //$scope.uploadFile();
      $scope.error = null;

      if (!isValid) {
        $scope.$broadcast('show-errors-check-validity', 'fileForm');

        return false;
      }
      var fd = new FormData();
      fd.append('uploadFile', $scope.newFile);
      fd.append('title', $scope.title);
      fd.append('content', $scope.content);

      $http.post('api/files', fd, {
        transformRequest: angular.identity,
        headers: {
          'Content-Type': undefined
        }
      })

        .success(function(response) {
          $scope.success = true;

          $location.path('files/' + response._id);
        })

        .error(function() {});
    };

    // Remove existing file
    $scope.remove = function(file) {
      if (file) {
        file.$remove();

        for (var i in $scope.files) {
          if ($scope.files[i] === file) {
            $scope.files.splice(i, 1);
          }
        }
      } else {
        $scope.file.$remove(function() {
          $location.path('files');
        });
      }
    };

    // Update existing file
    $scope.update = function(isValid) {
      $scope.error = null;

      if (!isValid) {
        $scope.$broadcast('show-errors-check-validity', 'fileForm');

        return false;
      }

      var file = $scope.file;

      file.$update(function() {
        $location.path('files/' + file._id);
      }, function(errorResponse) {
        $scope.error = errorResponse.data.message;
      });
    };

    // Find a list of files
    $scope.find = function() {
      $scope.files = Files.query();
    };

    $scope.fileFilter = function(file,type){
      if(!type)return true;
      if(type.slice(-1) == '*')
        return file.filetype.substr(0, file.filetype.indexOf('/')) === type.substr(0, type.indexOf('/'));
      else
        return file.filetype == type
    };

    // Find existing file
    $scope.findOne = function() {
      $scope.file = Files.get({
        fileId: $stateParams.fileId
      });
    };
    $scope.showImage = function(){
      console.log("Show Image");
      $scope.files = Files.query();
    };
  }
])
  .directive('fileModel', ['$parse', function($parse) {
    return {
      restrict: 'A',
      link: function(scope, element, attrs) {
        var model = $parse(attrs.fileModel);
        var modelSetter = model.assign;

        element.bind('change', function() {
          scope.$apply(function() {
            modelSetter(scope, element[0].files[0]);
          });
        });
      }
    };
  }]);<commit_msg>changes for error<commit_after>'use strict';

// files controller
angular.module('files').controller('FilesController', ['$scope', '$stateParams', '$location', 'Authentication', 'Files', 'FileUploader', '$timeout', '$window', '$http',
  function($scope, $stateParams, $location, Authentication, Files, FileUploader, $timeout, $window, $http) {
    $scope.authentication = Authentication;

    // Create new File
    $scope.create = function(isValid) {
      //$scope.uploadFile();
      $scope.error = null;

      if (!isValid) {
        $scope.$broadcast('show-errors-check-validity', 'fileForm');

        return false;
      }
      var fd = new FormData();
      fd.append('uploadFile', $scope.newFile);
      fd.append('title', $scope.title);
      fd.append('content', $scope.content);

      $http.post('api/files', fd, {
        transformRequest: angular.identity,
        headers: {
          'Content-Type': undefined
        }
      })

        .success(function(response) {
          $scope.success = true;

          $location.path('files/' + response._id);
        })

        .error(function() {});
    };

    // Remove existing file
    $scope.remove = function(file) {
      if (file) {
        file.$remove();

        for (var i in $scope.files) {
          if ($scope.files[i] === file) {
            $scope.files.splice(i, 1);
          }
        }
      } else {
        $scope.file.$remove(function() {
          $location.path('files');
        });
      }
    };

    // Update existing file
    $scope.update = function(isValid) {
      $scope.error = null;

      if (!isValid) {
        $scope.$broadcast('show-errors-check-validity', 'fileForm');

        return false;
      }

      var file = $scope.file;

      file.$update(function() {
        $location.path('files/' + file._id);
      }, function(errorResponse) {
        $scope.error = errorResponse.data.message;
      });
    };

    // Find a list of files
    $scope.find = function() {
      $scope.files = Files.query();
    };

    $scope.fileFilter = function(file,type){
      if(!type)return true;
      if(type.slice(-1) === '*')
        return file.filetype.substr(0, file.filetype.indexOf('/')) === type.substr(0, type.indexOf('/'));
      else
        return file.filetype === type;
    };

    // Find existing file
    $scope.findOne = function() {
      $scope.file = Files.get({
        fileId: $stateParams.fileId
      });
    };
    $scope.showImage = function(){
      console.log("Show Image");
      $scope.files = Files.query();
    };
  }
])
  .directive('fileModel', ['$parse', function($parse) {
    return {
      restrict: 'A',
      link: function(scope, element, attrs) {
        var model = $parse(attrs.fileModel);
        var modelSetter = model.assign;

        element.bind('change', function() {
          scope.$apply(function() {
            modelSetter(scope, element[0].files[0]);
          });
        });
      }
    };
  }]);<|endoftext|>