<commit_before>const _ = require('lodash');
const url = require('url');
const crypto = require('crypto');
const moment = require('moment');
const Promise = require('bluebird');
const exec = require('child_process').exec;
const errors = require('@tryghost/errors');
const debug = require('ghost-ignition').debug('update-check');

const internal = {context: {internal: true}};

/**
 * Update Checker Class
 *
 * Makes a request to Ghost.org to request release & custom notifications.
 * The service is provided in return for users opting in to anonymous usage data collection.
 *
 * Blog owners can opt-out of update checks by setting `privacy: { useUpdateCheck: false }` in their config file.
 */
class UpdateCheckService {
    /**
     *
     * @param {Object} options
     * @param {Object} options.api - set of Ghost's API methods needed for update check to function
     * @param {Object} options.api.settings - Settings API methods
     * @param {Function} options.api.settings.read - method allowing to read Ghost's settings
     * @param {Function} options.api.settings.edit - method allowing to edit Ghost's settings
     * @param {Object} options.api.posts - Posts API methods
     * @param {Function} options.api.posts.browse - method allowing to read Ghost's posts
     * @param {Object} options.api.users - Users API methods
     * @param {Function} options.api.users.browse - method allowing to read Ghost's users
     * @param {Object} options.api.notifications - Notification API methods
     * @param {Function} options.api.notifications.add - method allowing to add Ghost notifications
     * @param {Object} options.config
     * @param {Object} options.config.mail
     * @param {string} options.config.env
     * @param {string} options.config.databaseType
     * @param {string} options.config.checkEndpoint - update check service URL
     * @param {boolean} [options.config.isPrivacyDisabled]
     * @param {string[]} [options.config.notificationGroups] - example values ["migration", "something"]
     * @param {string} options.config.siteUrl - Ghost instance URL
     * @param {boolean} [options.config.forceUpdate]
     * @param {string} options.config.ghostVersion - Ghost instance version
     * @param {Function} options.request - a HTTP request proxy function
     * @param {Function} options.sendEmail - function handling sending an email
    */
    constructor({api, config, i18n, logging, request, sendEmail}) {
        this.api = api;
        this.config = config;
        this.i18n = i18n;
        this.logging = logging;
        this.request = request;
        this.sendEmail = sendEmail;
    }

    nextCheckTimestamp() {
        const now = Math.round(new Date().getTime() / 1000);
        return now + (24 * 3600);
    }

    /**
     * @description Centralised error handler for the update check unit.
     *
     * CASES:
     *   - the update check service returns an error
     *   - error during collecting blog stats
     *
     * We still need to ensure that we set the "next_update_check" to a new value, otherwise no more
     * update checks will happen.
     *
     * @param err
     */
    updateCheckError(err) {
        this.api.settings.edit({
            settings: [{
                key: 'next_update_check',
                value: this.nextCheckTimestamp()
            }]
        }, internal);

        err.context = this.i18n.t('errors.updateCheck.checkingForUpdatesFailed.error');
        err.help = this.i18n.t('errors.updateCheck.checkingForUpdatesFailed.help', {url: 'https://ghost.org/docs/'});

        this.logging.error(err);
    }

    /**
     * @description Collect stats from your blog.
     * @returns {Promise}
     */
    async updateCheckData() {
        let data = {};
        let mailConfig = this.config.mail;

        data.ghost_version = this.config.ghostVersion;
        data.node_version = process.versions.node;
        data.env = this.config.env;
        data.database_type = this.config.databaseType;
        data.email_transport = mailConfig &&
            (mailConfig.options && mailConfig.options.service ?
                mailConfig.options.service :
                mailConfig.transport);

        try {
            const hash = (await this.api.settings.read(_.extend({key: 'db_hash'}, internal))).settings[0];
            const theme = (await this.api.settings.read(_.extend({key: 'active_theme'}, internal))).settings[0];
            const posts = await this.api.posts.browse();
            const users = await this.api.users.browse(internal);
            const npm = await Promise.promisify(exec)('npm -v');

            const blogUrl = this.config.siteUrl;
            const parsedBlogUrl = url.parse(blogUrl);
            const blogId = parsedBlogUrl.hostname + parsedBlogUrl.pathname.replace(/\//, '') + hash.value;

            data.url = blogUrl;
            data.blog_id = crypto.createHash('md5').update(blogId).digest('hex');
            data.theme = theme ? theme.value : '';
            data.post_count = posts && posts.meta && posts.meta.pagination ? posts.meta.pagination.total : 0;
            data.user_count = users && users.users && users.users.length ? users.users.length : 0;
            data.blog_created_at = users && users.users && users.users[0] && users.users[0].created_at ? moment(users.users[0].created_at).unix() : '';
            data.npm_version = npm.trim();

            return data;
        } catch (err) {
            this.updateCheckError(err);
        }
    }

    /**
     * @description Perform request to update check service.
     *
     * With the privacy setting `useUpdateCheck` you can control if you want to expose data/stats from your blog to the
     * service. Enabled or disabled, you will receive the latest notification available from the service.
     *
     * @see https://ghost.org/docs/concepts/config/#privacy
     * @returns {Promise}
     */
    async updateCheckRequest() {
        const reqData = await this.updateCheckData();

        let reqObj = {
            timeout: 1000,
            headers: {}
        };

        let checkEndpoint = this.config.checkEndpoint;
        let checkMethod = this.config.isPrivacyDisabled ? 'GET' : 'POST';

        // CASE: Expose stats and do a check-in
        if (checkMethod === 'POST') {
            reqObj.json = true;
            reqObj.body = reqData;
            reqObj.headers['Content-Length'] = Buffer.byteLength(JSON.stringify(reqData));
            reqObj.headers['Content-Type'] = 'application/json';
        } else {
            reqObj.json = true;
            reqObj.query = {
                ghost_version: reqData.ghost_version
            };
        }

        debug('Request Update Check Service', checkEndpoint);

        try {
            const response = await this.request(checkEndpoint, reqObj);
            return response.body;
        } catch (err) {
            // CASE: no notifications available, ignore
            if (err.statusCode === 404) {
                return {
                    next_check: this.nextCheckTimestamp(),
                    notifications: []
                };
            }

            // CASE: service returns JSON error, deserialize into JS error
            if (err.response && err.response.body && typeof err.response.body === 'object') {
                throw errors.utils.deserialize(err.response.body);
            } else {
                throw err;
            }
        }
    }

    /**
     * @description This function handles the response from the update check service.
     *
     * The helper does three things:
     *
     * 1. Updates the time in the settings table to know when we can execute the next update check request.
     * 2. Iterates over the received notifications and filters them out based on your notification groups.
     * 3. Calls a custom helper to generate a Ghost notification for the database.
     *
     * The structure of the response is:
     *
     * {
     *  id: 20,
     *  version: 'all4',
     *  messages:
     *     [{
     *          id: 'f8ff6c80-aa61-11e7-a126-6119te37e2b8',
     *          version: '^2',
     *          content: 'Hallouuuu custom',
     *          top: true,
     *          dismissible: true,
     *          type: 'info'
     *      }],
     *  created_at: '2021-10-06T07:00:00.000Z',
     *  custom: 1,
     *  next_check: 1555608722
     * }
     *
     *
     * Example for grouped custom notifications in config:
     *
     *  "notificationGroups": ["migration", "something"]
     *
     * The group 'all' is a reserved name for general custom notifications, which every self hosted blog can receive.
     *
     * @param {Object} response
     * @return {Promise}
     */
    async updateCheckResponse(response) {
        let notifications = [];
        let notificationGroups = (this.config.notificationGroups || []).concat(['all']);

        debug('Notification Groups', notificationGroups);
        debug('Response Update Check Service', response);

        await this.api.settings.edit({
            settings: [{
                key: 'next_update_check',
                value: response.next_check
            }]
        }, internal);

        /**
         * @NOTE:
         *
         * When we refactored notifications in Ghost 1.20, the service did not support returning multiple messages.
         * But we wanted to already add the support for future functionality.
         * That's why this helper supports two ways: returning an array of messages or returning an object with
         * a "notifications" key. The second one is probably the best, because we need to support "next_check"
         * on the root level of the response.
         */
        if (_.isArray(response)) {
            notifications = response;
        } else if ((Object.prototype.hasOwnProperty.call(response, 'notifications') && _.isArray(response.notifications))) {
            notifications = response.notifications;
        } else {
            // CASE: default right now
            notifications = [response];
        }

        // CASE: Hook into received notifications and decide whether you are allowed to receive custom group messages.
        if (notificationGroups.length) {
            notifications = notifications.filter(function (notification) {
                // CASE: release notification, keep
                if (!notification.custom) {
                    return true;
                }

                // CASE: filter out messages based on your groups
                return _.includes(notificationGroups.map(function (groupIdentifier) {
                    if (notification && notification.version && notification.version.match(new RegExp(groupIdentifier))) {
                        return true;
                    }

                    return false;
                }), true) === true;
            });
        }

        for (const notification of notifications) {
            await this.createCustomNotification(notification);
        }
    }

    /**
     * @description Create a Ghost notification and call the API controller.
     *
     * @param {Object} notification
     * @return {Promise}
     */
    async createCustomNotification(notification) {
        if (!notification) {
            return;
        }

        const {users} = await this.api.users.browse(Object.assign({
            limit: 'all',
            include: ['roles']
        }, internal));

        const adminEmails = users
            .filter(user => ['Owner', 'Administrator'].includes(user.roles[0].name))
            .map(user => user.email);

        const siteUrl = this.config.siteUrl;

        for (const message of notification.messages) {
            const toAdd = {
                // @NOTE: the update check service returns "0" or "1" (https://github.com/TryGhost/UpdateCheck/issues/43)
                custom: !!notification.custom,
                createdAt: moment(notification.created_at).toDate(),
                status: message.status || 'alert',
                type: message.type || 'info',
                id: message.id,
                dismissible: Object.prototype.hasOwnProperty.call(message, 'dismissible') ? message.dismissible : true,
                top: !!message.top,
                message: message.content
            };

            if (toAdd.type === 'alert') {
                for (const email of adminEmails) {
                    try {
                        this.sendEmail({
                            to: email,
                            subject: `Action required: Critical alert from Ghost instance ${siteUrl}`,
                            html: toAdd.message,
                            forceTextContent: true
                        });
                    } catch (err) {
                        this.logging.err(err);
                    }
                }
            }

            debug('Add Custom Notification', toAdd);
            await this.api.notifications.add({notifications: [toAdd]}, {context: {internal: true}});
        }
    }
    /**
     * @description Entry point to trigger the update check unit.
     *
     * Based on a settings value, we check if `next_update_check` is less than now to decide whether
     * we should request the update check service (http://updates.ghost.org) or not.
     *
     * @returns {Promise}
     */
    async check() {
        try {
            const result = await this.api.settings.read(_.extend({key: 'next_update_check'}, internal));

            const nextUpdateCheck = result.settings[0];

            // CASE: Next update check should happen now?
            // @NOTE: You can skip this check by adding a config value. This is helpful for developing.
            if (!this.config.forceUpdate && nextUpdateCheck && nextUpdateCheck.value && nextUpdateCheck.value > moment().unix()) {
                return;
            }

            const response = await this.updateCheckRequest();

            return await this.updateCheckResponse(response);
        } catch (err) {
            this.updateCheckError(err);
        }
    }
}

module.exports = UpdateCheckService;
<commit_msg>Added debug calls<commit_after>const _ = require('lodash');
const url = require('url');
const crypto = require('crypto');
const moment = require('moment');
const Promise = require('bluebird');
const exec = require('child_process').exec;
const errors = require('@tryghost/errors');
const debug = require('ghost-ignition').debug('update-check');

const internal = {context: {internal: true}};

/**
 * Update Checker Class
 *
 * Makes a request to Ghost.org to request release & custom notifications.
 * The service is provided in return for users opting in to anonymous usage data collection.
 *
 * Blog owners can opt-out of update checks by setting `privacy: { useUpdateCheck: false }` in their config file.
 */
class UpdateCheckService {
    /**
     *
     * @param {Object} options
     * @param {Object} options.api - set of Ghost's API methods needed for update check to function
     * @param {Object} options.api.settings - Settings API methods
     * @param {Function} options.api.settings.read - method allowing to read Ghost's settings
     * @param {Function} options.api.settings.edit - method allowing to edit Ghost's settings
     * @param {Object} options.api.posts - Posts API methods
     * @param {Function} options.api.posts.browse - method allowing to read Ghost's posts
     * @param {Object} options.api.users - Users API methods
     * @param {Function} options.api.users.browse - method allowing to read Ghost's users
     * @param {Object} options.api.notifications - Notification API methods
     * @param {Function} options.api.notifications.add - method allowing to add Ghost notifications
     * @param {Object} options.config
     * @param {Object} options.config.mail
     * @param {string} options.config.env
     * @param {string} options.config.databaseType
     * @param {string} options.config.checkEndpoint - update check service URL
     * @param {boolean} [options.config.isPrivacyDisabled]
     * @param {string[]} [options.config.notificationGroups] - example values ["migration", "something"]
     * @param {string} options.config.siteUrl - Ghost instance URL
     * @param {boolean} [options.config.forceUpdate]
     * @param {string} options.config.ghostVersion - Ghost instance version
     * @param {Function} options.request - a HTTP request proxy function
     * @param {Function} options.sendEmail - function handling sending an email
    */
    constructor({api, config, i18n, logging, request, sendEmail}) {
        this.api = api;
        this.config = config;
        this.i18n = i18n;
        this.logging = logging;
        this.request = request;
        this.sendEmail = sendEmail;
    }

    nextCheckTimestamp() {
        const now = Math.round(new Date().getTime() / 1000);
        return now + (24 * 3600);
    }

    /**
     * @description Centralised error handler for the update check unit.
     *
     * CASES:
     *   - the update check service returns an error
     *   - error during collecting blog stats
     *
     * We still need to ensure that we set the "next_update_check" to a new value, otherwise no more
     * update checks will happen.
     *
     * @param err
     */
    updateCheckError(err) {
        this.api.settings.edit({
            settings: [{
                key: 'next_update_check',
                value: this.nextCheckTimestamp()
            }]
        }, internal);

        err.context = this.i18n.t('errors.updateCheck.checkingForUpdatesFailed.error');
        err.help = this.i18n.t('errors.updateCheck.checkingForUpdatesFailed.help', {url: 'https://ghost.org/docs/'});

        this.logging.error(err);
    }

    /**
     * @description Collect stats from your blog.
     * @returns {Promise}
     */
    async updateCheckData() {
        let data = {};
        let mailConfig = this.config.mail;

        data.ghost_version = this.config.ghostVersion;
        data.node_version = process.versions.node;
        data.env = this.config.env;
        data.database_type = this.config.databaseType;
        data.email_transport = mailConfig &&
            (mailConfig.options && mailConfig.options.service ?
                mailConfig.options.service :
                mailConfig.transport);

        try {
            const hash = (await this.api.settings.read(_.extend({key: 'db_hash'}, internal))).settings[0];
            const theme = (await this.api.settings.read(_.extend({key: 'active_theme'}, internal))).settings[0];
            const posts = await this.api.posts.browse();
            const users = await this.api.users.browse(internal);
            const npm = await Promise.promisify(exec)('npm -v');

            const blogUrl = this.config.siteUrl;
            const parsedBlogUrl = url.parse(blogUrl);
            const blogId = parsedBlogUrl.hostname + parsedBlogUrl.pathname.replace(/\//, '') + hash.value;

            data.url = blogUrl;
            data.blog_id = crypto.createHash('md5').update(blogId).digest('hex');
            data.theme = theme ? theme.value : '';
            data.post_count = posts && posts.meta && posts.meta.pagination ? posts.meta.pagination.total : 0;
            data.user_count = users && users.users && users.users.length ? users.users.length : 0;
            data.blog_created_at = users && users.users && users.users[0] && users.users[0].created_at ? moment(users.users[0].created_at).unix() : '';
            data.npm_version = npm.trim();

            return data;
        } catch (err) {
            this.updateCheckError(err);
        }
    }

    /**
     * @description Perform request to update check service.
     *
     * With the privacy setting `useUpdateCheck` you can control if you want to expose data/stats from your blog to the
     * service. Enabled or disabled, you will receive the latest notification available from the service.
     *
     * @see https://ghost.org/docs/concepts/config/#privacy
     * @returns {Promise}
     */
    async updateCheckRequest() {
        const reqData = await this.updateCheckData();

        let reqObj = {
            timeout: 1000,
            headers: {}
        };

        let checkEndpoint = this.config.checkEndpoint;
        let checkMethod = this.config.isPrivacyDisabled ? 'GET' : 'POST';

        // CASE: Expose stats and do a check-in
        if (checkMethod === 'POST') {
            reqObj.json = true;
            reqObj.body = reqData;
            reqObj.headers['Content-Length'] = Buffer.byteLength(JSON.stringify(reqData));
            reqObj.headers['Content-Type'] = 'application/json';
        } else {
            reqObj.json = true;
            reqObj.query = {
                ghost_version: reqData.ghost_version
            };
        }

        debug('Request Update Check Service', checkEndpoint);

        try {
            const response = await this.request(checkEndpoint, reqObj);
            return response.body;
        } catch (err) {
            // CASE: no notifications available, ignore
            if (err.statusCode === 404) {
                return {
                    next_check: this.nextCheckTimestamp(),
                    notifications: []
                };
            }

            // CASE: service returns JSON error, deserialize into JS error
            if (err.response && err.response.body && typeof err.response.body === 'object') {
                throw errors.utils.deserialize(err.response.body);
            } else {
                throw err;
            }
        }
    }

    /**
     * @description This function handles the response from the update check service.
     *
     * The helper does three things:
     *
     * 1. Updates the time in the settings table to know when we can execute the next update check request.
     * 2. Iterates over the received notifications and filters them out based on your notification groups.
     * 3. Calls a custom helper to generate a Ghost notification for the database.
     *
     * The structure of the response is:
     *
     * {
     *  id: 20,
     *  version: 'all4',
     *  messages:
     *     [{
     *          id: 'f8ff6c80-aa61-11e7-a126-6119te37e2b8',
     *          version: '^2',
     *          content: 'Hallouuuu custom',
     *          top: true,
     *          dismissible: true,
     *          type: 'info'
     *      }],
     *  created_at: '2021-10-06T07:00:00.000Z',
     *  custom: 1,
     *  next_check: 1555608722
     * }
     *
     *
     * Example for grouped custom notifications in config:
     *
     *  "notificationGroups": ["migration", "something"]
     *
     * The group 'all' is a reserved name for general custom notifications, which every self hosted blog can receive.
     *
     * @param {Object} response
     * @return {Promise}
     */
    async updateCheckResponse(response) {
        let notifications = [];
        let notificationGroups = (this.config.notificationGroups || []).concat(['all']);

        debug('Notification Groups', notificationGroups);
        debug('Response Update Check Service', response);

        await this.api.settings.edit({
            settings: [{
                key: 'next_update_check',
                value: response.next_check
            }]
        }, internal);

        /**
         * @NOTE:
         *
         * When we refactored notifications in Ghost 1.20, the service did not support returning multiple messages.
         * But we wanted to already add the support for future functionality.
         * That's why this helper supports two ways: returning an array of messages or returning an object with
         * a "notifications" key. The second one is probably the best, because we need to support "next_check"
         * on the root level of the response.
         */
        if (_.isArray(response)) {
            notifications = response;
        } else if ((Object.prototype.hasOwnProperty.call(response, 'notifications') && _.isArray(response.notifications))) {
            notifications = response.notifications;
        } else {
            // CASE: default right now
            notifications = [response];
        }

        // CASE: Hook into received notifications and decide whether you are allowed to receive custom group messages.
        if (notificationGroups.length) {
            notifications = notifications.filter(function (notification) {
                // CASE: release notification, keep
                if (!notification.custom) {
                    return true;
                }

                // CASE: filter out messages based on your groups
                return _.includes(notificationGroups.map(function (groupIdentifier) {
                    if (notification && notification.version && notification.version.match(new RegExp(groupIdentifier))) {
                        return true;
                    }

                    return false;
                }), true) === true;
            });
        }

        for (const notification of notifications) {
            await this.createCustomNotification(notification);
        }
    }

    /**
     * @description Create a Ghost notification and call the API controller.
     *
     * @param {Object} notification
     * @return {Promise}
     */
    async createCustomNotification(notification) {
        if (!notification) {
            debug(`Skipping notification creation as there are no messages to process`);
            return;
        }

        debug(`creating custom notifications for ${notification.messages.length} notifications`);
        const {users} = await this.api.users.browse(Object.assign({
            limit: 'all',
            include: ['roles']
        }, internal));

        const adminEmails = users
            .filter(user => ['Owner', 'Administrator'].includes(user.roles[0].name))
            .map(user => user.email);

        const siteUrl = this.config.siteUrl;

        for (const message of notification.messages) {
            const toAdd = {
                // @NOTE: the update check service returns "0" or "1" (https://github.com/TryGhost/UpdateCheck/issues/43)
                custom: !!notification.custom,
                createdAt: moment(notification.created_at).toDate(),
                status: message.status || 'alert',
                type: message.type || 'info',
                id: message.id,
                dismissible: Object.prototype.hasOwnProperty.call(message, 'dismissible') ? message.dismissible : true,
                top: !!message.top,
                message: message.content
            };

            if (toAdd.type === 'alert') {
                for (const email of adminEmails) {
                    try {
                        this.sendEmail({
                            to: email,
                            subject: `Action required: Critical alert from Ghost instance ${siteUrl}`,
                            html: toAdd.message,
                            forceTextContent: true
                        });
                    } catch (err) {
                        this.logging.err(err);
                    }
                }
            }

            debug('Add Custom Notification', toAdd);
            await this.api.notifications.add({notifications: [toAdd]}, {context: {internal: true}});
        }
    }
    /**
     * @description Entry point to trigger the update check unit.
     *
     * Based on a settings value, we check if `next_update_check` is less than now to decide whether
     * we should request the update check service (http://updates.ghost.org) or not.
     *
     * @returns {Promise}
     */
    async check() {
        try {
            const result = await this.api.settings.read(_.extend({key: 'next_update_check'}, internal));

            const nextUpdateCheck = result.settings[0];

            // CASE: Next update check should happen now?
            // @NOTE: You can skip this check by adding a config value. This is helpful for developing.
            if (!this.config.forceUpdate && nextUpdateCheck && nextUpdateCheck.value && nextUpdateCheck.value > moment().unix()) {
                return;
            }

            const response = await this.updateCheckRequest();

            return await this.updateCheckResponse(response);
        } catch (err) {
            this.updateCheckError(err);
        }
    }
}

module.exports = UpdateCheckService;
<|endoftext|>


<commit_before>var _               = require('underscore');
var imagemagick     = require('imagemagick-native');
var request         = require('request');
var url             = require('url');
var webshot         = require('webshot');

function screenshotService(app){

    const webshot_options = {
        screenSize: {
            width: 1024, height: 768 // default render size
        },
        shotSize: {
            width: 'window', height: 'window' // snapshot size of whole window
        },
        streamType: 'png', // by default png
        timeout: 60000 // waiting maximum 60 seconds
    };

    // resize to 400px x 300px
    // we are showing screenshots as 200x150 images, but multiplying on 2 to support retina
    const imagepick_options = {
        width: 400,
        height: 300
    };

    app.get('/screenshot/:url/screenshot.png', function(req, res) {
        var requestedUrl = req.param('url');

        if (!requestedUrl) {
            res.status(400).end();
            return;
        }

        res.header('Content-Type', 'image/png');
        webshot(requestedUrl, webshot_options)
            .on('error', function(err) {
                res.status(400).end();
            })
            .pipe(imagemagick.streams.convert(imagepick_options))
            .on('error', function(err) {
                res.status(400).end();
            })
            .pipe(res)
            .on('error', function(err) {
                res.status(500).end();
            })
            .on('close', function() {
                res.end();
            });
    });
}

module.exports = screenshotService;
<commit_msg>Switch to jpg to save traffic<commit_after>var _               = require('underscore');
var imagemagick     = require('imagemagick-native');
var request         = require('request');
var url             = require('url');
var webshot         = require('webshot');

function screenshotService(app){
    // resize to 400px x 300px
    // we are showing screenshots as 200x150 images, but multiplying on 2 to support retina
    const imagepick_options = {
        width: 400,
        height: 300
    };

    function handleRequest(req, res, type) {
        var requestedUrl = req.param('url');

        if (!requestedUrl) {
            res.status(400).end();
            return;
        }

        res.header('Content-Type', 'image/' + type);
        webshot(requestedUrl, {
            screenSize: {
                width: 1024, height: 768 // default render size
            },
            shotSize: {
                width: 'window', height: 'window' // snapshot size of whole window
            },
            streamType: type,
            timeout: 60000 // waiting maximum 60 seconds
        })
            .on('error', function(err) {
                res.status(400).end();
            })
            .pipe(imagemagick.streams.convert(imagepick_options))
            .on('error', function(err) {
                res.status(400).end();
            })
            .pipe(res)
            .on('error', function(err) {
                res.status(500).end();
            })
            .on('close', function() {
                res.end();
            });
    }

    app.get('/screenshot/:url/screenshot.jpg', function(req, res) {
        handleRequest(req, res, 'jpg');
    });

    app.get('/screenshot/:url/screenshot.png', function(req, res) {
        handleRequest(req, res, 'png');
    });
}

module.exports = screenshotService;
<|endoftext|>


<commit_before>define(['sprd/model/ProductBase', 'js/core/List', 'js/data/AttributeTypeResolver', 'sprd/entity/DesignConfiguration', 'sprd/entity/TextConfiguration', 'sprd/entity/Price', 'js/data/TypeResolver', 'js/data/Entity', "underscore", "flow", "sprd/manager/IProductManager", "sprd/error/ProductCreationError", 'sprd/model/ProductType'],
    function (ProductBase, List, AttributeTypeResolver, DesignConfiguration, TextConfiguration, Price, TypeResolver, Entity, _, flow, IProductManager, ProductCreationError, ProductType) {

    var undefined;

    return ProductBase.inherit("sprd.model.Product", {

        schema: {
            productType: ProductType,
            configurations: [new AttributeTypeResolver({
                attribute: "type",
                mapping: {
                    "design": DesignConfiguration,
                    "text": TextConfiguration
                }
            })],
            creator: String
        },

        defaults: {
            configurations: List,
            creator: null
        },

        inject: {
            manager: IProductManager
        },

        ctor: function () {
            this.configurationsOnViewCache = {};

            this.callBase();

            var priceChangeHandler = function () {
                this.trigger("priceChanged");
            };

            var productChangeHandler = function () {
                this.trigger("productChanged");
            };

            var configurationAdd = function (e) {
                var configuration = e.$.item,
                    viewId = configuration.get("printArea.getDefaultView().id");

                if (viewId) {
                    // clear configuration cache
                    this.configurationsOnViewCache[viewId] = null;
                    this.trigger("configurationValidChanged");
                }

                this._setUpConfiguration(configuration);

                if(configuration instanceof TextConfiguration){
                    configuration._composeText();
                }
                priceChangeHandler.call(this);
                productChangeHandler.call(this);

            };

            var configurationRemove = function (e) {
                var configuration = e.$.item,
                    viewId = configuration.get("printArea.getDefaultView().id");

                if (viewId) {
                    // clear configuration cache
                    this.configurationsOnViewCache[viewId] = null;
                    this.trigger("configurationValidChanged");
                }

                this._tearDownConfiguration(configuration);

                productChangeHandler.call(this);
                priceChangeHandler.call(this);
            };

            this.bind("configurations", "add", configurationAdd, this);
            this.bind("configurations", "remove", configurationRemove, this);
            this.bind("configurations", "reset", function() {
                this.configurationsOnViewCache = {};
                this.trigger("configurationValidChanged");

                priceChangeHandler.call(this);
                productChangeHandler.call(this);
            }, this);

            this.bind("configurations", "item:priceChanged", priceChangeHandler, this);
            this.bind('configurations', 'item:configurationChanged', productChangeHandler, this);
            this.bind('change:configurations', productChangeHandler, this);
            this.bind('configurations', 'item:change:printArea', function() {
                this.configurationsOnViewCache = {};
                this.trigger("configurationValidChanged");

                productChangeHandler.call(this);
            }, this);

            this.bind('configurations', 'item:isValidChanged', function() {
                this.trigger("configurationValidChanged");
            }, this);

            this.bind('configurations', 'item:change:offset', this._onConfigurationOffsetChanged, this);

        },

        getDefaultAppearance: function () {
            var productType = this.$.productType;

            if (productType) {
                return productType.getAppearanceById(this.get('appearance.id'));
            }

            return null;
        }.onChange('appearance', 'productType'),

        _onConfigurationOffsetChanged: function(e) {

            var manager = this.$.manager,
                configuration = e.$.item;

            if (manager && manager.checkConfigurationOffset) {
                manager.checkConfigurationOffset(this, configuration)
            }
        },

        configurationsOnViewErrorKey: function(view) {

            var errorKey = null,
                configurations;

            if (!view) {
                return null;
            }

            var viewId = view.$.id;

            var productType = this.$.productType;

            configurations = this.configurationsOnViewCache[viewId];
            if (!configurations && productType && productType.containsView(view)) {
                this.configurationsOnViewCache[viewId] = configurations = this.getConfigurationsOnView(view);
            }

            if (!configurations) {
                return null;
            }

            for (var i = 0; i < configurations.length; i++) {
                var configuration = configurations[i];

                if (!configuration.isValid() && configuration.$errors) {

                    for (var key in configuration.$errors.$) {
                        if (configuration.$errors.$.hasOwnProperty(key)) {
                            errorKey = configuration.$errors.$[key];
                            if (errorKey) {
                                errorKey = key;
                                break;
                            }
                        }
                    }
                }
            }

            this.configurationsOnViewCache[viewId].errorKey = errorKey;

            return errorKey;

        }.on("configurationValidChanged"),

        _setUpConfiguration: function (configuration) {

            if (!this.$stage) {
                return;
            }

            this.$stage.$bus.setUp(configuration);
        },

        _tearDownConfiguration: function (configuration) {

            if (!this.$stage) {
                return;
            }

            this.$stage.$bus.tearDown(configuration);
        },

        _postConstruct: function () {

            var configurations = this.$.configurations,
                self = this;

            if (configurations) {
                configurations.each(function (configuration) {
                    self._setUpConfiguration(configuration);
                });
            }
        },

        _preDestroy: function () {
            var configurations = this.$.configurations,
                self = this;

            if (configurations) {
                configurations.each(function (configuration) {
                    self._tearDownConfiguration(configuration);
                });
            }
        },

        price: function () {

            // calculate price
            var price = new Price({
                vatIncluded: this.get("productType.price.vatIncluded"),
                currency: this.get('productType.price.currency')
            });
            this.$.configurations.each(function (configuration) {
                price.add(configuration.price());
            });

            return price;

        }.on("priceChanged", ["productType", 'change:price']).onChange('productType'),

        _addConfiguration: function (configuration) {
            this.trigger('beforeConfigurationAdd', configuration);
            this.$.configurations.add(configuration);
        },

        getConfigurationsOnView: function (view) {

            view = view || this.$.view;

            var productType = this.$.productType;

            if (view && productType) {

                if (productType.containsView(view)) {
                    return this.getConfigurationsOnPrintAreas(view.getPrintAreas());
                } else {
                    throw new Error("View not on product type");
                }

            }

            return [];

        },

        getConfigurationsOnPrintAreas: function (printAreas) {
            printAreas = printAreas || [];

            if (!(printAreas instanceof Array)) {
                printAreas = [printAreas];
            }

            var ret = [];

            for (var i = 0; i < this.$.configurations.$items.length; i++) {
                var configuration = this.$.configurations.$items[i];
                if (_.contains(printAreas, configuration.$.printArea)) {
                    ret.push(configuration);
                }
            }

            return ret;

        },

        compose: function () {
            var ret = this.callBase();

            ret.restrictions = {
                freeColorSelection: false,
                example: false
            };

            var viewId = this.get("view.id");

            if (viewId) {
                ret.defaultValues = {
                    defaultView: {
                        id: viewId
                    }
                };
            }

            return ret;
        },

        fetch: function (options, callback) {
            var self = this;
            this.callBase(options, function (err) {
                if (!err) {
                    self.$originalProduct = self.clone();
                }
                callback && callback(err, self);
            });
        },

        hasChanges: function () {
            return !this.equals(this.$originalProduct);
        },

        equals: function(product) {
            return (this.$.configurations.isDeepEqual(product.$.configurations) &&
                this.$.appearance.isDeepEqual(product.$.appearance) &&
                this.$.productType.isDeepEqual(product.$.productType));
        },

        save: function (options, callback) {

            if (this.$originalProduct) {
                if (this.hasChanges()) {
                    this.set('id', undefined);
                } else {
                    this.set({
                        id: this.$originalProduct.$.id,
                        href: this.$originalProduct.$.href
                    });

                    callback && callback(null, this);
                    return;
                }
            }

            var self = this;

            if (!this.$.creator) {
                if (this.$stage && this.$stage.$application.name) {
                    this.set("creator", this.$stage.$application.name);
                }
            }

            this.callBase(options, function (err) {
                if (!err) {
                    self.$originalProduct = self.clone();
                } else {
                    err = ProductCreationError.createFromResponse(err);
                }

                callback && callback(err, self);
            });
        },

        init: function (callback) {
            var self = this;

            flow()
                .seq(function (cb) {
                    if (self.isNew()) {
                        cb();
                    } else {
                        self.fetch(null, cb);
                    }
                })
                .seq(function (cb) {
                    var productType = self.$.productType;
                    productType.fetch(null, cb);
                })
                .seq(function () {
                    var productType = self.$.productType;

                    var appearance;

                    if (self.$.appearance) {
                        appearance = productType.getAppearanceById(self.$.appearance.$.id);
                    }

                    appearance = appearance || productType.getDefaultAppearance();

                    self.set({
                        appearance: appearance,
                        view: self.$.view || productType.getViewById(self.get("defaultValues.defaultView.id")) || productType.getDefaultView()
                    });
                })
                .seq(function (cb) {
                    flow()
                        .parEach(self.$.configurations.$items, function (configuration, cb) {
                            self._setUpConfiguration(configuration);
                            configuration.init(cb);
                        })
                        .exec(cb);
                })
                .exec(function (err) {

                    self.trigger("priceChanged");

                    if (err) {
                        callback && callback(err);
                    } else {
                        callback && callback(null, self);
                    }
                });
        },

        appearanceBrightness: function () {

            var color = this.get("appearance.getMainColor()");

            if (color) {
                return color.distanceTo("#000000") < color.distanceTo("#FFFFFF") ?
                    "dark" : "bright";
            }

            return "";
        }.onChange(["appearance"]),

        isReadyForCompose: function () {
            var ready = true;

            if (this.$.configurations) {
                this.$.configurations.each(function (configuration) {
                    ready = ready && configuration.isReadyForCompose();
                });
            }

            return ready;
        },

        _commitChangedAttributes: function (attributes) {
            this.callBase();
            if (attributes.hasOwnProperty("appearance") || attributes.hasOwnProperty("productType")) {
                this.trigger("productChanged");
            }
        }
    });
});
<commit_msg>added getDefaultView to Product<commit_after>define(['sprd/model/ProductBase', 'js/core/List', 'js/data/AttributeTypeResolver', 'sprd/entity/DesignConfiguration', 'sprd/entity/TextConfiguration', 'sprd/entity/Price', 'js/data/TypeResolver', 'js/data/Entity', "underscore", "flow", "sprd/manager/IProductManager", "sprd/error/ProductCreationError", 'sprd/model/ProductType'],
    function (ProductBase, List, AttributeTypeResolver, DesignConfiguration, TextConfiguration, Price, TypeResolver, Entity, _, flow, IProductManager, ProductCreationError, ProductType) {

    var undefined;

    return ProductBase.inherit("sprd.model.Product", {

        schema: {
            productType: ProductType,
            configurations: [new AttributeTypeResolver({
                attribute: "type",
                mapping: {
                    "design": DesignConfiguration,
                    "text": TextConfiguration
                }
            })],
            creator: String
        },

        defaults: {
            configurations: List,
            creator: null
        },

        inject: {
            manager: IProductManager
        },

        ctor: function () {
            this.configurationsOnViewCache = {};

            this.callBase();

            var priceChangeHandler = function () {
                this.trigger("priceChanged");
            };

            var productChangeHandler = function () {
                this.trigger("productChanged");
            };

            var configurationAdd = function (e) {
                var configuration = e.$.item,
                    viewId = configuration.get("printArea.getDefaultView().id");

                if (viewId) {
                    // clear configuration cache
                    this.configurationsOnViewCache[viewId] = null;
                    this.trigger("configurationValidChanged");
                }

                this._setUpConfiguration(configuration);

                if(configuration instanceof TextConfiguration){
                    configuration._composeText();
                }
                priceChangeHandler.call(this);
                productChangeHandler.call(this);

            };

            var configurationRemove = function (e) {
                var configuration = e.$.item,
                    viewId = configuration.get("printArea.getDefaultView().id");

                if (viewId) {
                    // clear configuration cache
                    this.configurationsOnViewCache[viewId] = null;
                    this.trigger("configurationValidChanged");
                }

                this._tearDownConfiguration(configuration);

                productChangeHandler.call(this);
                priceChangeHandler.call(this);
            };

            this.bind("configurations", "add", configurationAdd, this);
            this.bind("configurations", "remove", configurationRemove, this);
            this.bind("configurations", "reset", function() {
                this.configurationsOnViewCache = {};
                this.trigger("configurationValidChanged");

                priceChangeHandler.call(this);
                productChangeHandler.call(this);
            }, this);

            this.bind("configurations", "item:priceChanged", priceChangeHandler, this);
            this.bind('configurations', 'item:configurationChanged', productChangeHandler, this);
            this.bind('change:configurations', productChangeHandler, this);
            this.bind('configurations', 'item:change:printArea', function() {
                this.configurationsOnViewCache = {};
                this.trigger("configurationValidChanged");

                productChangeHandler.call(this);
            }, this);

            this.bind('configurations', 'item:isValidChanged', function() {
                this.trigger("configurationValidChanged");
            }, this);

            this.bind('configurations', 'item:change:offset', this._onConfigurationOffsetChanged, this);

        },

        getDefaultAppearance: function () {
            var productType = this.$.productType;

            if (productType) {
                return productType.getAppearanceById(this.get('appearance.id'));
            }

            return null;
        }.onChange('appearance', 'productType'),

        getDefaultView: function() {
            var productType = this.$.productType,
                viewId = this.get("defaultValues.defaultView.id");

            if (productType) {
                return productType.getViewById(viewId) || productType.getDefaultView();
            }

            return null;
        },

        _onConfigurationOffsetChanged: function(e) {

            var manager = this.$.manager,
                configuration = e.$.item;

            if (manager && manager.checkConfigurationOffset) {
                manager.checkConfigurationOffset(this, configuration)
            }
        },

        configurationsOnViewErrorKey: function(view) {

            var errorKey = null,
                configurations;

            if (!view) {
                return null;
            }

            var viewId = view.$.id;

            var productType = this.$.productType;

            configurations = this.configurationsOnViewCache[viewId];
            if (!configurations && productType && productType.containsView(view)) {
                this.configurationsOnViewCache[viewId] = configurations = this.getConfigurationsOnView(view);
            }

            if (!configurations) {
                return null;
            }

            for (var i = 0; i < configurations.length; i++) {
                var configuration = configurations[i];

                if (!configuration.isValid() && configuration.$errors) {

                    for (var key in configuration.$errors.$) {
                        if (configuration.$errors.$.hasOwnProperty(key)) {
                            errorKey = configuration.$errors.$[key];
                            if (errorKey) {
                                errorKey = key;
                                break;
                            }
                        }
                    }
                }
            }

            this.configurationsOnViewCache[viewId].errorKey = errorKey;

            return errorKey;

        }.on("configurationValidChanged"),

        _setUpConfiguration: function (configuration) {

            if (!this.$stage) {
                return;
            }

            this.$stage.$bus.setUp(configuration);
        },

        _tearDownConfiguration: function (configuration) {

            if (!this.$stage) {
                return;
            }

            this.$stage.$bus.tearDown(configuration);
        },

        _postConstruct: function () {

            var configurations = this.$.configurations,
                self = this;

            if (configurations) {
                configurations.each(function (configuration) {
                    self._setUpConfiguration(configuration);
                });
            }
        },

        _preDestroy: function () {
            var configurations = this.$.configurations,
                self = this;

            if (configurations) {
                configurations.each(function (configuration) {
                    self._tearDownConfiguration(configuration);
                });
            }
        },

        price: function () {

            // calculate price
            var price = new Price({
                vatIncluded: this.get("productType.price.vatIncluded"),
                currency: this.get('productType.price.currency')
            });
            this.$.configurations.each(function (configuration) {
                price.add(configuration.price());
            });

            return price;

        }.on("priceChanged", ["productType", 'change:price']).onChange('productType'),

        _addConfiguration: function (configuration) {
            this.trigger('beforeConfigurationAdd', configuration);
            this.$.configurations.add(configuration);
        },

        getConfigurationsOnView: function (view) {

            view = view || this.$.view;

            var productType = this.$.productType;

            if (view && productType) {

                if (productType.containsView(view)) {
                    return this.getConfigurationsOnPrintAreas(view.getPrintAreas());
                } else {
                    throw new Error("View not on product type");
                }

            }

            return [];

        },

        getConfigurationsOnPrintAreas: function (printAreas) {
            printAreas = printAreas || [];

            if (!(printAreas instanceof Array)) {
                printAreas = [printAreas];
            }

            var ret = [];

            for (var i = 0; i < this.$.configurations.$items.length; i++) {
                var configuration = this.$.configurations.$items[i];
                if (_.contains(printAreas, configuration.$.printArea)) {
                    ret.push(configuration);
                }
            }

            return ret;

        },

        compose: function () {
            var ret = this.callBase();

            ret.restrictions = {
                freeColorSelection: false,
                example: false
            };

            var viewId = this.get("view.id");

            if (viewId) {
                ret.defaultValues = {
                    defaultView: {
                        id: viewId
                    }
                };
            }

            return ret;
        },

        fetch: function (options, callback) {
            var self = this;
            this.callBase(options, function (err) {
                if (!err) {
                    self.$originalProduct = self.clone();
                }
                callback && callback(err, self);
            });
        },

        hasChanges: function () {
            return !this.equals(this.$originalProduct);
        },

        equals: function(product) {
            return (this.$.configurations.isDeepEqual(product.$.configurations) &&
                this.$.appearance.isDeepEqual(product.$.appearance) &&
                this.$.productType.isDeepEqual(product.$.productType));
        },

        save: function (options, callback) {

            if (this.$originalProduct) {
                if (this.hasChanges()) {
                    this.set('id', undefined);
                } else {
                    this.set({
                        id: this.$originalProduct.$.id,
                        href: this.$originalProduct.$.href
                    });

                    callback && callback(null, this);
                    return;
                }
            }

            var self = this;

            if (!this.$.creator) {
                if (this.$stage && this.$stage.$application.name) {
                    this.set("creator", this.$stage.$application.name);
                }
            }

            this.callBase(options, function (err) {
                if (!err) {
                    self.$originalProduct = self.clone();
                } else {
                    err = ProductCreationError.createFromResponse(err);
                }

                callback && callback(err, self);
            });
        },

        init: function (callback) {
            var self = this;

            flow()
                .seq(function (cb) {
                    if (self.isNew()) {
                        cb();
                    } else {
                        self.fetch(null, cb);
                    }
                })
                .seq(function (cb) {
                    var productType = self.$.productType;
                    productType.fetch(null, cb);
                })
                .seq(function () {
                    var productType = self.$.productType;

                    var appearance;

                    if (self.$.appearance) {
                        appearance = productType.getAppearanceById(self.$.appearance.$.id);
                    }

                    appearance = appearance || productType.getDefaultAppearance();

                    self.set({
                        appearance: appearance,
                        view: self.$.view || productType.getViewById(self.get("defaultValues.defaultView.id")) || productType.getDefaultView()
                    });
                })
                .seq(function (cb) {
                    flow()
                        .parEach(self.$.configurations.$items, function (configuration, cb) {
                            self._setUpConfiguration(configuration);
                            configuration.init(cb);
                        })
                        .exec(cb);
                })
                .exec(function (err) {

                    self.trigger("priceChanged");

                    if (err) {
                        callback && callback(err);
                    } else {
                        callback && callback(null, self);
                    }
                });
        },

        appearanceBrightness: function () {

            var color = this.get("appearance.getMainColor()");

            if (color) {
                return color.distanceTo("#000000") < color.distanceTo("#FFFFFF") ?
                    "dark" : "bright";
            }

            return "";
        }.onChange(["appearance"]),

        isReadyForCompose: function () {
            var ready = true;

            if (this.$.configurations) {
                this.$.configurations.each(function (configuration) {
                    ready = ready && configuration.isReadyForCompose();
                });
            }

            return ready;
        },

        _commitChangedAttributes: function (attributes) {
            this.callBase();
            if (attributes.hasOwnProperty("appearance") || attributes.hasOwnProperty("productType")) {
                this.trigger("productChanged");
            }
        }
    });
});
<|endoftext|>


<commit_before>// ==UserScript==
// @name easyreply
// @namespace http://reddit.com/u/undergroundmonorail
// @author monorail
// @version 0.1
// @description easily respond to a user's last message in se chat
// @grant none
// @copyright MIT
// @include *://chat.stackoverflow.com/*
// @include *://chat.stackexchange.com/*
// @include *://chat.meta.stackexchange.com/*
// ==/UserScript==

// workaround for Safari
// (as if anyone uses Safari)
// ((glares at @AlexA.))
function withJQuery(f) {
	var script = document.createElement('script');
	script.type = 'text/javascript';
	script.textContent = '('+ f + ')(jQuery)';
	document.head.appendChild(script);
}

withJQuery(easyreply);

function easyreply($) {
	// OPTIONS
	// change the identifiers to look for on either end of a username
	var PREFIX = '@';
	var SUFFIX = '@';

	function messageAuthor(message) {
		// a group of continuous messages by the same user is a "monologue"
		// go up to the monologue, then find the author
		// strip spaces from the username because that's how the name you type to ping someone is found
		// (e.g.) pinging 'Alex A.' is done by typing '@AlexA.')
		return message.parent().parent().children('.signature').children('.username').text().replace(/ /g, '');
	}

	function getNewText(old) {
		// runs just before sending a message
		// takes text of message about to be sent
		// returns message to send instead
		var newText = old; // by default don't change anything
		var firstWord = old.split(' ')[0];

		// if the first word denotes a reply
		if (firstWord.slice(0, PREFIX.length) == PREFIX && firstWord.slice(-SUFFIX.length) == SUFFIX) {
			// go backwards through the messages until finding one that is by the mentioned user OR there are no more messages
			var message = $('.message:last');
			while (message.length && firstWord != PREFIX + messageAuthor(message) + SUFFIX) {
				var prev = message.prev('.message'); // go up one message
				if (!prev.length) { // handle monologue barriers
					// stole this code wholesale from doorknob. i mostly understand it.
					prev = message.closest('.monologue').prevAll('.monologue').eq(0).find('.message:last');
				}
				message = prev;
			}
			if (message.length) { // if a message was found
				// reply syntax is ':[message id]'
				// messages have the attribute 'id', containing 'message-[message id]'
				// put a colon, everything after the hyphen in the id, then the original message minus the first word
				newText = ':' + message.attr('id').split('-')[1] + ' ' + old.split(' ').slice(1).join(' ');
			}
		}
		return newText;
	}

	var input = $('#input');

	input.keydown(function(e) {
		if (e.which == 13) { // when enter key is pressed
			e.preventDefault();
			input[0].value = getNewText(input[0].value); // replace text in message box
			$('sayit-button').click(); // SLAM THAT SEND BUTTON
		}
	});

	// i don't know what this means but doorknob's code has it ¯\_(ツ)_/¯
	var kdevts = $('#input').data('events').keydown;
	kdevts.unshift(kdevts.pop());
	// doorknob: "For the record, it moves the last keydown event to the front of the 'list' of events."
	// "That way your function is called before SE's default functions."
	// "Otherwise, the message would be sent *before* the replacement would be made."
	// thanks doorknob
}
<commit_msg>make case-insensitive<commit_after>// ==UserScript==
// @name easyreply
// @namespace http://reddit.com/u/undergroundmonorail
// @author monorail
// @version 0.1
// @description easily respond to a user's last message in se chat
// @grant none
// @copyright MIT
// @include *://chat.stackoverflow.com/*
// @include *://chat.stackexchange.com/*
// @include *://chat.meta.stackexchange.com/*
// ==/UserScript==

// workaround for Safari
// (as if anyone uses Safari)
// ((glares at @AlexA.))
function withJQuery(f) {
	var script = document.createElement('script');
	script.type = 'text/javascript';
	script.textContent = '('+ f + ')(jQuery)';
	document.head.appendChild(script);
}

withJQuery(easyreply);

function easyreply($) {
	// OPTIONS
	// change the identifiers to look for on either end of a username
	var PREFIX = '@';
	var SUFFIX = '@';

	function messageAuthor(message) {
		// a group of continuous messages by the same user is a "monologue"
		// go up to the monologue, then find the author
		// strip spaces from the username because that's how the name you type to ping someone is found
		// (e.g.) pinging 'Alex A.' is done by typing '@AlexA.')
		return message.parent().parent().children('.signature').children('.username').text().replace(/ /g, '');
	}

	function getNewText(old) {
		// runs just before sending a message
		// takes text of message about to be sent
		// returns message to send instead
		var newText = old; // by default don't change anything
		var firstWord = old.split(' ')[0];

		// if the first word denotes a reply
		if (firstWord.slice(0, PREFIX.length) == PREFIX && firstWord.slice(-SUFFIX.length) == SUFFIX) {
			// go backwards through the messages until finding one that is by the mentioned user OR there are no more messages
			var message = $('.message:last');
			while (message.length && firstWord.slice(1,-1).toLowerCase() != messageAuthor(message).toLowerCase() ) {
				var prev = message.prev('.message'); // go up one message
				if (!prev.length) { // handle monologue barriers
					// stole this code wholesale from doorknob. i mostly understand it.
					prev = message.closest('.monologue').prevAll('.monologue').eq(0).find('.message:last');
				}
				message = prev;
			}
			if (message.length) { // if a message was found
				// reply syntax is ':[message id]'
				// messages have the attribute 'id', containing 'message-[message id]'
				// put a colon, everything after the hyphen in the id, then the original message minus the first word
				newText = ':' + message.attr('id').split('-')[1] + ' ' + old.split(' ').slice(1).join(' ');
			}
		}
		return newText;
	}

	var input = $('#input');

	input.keydown(function(e) {
		if (e.which == 13) { // when enter key is pressed
			e.preventDefault();
			input[0].value = getNewText(input[0].value); // replace text in message box
			$('sayit-button').click(); // SLAM THAT SEND BUTTON
		}
	});

	// i don't know what this means but doorknob's code has it ¯\_(ツ)_/¯
	var kdevts = $('#input').data('events').keydown;
	kdevts.unshift(kdevts.pop());
	// doorknob: "For the record, it moves the last keydown event to the front of the 'list' of events."
	// "That way your function is called before SE's default functions."
	// "Otherwise, the message would be sent *before* the replacement would be made."
	// thanks doorknob
}
<|endoftext|>


<commit_before>describe("About Mutability", function() {

  it("should expect object properties to be public and mutable", function () {
    var aPerson = {firstname: "John", lastname: "Smith" };
    aPerson.firstname = "Alan";

    expect(aPerson.firstname).toBe("Alan");
  });

  it("should understand that constructed properties are public and mutable", function () {
    function Person(firstname, lastname)
    {
      this.firstname = firstname;
      this.lastname = lastname;
    }
    var aPerson = new Person ("John", "Smith");
    aPerson.firstname = "Alan";

    expect(aPerson.firstname).toBe("Alan");
  });

  it("should expect prototype properties to be public and mutable", function () {
    function Person(firstname, lastname)
    {
      this.firstname = firstname;
      this.lastname = lastname;
    }
    Person.prototype.getFullName = function () {
      return this.firstname + " " + this.lastname;
    };

    var aPerson = new Person ("John", "Smith");
    expect(aPerson.getFullName()).toBe("John Smith");

    aPerson.getFullName = function () {
      return this.lastname + ", " + this.firstname;
    };

    expect(aPerson.getFullName()).toBe("Smith, John");
  });

  it("should know that variables inside a constructor and constructor args are private", function () {
    function Person(firstname, lastname)
    {
      var fullName = firstname + " " + lastname;

      this.getFirstName = function () { return firstname; };
      this.getLastName = function () { return lastname; };
      this.getFullName = function () { return fullName; };
    }
    var aPerson = new Person ("John", "Smith");

    aPerson.firstname = "Penny";
    aPerson.lastname = "Andrews";
    aPerson.fullName = "Penny Andrews";

    expect(aPerson.getFirstName()).toBe(FILL_ME_IN);
    expect(aPerson.getLastName()).toBe(FILL_ME_IN);
    expect(aPerson.getFullName()).toBe(FILL_ME_IN);

    aPerson.getFullName = function () {
      return aPerson.lastname + ", " + aPerson.firstname;
    };

    expect(aPerson.getFullName()).toBe(FILL_ME_IN);
  });

});
<commit_msg>Corrected values for the finals tests<commit_after>describe("About Mutability", function() {

  it("should expect object properties to be public and mutable", function () {
    var aPerson = {firstname: "John", lastname: "Smith" };
    aPerson.firstname = "Alan";

    expect(aPerson.firstname).toBe("Alan");
  });

  it("should understand that constructed properties are public and mutable", function () {
    function Person(firstname, lastname)
    {
      this.firstname = firstname;
      this.lastname = lastname;
    }
    var aPerson = new Person ("John", "Smith");
    aPerson.firstname = "Alan";

    expect(aPerson.firstname).toBe("Alan");
  });

  it("should expect prototype properties to be public and mutable", function () {
    function Person(firstname, lastname)
    {
      this.firstname = firstname;
      this.lastname = lastname;
    }
    Person.prototype.getFullName = function () {
      return this.firstname + " " + this.lastname;
    };

    var aPerson = new Person ("John", "Smith");
    expect(aPerson.getFullName()).toBe("John Smith");

    aPerson.getFullName = function () {
      return this.lastname + ", " + this.firstname;
    };

    expect(aPerson.getFullName()).toBe("Smith, John");
  });

  it("should know that variables inside a constructor and constructor args are private", function () {
    function Person(firstname, lastname)
    {
      var fullName = firstname + " " + lastname;

      this.getFirstName = function () { return firstname; };
      this.getLastName = function () { return lastname; };
      this.getFullName = function () { return fullName; };
    }
    var aPerson = new Person ("John", "Smith");

    aPerson.firstname = "Penny";
    aPerson.lastname = "Andrews";
    aPerson.fullName = "Penny Andrews";

    expect(aPerson.getFirstName()).toBe("John");
    expect(aPerson.getLastName()).toBe("Smith");
    expect(aPerson.getFullName()).toBe("John Smith");

    aPerson.getFullName = function () {
      return aPerson.lastname + ", " + aPerson.firstname;
    };

    expect(aPerson.getFullName()).toBe("Andrews, Penny");
  });

});
<|endoftext|>


<commit_before>
stupid = { inc : 0 };

function foo( a ){
    stupid.inc++;
    return a;
}

assert( 0 == stupid.inc );
foo.cache( 100000 )
assert( 1 == stupid.inc , stupid.inc );
foo.cache( 100000 )
assert( 1 == stupid.inc );

foo.cache( 100000 , { name : "asd" } );
assert( 2 == stupid.inc );
foo.cache( 100000 , { name : "asd" } );
assert( 2 == stupid.inc );

<commit_msg>more tests that work<commit_after>
stupid = { inc : 0 };

function foo( a ){
    stupid.inc++;
    return a;
}

assert( 0 == stupid.inc );
foo.cache( 100000 )
assert( 1 == stupid.inc , stupid.inc );
foo.cache( 100000 )
assert( 1 == stupid.inc );

foo.cache( 100000 , { name : "asd" } );
assert( 2 == stupid.inc );
foo.cache( 100000 , { name : "asd" } );
assert( 2 == stupid.inc );

foo.cache( 100000 , "nuts" );
assert( 3 == stupid.inc );
foo.cache( 100000 , "nuts" );
assert( 3 == stupid.inc );

foo.cache( 100000 , { name : "asd" , foo : 1 } );
assert( 4 == stupid.inc );
foo.cache( 100000 , { name : "asd" , foo : 1 } );
assert( 4 == stupid.inc );

foo.cache( 100000 , { name : "asd" , foo : 2 } );
assert( 5 == stupid.inc );
foo.cache( 100000 , { name : "asd" , foo : 2 } );
assert( 5 == stupid.inc );

foo.cache( 100000 , { name : "asd" , foo : { z : 1 } } );
assert( 6 == stupid.inc );
foo.cache( 100000 , { name : "asd" , foo : { z : 1 } } );
assert( 6 == stupid.inc );

foo.cache( 100000 , { name : "asd" , foo : { z : 2 } } );
assert( 7 == stupid.inc );
foo.cache( 100000 , { name : "asd" , foo : { z : 2 } } );
assert( 7 == stupid.inc );

<|endoftext|>


<commit_before>import React, { Component, PropTypes } from 'react'
import Vertical from './vertical.js'

export default class Panel extends Component {
  render() {
    const { props } = this

    return (
      <Vertical
        ref={props._ref}
        style={{
          height: '100%',
          overflowY: 'auto',
          ...props.style
        }}
      >
        {props.children}
      </Vertical>
    )
  }
}
Panel.propTypes = {
  _ref: PropTypes.func,
  style: PropTypes.object
}
<commit_msg>fix: add aria stuff to panel for legacy pages<commit_after>import React, { Component, PropTypes } from 'react'
import Vertical from './vertical.js'

export default class Panel extends Component {
  render() {
    const { props } = this

    return (
      <Vertical
        aria-labelledby={props['aria-labelledby']}
        role={props.role}
        ref={props._ref}
        style={{
          height: '100%',
          overflowY: 'auto',
          ...props.style
        }}
      >
        {props.children}
      </Vertical>
    )
  }
}
Panel.propTypes = {
  _ref: PropTypes.func,
  style: PropTypes.object
}
<|endoftext|>


<commit_before>#!/usr/bin/env node

var dbinit = require('../lib/db'),
    args = require('optimist').argv,
    split = require('split'),
    learner = require('../lib/learner'),
    XRegExp = require('xregexp').XRegExp,
    loadConfig = require('../lib/load-config'),
    async = require('async');

function createTimestamp(spec, defaults) {
    if (spec.timestampms)
        return spec.timestampms;
    else if (spec.timestamp)
        return spec.timestamp;
    spec.year = spec.year || defaults.getYear();
    spec.month = spec.month || defaults.getMonth() + 1;
    spec.day = spec.day  || defaults.getDay();
    spec.hour = spec.hour || defauls.getHours();
    spec.minute = spec.minute || defaults.getMinutes();
    spec.second = spec.second || defaults.getSeconds();
    return new Date(spec.year, spec.month - 1, spec.day, spec.hour, spec.minute, spec.second).getTime();
}

var config = loadConfig(args);

dbinit({
    mode: 'feed',
    name: 'triplie.db'
}, function(err, db) {

    var lines = 0;

    var learn = learner(db, {inBatch: true});
    db.batch.begin();

    var past = Date.now() - 30 * 24 * 3600 * 1000; // 30 days
    var last = Date.now();
    var learnQueue = async.queue(function(l, cb) {
        learn(l.text, l.timestamp || (past + lines * 5000), function(err) {
            if (++lines % 20 === 0)
                process.stdout.write('+');
            if (lines % 100 === 0)
                process.stdout.write(' ');
            if (lines % 1000 === 0) {
                var spd = 1000 / ((Date.now() - last) / 1000);
                console.log((lines/1000).toFixed(0)+'K', 'at', spd.toFixed(1), 'l/s');
                last = Date.now();
            }
            cb(err);
        });
    }, 1);

    var firstLine = true;
    var today = new Date();
    var reg = new XRegExp(args.regex);
    process.stdin.pipe(split()).on('data', function(line) {
        var m = XRegExp.exec(line, reg);
        var ts = null;
        if (m.year || m.month || m.day || m.hour || m.minute || m.second)
            ts = createTimestamp(m, today);
        if (firstLine) {
            console.log('First line is', m && m.text);
            console.log('Happening at', new Date(ts));
            firstLine = false;
        }
        if (m) learnQueue.push({text: m.text, timestamp: ts});
    });

    learnQueue.drain =  function() {
        db.batch.end();
        db.close(function() {});
        console.log("=");
        console.log("done");
    };
});
<commit_msg>fixed feed command<commit_after>#!/usr/bin/env node

var dbinit = require('../lib/db'),
    args = require('optimist').argv,
    split = require('split'),
    learner = require('../lib/learner'),
    XRegExp = require('xregexp').XRegExp,
    loadConfig = require('../lib/load-config'),
    async = require('async');

function createTimestamp(spec, defaults) {
    if (spec.timestampms)
        return spec.timestampms;
    else if (spec.timestamp)
        return spec.timestamp * 1000;
    spec.year = spec.year || defaults.getYear();
    spec.month = spec.month || defaults.getMonth() + 1;
    spec.day = spec.day  || defaults.getDay();
    spec.hour = spec.hour || defauls.getHours();
    spec.minute = spec.minute || defaults.getMinutes();
    spec.second = spec.second || defaults.getSeconds();
    return new Date(spec.year, spec.month - 1, spec.day, spec.hour, spec.minute, spec.second).getTime();
}

var config = loadConfig(args);

dbinit({
    mode: 'feed',
    name: 'triplie.db'
}, function(err, db) {

    var lines = 0;

    var learn = learner(db, {inBatch: true});
    db.batch.begin();

    var past = Date.now() - 30 * 24 * 3600 * 1000; // 30 days
    var last = Date.now();
    var learnQueue = async.queue(function(l, cb) {
        learn(l.text, l.timestamp || (past + lines * 5000), function(err) {
            if (++lines % 20 === 0)
                process.stdout.write('+');
            if (lines % 100 === 0)
                process.stdout.write(' ');
            if (lines % 1000 === 0) {
                var spd = 1000 / ((Date.now() - last) / 1000);
                console.log((lines/1000).toFixed(0)+'K', 'at', spd.toFixed(1), 'l/s');
                last = Date.now();
            }
            cb(err);
        });
    }, 1);

    var firstLine = true;
    var today = new Date();
    var reg = new XRegExp(args.regex);
    process.stdin.pipe(split()).on('data', function(line) {
        var m = XRegExp.exec(line, reg);
        var ts = null;
        if (!m) return;
        if (m.year || m.month || m.day || m.hour || m.minute
            || m.second || m.timestamp || m.timestampms)
            ts = createTimestamp(m, today);
        if (firstLine) {
            console.log('First line is', m && m.text);
            console.log('Happening at', new Date(ts));
            firstLine = false;
        }
        if (m) learnQueue.push({text: m.text, timestamp: ts});
    });

    learnQueue.drain =  function() {
        db.batch.end();
        db.close(function() {});
        console.log("=");
        console.log("done");
    };
});
<|endoftext|>


<commit_before>#!/usr/bin/env node

var jet = require('../lib/jet');
jet.createDaemon().listen(33326);
<commit_msg>port has changed<commit_after>#!/usr/bin/env node

var jet = require('../lib/jet');
jet.createDaemon().listen(11122);
<|endoftext|>


<commit_before>import external from '../externalModules.js';
import BaseTool from './base/BaseTool.js';

/**
 *
 * http://dicom.nema.org/dicom/2013/output/chtml/part03/sect_C.9.html
 *
 * @public
 * @class Overlay
 * @memberof Tools
 *
 * @classdesc Tool for displaying a scale overlay on the image.
 * @extends Tools.Base.BaseTool
 */
export default class OverlayTool extends BaseTool {
  constructor(configuration = {}) {
    const defaultConfig = {
      name: 'Overlay',
      configuration: {},
      mixins: ['enabledOrDisabledBinaryTool'],
    };
    const initialConfiguration = Object.assign(defaultConfig, configuration);

    super(initialConfiguration);

    this.initialConfiguration = initialConfiguration;
  }

  enabledCallback(element) {
    this.forceImageUpdate(element);
  }

  disabledCallback(element) {
    this.forceImageUpdate(element);
  }

  forceImageUpdate(element) {
    const enabledElement = external.cornerstone.getEnabledElement(element);

    if (enabledElement.image) {
      external.cornerstone.updateImage(element);
    }
  }

  renderToolData(evt) {
    const eventData = evt.detail;
    const { enabledElement, image, viewport, canvasContext } = eventData;

    if (!eventData || !enabledElement || !image) {
      return;
    }

    const overlayPlaneMetadata = external.cornerstone.metaData.get(
      'overlayPlaneModule',
      image.imageId
    );

    if (
      !overlayPlaneMetadata ||
      !overlayPlaneMetadata.overlays ||
      !overlayPlaneMetadata.overlays.length
    ) {
      return;
    }

    const viewportPixelSpacing = {
      column: viewport.displayedArea.columnPixelSpacing || 1,
      row: viewport.displayedArea.rowPixelSpacing || 1,
    };
    const imageWidth =
      Math.abs(viewport.displayedArea.brhc.x - viewport.displayedArea.tlhc.x) *
      viewportPixelSpacing.column;
    const imageHeight =
      Math.abs(viewport.displayedArea.brhc.y - viewport.displayedArea.tlhc.y) *
      viewportPixelSpacing.row;

    overlayPlaneMetadata.overlays.forEach(overlay => {
      if (overlay.visible === false) {
        return;
      }

      const layerCanvas = document.createElement('canvas');

      layerCanvas.width = imageWidth;
      layerCanvas.height = imageHeight;

      const layerContext = layerCanvas.getContext('2d');

      layerContext.fillStyle = overlay.fillStyle || 'white';

      if (overlay.type === 'R') {
        layerContext.fillRect(0, 0, layerCanvas.width, layerCanvas.height);
        layerContext.globalCompositeOperation = 'xor';
      }

      let i = 0;

      for (let y = 0; y < overlay.rows; y++) {
        for (let x = 0; x < overlay.columns; x++) {
          if (overlay.pixelData[i++] > 0) {
            layerContext.fillRect(x, y, 1, 1);
          }
        }
      }

      // Draw the overlay layer onto the canvas
      canvasContext.drawImage(layerCanvas, overlay.x, overlay.y);
    });
  }
}
<commit_msg>fix: guard against non-number values<commit_after>import external from '../externalModules.js';
import BaseTool from './base/BaseTool.js';

/**
 *
 * http://dicom.nema.org/dicom/2013/output/chtml/part03/sect_C.9.html
 *
 * @public
 * @class Overlay
 * @memberof Tools
 *
 * @classdesc Tool for displaying a scale overlay on the image.
 * @extends Tools.Base.BaseTool
 */
export default class OverlayTool extends BaseTool {
  constructor(configuration = {}) {
    const defaultConfig = {
      name: 'Overlay',
      configuration: {},
      mixins: ['enabledOrDisabledBinaryTool'],
    };
    const initialConfiguration = Object.assign(defaultConfig, configuration);

    super(initialConfiguration);

    this.initialConfiguration = initialConfiguration;
  }

  enabledCallback(element) {
    this.forceImageUpdate(element);
  }

  disabledCallback(element) {
    this.forceImageUpdate(element);
  }

  forceImageUpdate(element) {
    const enabledElement = external.cornerstone.getEnabledElement(element);

    if (enabledElement.image) {
      external.cornerstone.updateImage(element);
    }
  }

  renderToolData(evt) {
    const eventData = evt.detail;
    const { enabledElement, image, viewport, canvasContext } = eventData;

    if (!eventData || !enabledElement || !image) {
      return;
    }

    const overlayPlaneMetadata = external.cornerstone.metaData.get(
      'overlayPlaneModule',
      image.imageId
    );

    if (
      !overlayPlaneMetadata ||
      !overlayPlaneMetadata.overlays ||
      !overlayPlaneMetadata.overlays.length
    ) {
      return;
    }

    const viewportPixelSpacing = {
      column: viewport.displayedArea.columnPixelSpacing || 1,
      row: viewport.displayedArea.rowPixelSpacing || 1,
    };
    const imageWidth =
      Math.abs(viewport.displayedArea.brhc.x - viewport.displayedArea.tlhc.x) *
      viewportPixelSpacing.column;
    const imageHeight =
      Math.abs(viewport.displayedArea.brhc.y - viewport.displayedArea.tlhc.y) *
      viewportPixelSpacing.row;

    overlayPlaneMetadata.overlays.forEach(overlay => {
      if (overlay.visible === false) {
        return;
      }

      const layerCanvas = document.createElement('canvas');

      layerCanvas.width = imageWidth;
      layerCanvas.height = imageHeight;

      const layerContext = layerCanvas.getContext('2d');

      layerContext.fillStyle = overlay.fillStyle || 'white';

      if (overlay.type === 'R') {
        layerContext.fillRect(0, 0, layerCanvas.width, layerCanvas.height);
        layerContext.globalCompositeOperation = 'xor';
      }

      let i = 0;

      for (let y = 0; y < overlay.rows; y++) {
        for (let x = 0; x < overlay.columns; x++) {
          if (overlay.pixelData[i++] > 0) {
            layerContext.fillRect(x, y, 1, 1);
          }
        }
      }

      // Guard against non-number values
      const overlayX = (!isNaN(parseFloat(overlay.x)) && isFinite(overlay.x)) ? overlay.x : 0;
      const overlayY = (!isNaN(parseFloat(overlay.y)) && isFinite(overlay.y)) ? overlay.y : 0;

      // Draw the overlay layer onto the canvas
      canvasContext.drawImage(layerCanvas, overlayX, overlayY);
    });
  }
}
<|endoftext|>