<commit_before>/**
 * Pimcore
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://www.pimcore.org/license
 *
 * @copyright  Copyright (c) 2009-2010 elements.at New Media Solutions GmbH (http://www.elements.at)
 * @license    http://www.pimcore.org/license     New BSD License
 */

pimcore.registerNS("pimcore.object.helpers.gridTabAbstract");
pimcore.object.helpers.gridTabAbstract = Class.create({

    objecttype: 'object',

    filterUpdateFunction: function(gridfilters, toolbarFilterInfo) {
        var filterString = "";
        var filterStringConfig = [];
        var filterData = gridfilters.getFilterData();
        var operator;

        // reset
        toolbarFilterInfo.setText(" ");

        if(filterData.length > 0) {

            for (var i=0; i<filterData.length; i++) {

                operator = "=";
                if (filterData[i].data.type == "string") {
                    operator = "LIKE";
                } else if (filterData[i].data.type == "numeric" || filterData[i].data.type == "date") {
                    if(filterData[i].data.comparison == "lt") {
                        operator = "<";
                    } else if(filterData[i].data.comparison == "gt") {
                        operator = ">";
                    }
                } else if (filterData[i].data.type == "boolean") {
                   filterData[i].value = filterData[i].data.value ? "true" : "false";
                }

                if(filterData[i].data.value && typeof filterData[i].data.value == "object") {
                    filterStringConfig.push(filterData[i].field + " " + operator + " (" + filterData[i].data.value.join(" OR ") + ")");
                } else {
                    filterStringConfig.push(filterData[i].field + " " + operator + " " + filterData[i].data.value);
                }
            }

            toolbarFilterInfo.setText("" + t("filter_condition") + ": " + filterStringConfig.join(" AND ") + "");
        }
    },

    updateGridHeaderContextMenu: function(grid) {
        var columnConfig = new Ext.menu.Item({
            text: t("grid_column_config"),
            iconCls: "pimcore_icon_grid_column_config",
            handler: this.openColumnConfig.bind(this)
        });
        grid.getView().hmenu.add(columnConfig);

        var batchAllMenu = new Ext.menu.Item({
            text: t("batch_change"),
            iconCls: "pimcore_icon_batch",
            handler: function (view) {
                this.batchPrepare(view.hdCtxIndex, false);
            }.bind(this, grid.getView())
        });
        grid.getView().hmenu.add(batchAllMenu);

        var batchSelectedMenu = new Ext.menu.Item({
            text: t("batch_change_selected"),
            iconCls: "pimcore_icon_batch",
            handler: function (view) {
                this.batchPrepare(view.hdCtxIndex, true);
            }.bind(this, grid.getView())
        });
        grid.getView().hmenu.add(batchSelectedMenu);

        grid.getView().hmenu.on('beforeshow', function (batchAllMenu, batchSelectedMenu, view) {
            // no batch for system properties
            if(this.systemColumns.indexOf(view.cm.config[view.hdCtxIndex].dataIndex) < 0) {
                batchAllMenu.show();
                batchSelectedMenu.show();
            } else {
                batchAllMenu.hide();
                batchSelectedMenu.hide();
            }

        }.bind(this, batchAllMenu, batchSelectedMenu, grid.getView()));
    },

    batchPrepare: function(columnIndex, onlySelected){
        // no batch for system properties
        if(this.systemColumns.indexOf(this.grid.getColumnModel().config[columnIndex].dataIndex) > -1) {
            return;
        }

        var jobs = [];
        if(onlySelected) {
            var selectedRows = this.grid.getSelectionModel().getSelections();
            for (var i=0; i<selectedRows.length; i++) {
                jobs.push(selectedRows[i].get("id"));
            }
            this.batchOpen(columnIndex,jobs);

        } else {

            var filters = "";
            var condition = "";

            if(this.sqlButton.pressed) {
                condition = this.sqlEditor.getValue();
            } else {
                var filterData = this.gridfilters.getFilterData();
                if(filterData.length > 0) {
                    filters = this.gridfilters.buildQuery(filterData).filter;
                }
            }

           var params = {
                filter: filters,
                condition: condition,
                classId: this.classId,
                folderId: this.element.id,
                objecttype: this.objecttype
            };

            Ext.Ajax.request({
                url: "/admin/object-helper/get-batch-jobs",
                params: params,
                success: function (columnIndex,response) {
                    var rdata = Ext.decode(response.responseText);
                    if (rdata.success && rdata.jobs) {
                        this.batchOpen(columnIndex, rdata.jobs);
                    }

                }.bind(this,columnIndex)
            });
        }

    },

    batchOpen: function (columnIndex, jobs) {

        columnIndex = columnIndex-1;

        var fieldInfo = this.grid.getColumnModel().config[columnIndex+1];

        // HACK: typemapping for published (systemfields) because they have no edit masks, so we use them from the data-types
        if(fieldInfo.dataIndex == "published") {
            fieldInfo.layout = {
                layout: {
                    title: t("published"),
                    name: "published"
                },
                type: "checkbox"
            }
        }
        // HACK END

        if(!fieldInfo.layout || !fieldInfo.layout.layout) {
            return;
        }

        if(fieldInfo.layout.layout.noteditable) {
            Ext.MessageBox.alert(t('error'), t('this_element_cannot_be_edited'));
            return;
        }

        var editor = new pimcore.object.tags[fieldInfo.layout.type](null, fieldInfo.layout.layout);
        this.batchWin = new Ext.Window({
            modal: true,
            title: t("batch_edit_field") + " " + fieldInfo.header,
            items: [
                {
                    xtype: "form",
                    border: false,
                    items: [editor.getLayoutEdit()],
                    bodyStyle: "padding: 10px;",
                    buttons: [
                        {
                            text: t("save"),
                            handler: this.batchProcess.bind(this, jobs, editor, fieldInfo, true)
                        }
                    ]
                }
            ],
            bodyStyle: "background: #fff;",
            width: 700
        });
        this.batchWin.show();

    },

    batchProcess: function (jobs,  editor, fieldInfo, initial) {

        if(initial){

            this.batchErrors = [];
            this.batchJobCurrent = 0;

            var newValue = editor.getValue();

            var valueType = "primitive";
            if (newValue && typeof newValue == "object") {
                newValue = Ext.encode(newValue);
                valueType = "object";
            }

            this.batchParameters = {
                name: fieldInfo.dataIndex,
                value: newValue,
                valueType: valueType
            };

            this.batchWin.close();

            this.batchProgressBar = new Ext.ProgressBar({
                text: t('Initializing'),
                style: "margin: 10px;",
                width: 500
            });

            this.batchProgressWin = new Ext.Window({
                items: [this.batchProgressBar],
                modal: true,
                bodyStyle: "background: #fff;",
                closable: false
            });
            this.batchProgressWin.show();

        }

        if (this.batchJobCurrent >= jobs.length) {
            this.batchProgressWin.close();
            this.pagingtoolbar.moveFirst();
            try {
                pimcore.globalmanager.get("layout_object_tree").tree.getRootNode().reload();
            } catch (e) {
                console.log(e);
            }

            // error handling
            if (this.batchErrors.length > 0) {
                var jobErrors = [];
                for (var i = 0; i < this.batchErrors.length; i++) {
                    jobErrors.push(this.batchErrors[i].job);
                }
                Ext.Msg.alert(t("error"), t("error_jobs") + ": " + jobErrors.join(","));
            }

            return;
        }

        var status = (this.batchJobCurrent / jobs.length);
        var percent = Math.ceil(status * 100);
        this.batchProgressBar.updateProgress(status, percent + "%");

        this.batchParameters.job = jobs[this.batchJobCurrent];
        Ext.Ajax.request({
            url: "/admin/object-helper/batch",
            params: this.batchParameters,
            success: function (jobs, currentJob, response) {

                try {
                    var rdata = Ext.decode(response.responseText);
                    if (rdata) {
                        if (!rdata.success) {
                            throw "not successful";
                        }
                    }
                } catch (e) {
                    this.batchErrors.push({
                        job: currentJob
                    });
                }

                window.setTimeout(function() {
                    this.batchJobCurrent++;
                    this.batchProcess(jobs);
                }.bind(this), 400);
            }.bind(this,jobs, this.batchParameters.job)
        });
    },

    openColumnConfig: function() {
        var fields = this.getGridConfig().columns;

        var fieldKeys = Object.keys(fields);

        var visibleColumns = [];
        for(var i = 0; i < fieldKeys.length; i++) {
            if(!fields[fieldKeys[i]].hidden) {
                visibleColumns.push({
                    key: fieldKeys[i],
                    label: fields[fieldKeys[i]].fieldConfig.label,
                    dataType: fields[fieldKeys[i]].fieldConfig.type,
                    layout: fields[fieldKeys[i]].fieldConfig.layout
                });
            }
        }

        var columnConfig = {
            language: this.gridLanguage,
            classid: this.classId,
            selectedGridColumns: visibleColumns
        };
        var dialog = new pimcore.object.helpers.gridConfigDialog(columnConfig, function(data) {
            this.gridLanguage = data.language;
            this.createGrid(data.columns);
        }.bind(this) );
    },

    createGrid: function(columnConfig) {

    },

    getGridConfig : function () {
        var config = {
            language: this.gridLanguage,
            sortinfo: this.sortinfo,
            columns: {}
        };
        var cm = this.grid.getColumnModel();
        for (var i=0; i<cm.config.length; i++) {
            if(cm.config[i].dataIndex) {
                config.columns[cm.config[i].dataIndex] = {
                    name: cm.config[i].dataIndex,
                    position: i,
                    hidden: cm.config[i].hidden,
                    fieldConfig: this.fieldObject[cm.config[i].dataIndex]
                };
            }
        }

        return config;
    },

    startCsvExport: function () {
        var values = [];
        var filters = "";
        var condition = "";

        if(this.sqlButton.pressed) {
            condition = this.sqlEditor.getValue();
        } else {
            var filterData = this.gridfilters.getFilterData();
            if(filterData.length > 0) {
                filters = this.gridfilters.buildQuery(filterData).filter;
            }
        }

        var path = "/admin/object-helper/export/classId/" + this.classId + "/folderId/" + this.element.id ;
        path = path + "/?" + Ext.urlEncode({
            filter: filters,
            condition: condition,
            objecttype: this.objecttype
        });
        pimcore.helpers.download(path);
    },

    createSqlEditor: function() {
        this.sqlEditor = new Ext.form.TextField({
            xtype: "textfield",
            width: 500,
            name: "condition",
            hidden: true,
            enableKeyEvents: true,
            listeners: {
                "keydown" : function (field, key) {
                    if (key.getKey() == key.ENTER) {
                        this.gridfilters.clearFilters();

                        this.store.baseparams = {};
                        this.store.setBaseParam("condition", field.getValue());

                        this.pagingtoolbar.moveFirst();
                    }
                }.bind(this)
            }
        });

        this.sqlButton = new Ext.Button({
            iconCls: "pimcore_icon_sql",
            enableToggle: true,
            tooltip: t("direct_sql_query"),
            handler: function (button) {

                this.gridfilters.clearFilters();
                this.sqlEditor.setValue("");

                // reset base params, because of the condition
                this.store.baseparams = {};
                this.store.setBaseParam("condition", null);
                this.pagingtoolbar.moveFirst();

                if(button.pressed) {
                    this.sqlEditor.show();
                } else {
                    this.sqlEditor.hide();
                }
            }.bind(this)
        });

    }

});<commit_msg>PIMCORE-1393 - Batch change selected / greyed area too big<commit_after>/**
 * Pimcore
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://www.pimcore.org/license
 *
 * @copyright  Copyright (c) 2009-2010 elements.at New Media Solutions GmbH (http://www.elements.at)
 * @license    http://www.pimcore.org/license     New BSD License
 */

pimcore.registerNS("pimcore.object.helpers.gridTabAbstract");
pimcore.object.helpers.gridTabAbstract = Class.create({

    objecttype: 'object',

    filterUpdateFunction: function(gridfilters, toolbarFilterInfo) {
        var filterString = "";
        var filterStringConfig = [];
        var filterData = gridfilters.getFilterData();
        var operator;

        // reset
        toolbarFilterInfo.setText(" ");

        if(filterData.length > 0) {

            for (var i=0; i<filterData.length; i++) {

                operator = "=";
                if (filterData[i].data.type == "string") {
                    operator = "LIKE";
                } else if (filterData[i].data.type == "numeric" || filterData[i].data.type == "date") {
                    if(filterData[i].data.comparison == "lt") {
                        operator = "<";
                    } else if(filterData[i].data.comparison == "gt") {
                        operator = ">";
                    }
                } else if (filterData[i].data.type == "boolean") {
                   filterData[i].value = filterData[i].data.value ? "true" : "false";
                }

                if(filterData[i].data.value && typeof filterData[i].data.value == "object") {
                    filterStringConfig.push(filterData[i].field + " " + operator + " (" + filterData[i].data.value.join(" OR ") + ")");
                } else {
                    filterStringConfig.push(filterData[i].field + " " + operator + " " + filterData[i].data.value);
                }
            }

            toolbarFilterInfo.setText("" + t("filter_condition") + ": " + filterStringConfig.join(" AND ") + "");
        }
    },

    updateGridHeaderContextMenu: function(grid) {
        var columnConfig = new Ext.menu.Item({
            text: t("grid_column_config"),
            iconCls: "pimcore_icon_grid_column_config",
            handler: this.openColumnConfig.bind(this)
        });
        grid.getView().hmenu.add(columnConfig);

        var batchAllMenu = new Ext.menu.Item({
            text: t("batch_change"),
            iconCls: "pimcore_icon_batch",
            handler: function (view) {
                this.batchPrepare(view.hdCtxIndex, false);
            }.bind(this, grid.getView())
        });
        grid.getView().hmenu.add(batchAllMenu);

        var batchSelectedMenu = new Ext.menu.Item({
            text: t("batch_change_selected"),
            iconCls: "pimcore_icon_batch",
            handler: function (view) {
                this.batchPrepare(view.hdCtxIndex, true);
            }.bind(this, grid.getView())
        });
        grid.getView().hmenu.add(batchSelectedMenu);

        grid.getView().hmenu.on('beforeshow', function (batchAllMenu, batchSelectedMenu, view) {
            // no batch for system properties
            if(this.systemColumns.indexOf(view.cm.config[view.hdCtxIndex].dataIndex) < 0) {
                batchAllMenu.show();
                batchSelectedMenu.show();
            } else {
                batchAllMenu.hide();
                batchSelectedMenu.hide();
            }

        }.bind(this, batchAllMenu, batchSelectedMenu, grid.getView()));
    },

    batchPrepare: function(columnIndex, onlySelected){
        // no batch for system properties
        if(this.systemColumns.indexOf(this.grid.getColumnModel().config[columnIndex].dataIndex) > -1) {
            return;
        }

        var jobs = [];
        if(onlySelected) {
            var selectedRows = this.grid.getSelectionModel().getSelections();
            for (var i=0; i<selectedRows.length; i++) {
                jobs.push(selectedRows[i].get("id"));
            }
            this.batchOpen(columnIndex,jobs);

        } else {

            var filters = "";
            var condition = "";

            if(this.sqlButton.pressed) {
                condition = this.sqlEditor.getValue();
            } else {
                var filterData = this.gridfilters.getFilterData();
                if(filterData.length > 0) {
                    filters = this.gridfilters.buildQuery(filterData).filter;
                }
            }

           var params = {
                filter: filters,
                condition: condition,
                classId: this.classId,
                folderId: this.element.id,
                objecttype: this.objecttype
            };

            Ext.Ajax.request({
                url: "/admin/object-helper/get-batch-jobs",
                params: params,
                success: function (columnIndex,response) {
                    var rdata = Ext.decode(response.responseText);
                    if (rdata.success && rdata.jobs) {
                        this.batchOpen(columnIndex, rdata.jobs);
                    }

                }.bind(this,columnIndex)
            });
        }

    },

    batchOpen: function (columnIndex, jobs) {

        columnIndex = columnIndex-1;

        var fieldInfo = this.grid.getColumnModel().config[columnIndex+1];

        // HACK: typemapping for published (systemfields) because they have no edit masks, so we use them from the data-types
        if(fieldInfo.dataIndex == "published") {
            fieldInfo.layout = {
                layout: {
                    title: t("published"),
                    name: "published"
                },
                type: "checkbox"
            }
        }
        // HACK END

        if(!fieldInfo.layout || !fieldInfo.layout.layout) {
            return;
        }

        if(fieldInfo.layout.layout.noteditable) {
            Ext.MessageBox.alert(t('error'), t('this_element_cannot_be_edited'));
            return;
        }

        var editor = new pimcore.object.tags[fieldInfo.layout.type](null, fieldInfo.layout.layout);
        this.batchWin = new Ext.Window({
            modal: false,
            title: t("batch_edit_field") + " " + fieldInfo.header,
            items: [
                {
                    xtype: "form",
                    border: false,
                    items: [editor.getLayoutEdit()],
                    bodyStyle: "padding: 10px;",
                    buttons: [
                        {
                            text: t("save"),
                            handler: this.batchProcess.bind(this, jobs, editor, fieldInfo, true)
                        }
                    ]
                }
            ],
            bodyStyle: "background: #fff;",
            width: 700
        });
        this.batchWin.show();

    },

    batchProcess: function (jobs,  editor, fieldInfo, initial) {

        if(initial){

            this.batchErrors = [];
            this.batchJobCurrent = 0;

            var newValue = editor.getValue();

            var valueType = "primitive";
            if (newValue && typeof newValue == "object") {
                newValue = Ext.encode(newValue);
                valueType = "object";
            }

            this.batchParameters = {
                name: fieldInfo.dataIndex,
                value: newValue,
                valueType: valueType
            };

            this.batchWin.close();

            this.batchProgressBar = new Ext.ProgressBar({
                text: t('Initializing'),
                style: "margin: 10px;",
                width: 500
            });

            this.batchProgressWin = new Ext.Window({
                items: [this.batchProgressBar],
                modal: true,
                bodyStyle: "background: #fff;",
                closable: false
            });
            this.batchProgressWin.show();

        }

        if (this.batchJobCurrent >= jobs.length) {
            this.batchProgressWin.close();
            this.pagingtoolbar.moveFirst();
            try {
                pimcore.globalmanager.get("layout_object_tree").tree.getRootNode().reload();
            } catch (e) {
                console.log(e);
            }

            // error handling
            if (this.batchErrors.length > 0) {
                var jobErrors = [];
                for (var i = 0; i < this.batchErrors.length; i++) {
                    jobErrors.push(this.batchErrors[i].job);
                }
                Ext.Msg.alert(t("error"), t("error_jobs") + ": " + jobErrors.join(","));
            }

            return;
        }

        var status = (this.batchJobCurrent / jobs.length);
        var percent = Math.ceil(status * 100);
        this.batchProgressBar.updateProgress(status, percent + "%");

        this.batchParameters.job = jobs[this.batchJobCurrent];
        Ext.Ajax.request({
            url: "/admin/object-helper/batch",
            params: this.batchParameters,
            success: function (jobs, currentJob, response) {

                try {
                    var rdata = Ext.decode(response.responseText);
                    if (rdata) {
                        if (!rdata.success) {
                            throw "not successful";
                        }
                    }
                } catch (e) {
                    this.batchErrors.push({
                        job: currentJob
                    });
                }

                window.setTimeout(function() {
                    this.batchJobCurrent++;
                    this.batchProcess(jobs);
                }.bind(this), 400);
            }.bind(this,jobs, this.batchParameters.job)
        });
    },

    openColumnConfig: function() {
        var fields = this.getGridConfig().columns;

        var fieldKeys = Object.keys(fields);

        var visibleColumns = [];
        for(var i = 0; i < fieldKeys.length; i++) {
            if(!fields[fieldKeys[i]].hidden) {
                visibleColumns.push({
                    key: fieldKeys[i],
                    label: fields[fieldKeys[i]].fieldConfig.label,
                    dataType: fields[fieldKeys[i]].fieldConfig.type,
                    layout: fields[fieldKeys[i]].fieldConfig.layout
                });
            }
        }

        var columnConfig = {
            language: this.gridLanguage,
            classid: this.classId,
            selectedGridColumns: visibleColumns
        };
        var dialog = new pimcore.object.helpers.gridConfigDialog(columnConfig, function(data) {
            this.gridLanguage = data.language;
            this.createGrid(data.columns);
        }.bind(this) );
    },

    createGrid: function(columnConfig) {

    },

    getGridConfig : function () {
        var config = {
            language: this.gridLanguage,
            sortinfo: this.sortinfo,
            columns: {}
        };
        var cm = this.grid.getColumnModel();
        for (var i=0; i<cm.config.length; i++) {
            if(cm.config[i].dataIndex) {
                config.columns[cm.config[i].dataIndex] = {
                    name: cm.config[i].dataIndex,
                    position: i,
                    hidden: cm.config[i].hidden,
                    fieldConfig: this.fieldObject[cm.config[i].dataIndex]
                };
            }
        }

        return config;
    },

    startCsvExport: function () {
        var values = [];
        var filters = "";
        var condition = "";

        if(this.sqlButton.pressed) {
            condition = this.sqlEditor.getValue();
        } else {
            var filterData = this.gridfilters.getFilterData();
            if(filterData.length > 0) {
                filters = this.gridfilters.buildQuery(filterData).filter;
            }
        }

        var path = "/admin/object-helper/export/classId/" + this.classId + "/folderId/" + this.element.id ;
        path = path + "/?" + Ext.urlEncode({
            filter: filters,
            condition: condition,
            objecttype: this.objecttype
        });
        pimcore.helpers.download(path);
    },

    createSqlEditor: function() {
        this.sqlEditor = new Ext.form.TextField({
            xtype: "textfield",
            width: 500,
            name: "condition",
            hidden: true,
            enableKeyEvents: true,
            listeners: {
                "keydown" : function (field, key) {
                    if (key.getKey() == key.ENTER) {
                        this.gridfilters.clearFilters();

                        this.store.baseparams = {};
                        this.store.setBaseParam("condition", field.getValue());

                        this.pagingtoolbar.moveFirst();
                    }
                }.bind(this)
            }
        });

        this.sqlButton = new Ext.Button({
            iconCls: "pimcore_icon_sql",
            enableToggle: true,
            tooltip: t("direct_sql_query"),
            handler: function (button) {

                this.gridfilters.clearFilters();
                this.sqlEditor.setValue("");

                // reset base params, because of the condition
                this.store.baseparams = {};
                this.store.setBaseParam("condition", null);
                this.pagingtoolbar.moveFirst();

                if(button.pressed) {
                    this.sqlEditor.show();
                } else {
                    this.sqlEditor.hide();
                }
            }.bind(this)
        });

    }

});<|endoftext|>


<commit_before>'use strict';

const R = require('ramda');
const queryString = require('querystring');
const request = require('./utils/request');
const scriptData = require('./utils/scriptData');
const { BASE_URL } = require('./constants');
const helper = require('./utils/mappingHelpers');

const PLAYSTORE_URL = `${BASE_URL}/store/apps/details`;

function app (opts) {
  return new Promise(function (resolve, reject) {
    if (!opts || !opts.appId) {
      throw Error('appId missing');
    }

    opts.lang = opts.lang || 'en';
    opts.country = opts.country || 'us';

    const qs = queryString.stringify({
      id: opts.appId,
      hl: opts.lang,
      gl: opts.country
    });
    const reqUrl = `${PLAYSTORE_URL}?${qs}`;

    const options = Object.assign({
      url: reqUrl,
      followRedirect: true
    }, opts.requestOptions);

    request(options, opts.throttle)
      .then(scriptData.parse)
    // comment next line to get raw data
      .then(scriptData.extractor(MAPPINGS))
      .then(R.assoc('appId', opts.appId))
      .then(R.assoc('url', reqUrl))
      .then(resolve)
      .catch(reject);
  });
}

const MAPPINGS = {
  title: ['ds:4', 1, 2, 0, 0],
  description: {
    path: ['ds:4', 1, 2, 72, 0, 1],
    fun: helper.descriptionText
  },
  descriptionHTML: ['ds:4', 1, 2, 72, 0, 1],
  summary: ['ds:4', 1, 2, 73, 0, 1],
  installs: ['ds:4', 1, 2, 13, 0],
  minInstalls: ['ds:4', 1, 2, 13, 1],
  maxInstalls: ['ds:4', 1, 2, 13, 2],
  score: ['ds:4', 1, 2, 51, 0, 1],
  scoreText: ['ds:4', 1, 2, 51, 0, 0],
  ratings: ['ds:4', 1, 2, 51, 2, 1],
  reviews: ['ds:4', 1, 2, 51, 3, 1],
  histogram: {
    path: ['ds:4', 1, 2, 51, 1],
    fun: helper.buildHistogram
  },
  price: {
    path: ['ds:4', 1, 2, 57, 0, 0, 0, 0, 1, 0, 0],
    fun: (val) => val / 1000000 || 0
  },
  free: {
    path: ['ds:4', 1, 2, 57, 0, 0, 0, 0, 1, 0, 0],
    // considered free only if price is exactly zero
    fun: (val) => val === 0
  },
  currency: ['ds:4', 1, 2, 57, 0, 0, 0, 0, 1, 0, 1],
  priceText: {
    path: ['ds:4', 1, 2, 57, 0, 0, 0, 0, 1, 0, 2],
    fun: helper.priceText
  },
  available: {
    path: ['ds:4', 1, 2, 18, 0],
    fun: Boolean
  },
  offersIAP: {
    path: ['ds:4', 1, 2, 19, 0],
    fun: Boolean
  },
  IAPRange: ['ds:4', 1, 2, 19, 0],
  androidVersion: {
    path: ['ds:4', 1, 2, 140, 1, 1, 0, 0, 1],
    fun: helper.normalizeAndroidVersion
  },
  androidVersionText: {
    path: ['ds:4', 1, 2, 140, 1, 1, 0, 0, 1],
    fun: (version) => version || 'Varies with device'
  },
  developer: ['ds:4', 1, 2, 68, 0],
  developerId: {
    path: ['ds:4', 1, 2, 68, 1, 4, 2],
    fun: (devUrl) => devUrl.split('id=')[1]
  },
  developerEmail: ['ds:4', 1, 2, 69, 1, 0],
  developerWebsite: ['ds:4', 1, 2, 69, 0, 5, 2],
  developerAddress: ['ds:4', 1, 2, 69, 2, 0],
  privacyPolicy: ['ds:4', 1, 2, 99, 0, 5, 2],
  developerInternalID: {
    path: ['ds:4', 1, 2, 68, 1, 4, 2],
    fun: (devUrl) => devUrl.split('id=')[1]
  },
  genre: ['ds:4', 1, 2, 79, 0, 0, 0],
  genreId: ['ds:4', 1, 2, 79, 0, 0, 2],
  familyGenre: ['ds:5', 0, 12, 13, 1, 0],
  familyGenreId: ['ds:5', 0, 12, 13, 1, 2],
  icon: ['ds:4', 1, 2, 95, 0, 3, 2],
  headerImage: ['ds:4', 1, 2, 96, 0, 3, 2],
  screenshots: {
    path: ['ds:4', 1, 2, 78, 0],
    fun: (screenshots) => {
      if (screenshots === null) return [];
      return screenshots.map(R.path([3, 2]));
    }
  },
  video: ['ds:4', 1, 2, 100, 0, 0, 3, 2],
  videoImage: ['ds:4', 1, 2, 100, 1, 0, 3, 2],
  contentRating: ['ds:4', 1, 2, 9, 0],
  contentRatingDescription: ['ds:4', 1, 2, 9, 2, 1],
  adSupported: {
    path: ['ds:4', 1, 2, 48],
    fun: Boolean
  },
  released: ['ds:4', 1, 2, 10, 0],
  updated: {
    path: ['ds:4', 1, 2, 145, 0, 1, 0],
    fun: (ts) => ts * 1000
  },
  version: {
    path: ['ds:4', 1, 2, 140, 0, 0, 0],
    fun: (val) => val || 'VARY'
  },
  recentChanges: ['ds:4', 1, 2, 144, 1, 1],
  comments: {
    path: ['ds:8', 0],
    isArray: true,
    fun: helper.extractComments
  }
};

module.exports = app;
<commit_msg>Fix mapping issue #582 (#583)<commit_after>'use strict';

const R = require('ramda');
const queryString = require('querystring');
const request = require('./utils/request');
const scriptData = require('./utils/scriptData');
const { BASE_URL } = require('./constants');
const helper = require('./utils/mappingHelpers');

const PLAYSTORE_URL = `${BASE_URL}/store/apps/details`;

function app (opts) {
  return new Promise(function (resolve, reject) {
    if (!opts || !opts.appId) {
      throw Error('appId missing');
    }

    opts.lang = opts.lang || 'en';
    opts.country = opts.country || 'us';

    const qs = queryString.stringify({
      id: opts.appId,
      hl: opts.lang,
      gl: opts.country
    });
    const reqUrl = `${PLAYSTORE_URL}?${qs}`;

    const options = Object.assign({
      url: reqUrl,
      followRedirect: true
    }, opts.requestOptions);

    request(options, opts.throttle)
      .then(scriptData.parse)
    // comment next line to get raw data
      .then(scriptData.extractor(MAPPINGS))
      .then(R.assoc('appId', opts.appId))
      .then(R.assoc('url', reqUrl))
      .then(resolve)
      .catch(reject);
  });
}

const MAPPINGS = {
  title: ['ds:5', 1, 2, 0, 0],
  description: {
    path: ['ds:5', 1, 2, 72, 0, 1],
    fun: helper.descriptionText
  },
  descriptionHTML: ['ds:5', 1, 2, 72, 0, 1],
  summary: ['ds:5', 1, 2, 73, 0, 1],
  installs: ['ds:5', 1, 2, 13, 0],
  minInstalls: ['ds:5', 1, 2, 13, 1],
  maxInstalls: ['ds:5', 1, 2, 13, 2],
  score: ['ds:5', 1, 2, 51, 0, 1],
  scoreText: ['ds:5', 1, 2, 51, 0, 0],
  ratings: ['ds:5', 1, 2, 51, 2, 1],
  reviews: ['ds:5', 1, 2, 51, 3, 1],
  histogram: {
    path: ['ds:5', 1, 2, 51, 1],
    fun: helper.buildHistogram
  },
  price: {
    path: ['ds:5', 1, 2, 57, 0, 0, 0, 0, 1, 0, 0],
    fun: (val) => val / 1000000 || 0
  },
  free: {
    path: ['ds:5', 1, 2, 57, 0, 0, 0, 0, 1, 0, 0],
    // considered free only if price is exactly zero
    fun: (val) => val === 0
  },
  currency: ['ds:5', 1, 2, 57, 0, 0, 0, 0, 1, 0, 1],
  priceText: {
    path: ['ds:5', 1, 2, 57, 0, 0, 0, 0, 1, 0, 2],
    fun: helper.priceText
  },
  available: {
    path: ['ds:5', 1, 2, 18, 0],
    fun: Boolean
  },
  offersIAP: {
    path: ['ds:5', 1, 2, 19, 0],
    fun: Boolean
  },
  IAPRange: ['ds:5', 1, 2, 19, 0],
  androidVersion: {
    path: ['ds:5', 1, 2, 140, 1, 1, 0, 0, 1],
    fun: helper.normalizeAndroidVersion
  },
  androidVersionText: {
    path: ['ds:5', 1, 2, 140, 1, 1, 0, 0, 1],
    fun: (version) => version || 'Varies with device'
  },
  developer: ['ds:5', 1, 2, 68, 0],
  developerId: {
    path: ['ds:5', 1, 2, 68, 1, 4, 2],
    fun: (devUrl) => devUrl.split('id=')[1]
  },
  developerEmail: ['ds:5', 1, 2, 69, 1, 0],
  developerWebsite: ['ds:5', 1, 2, 69, 0, 5, 2],
  developerAddress: ['ds:5', 1, 2, 69, 2, 0],
  privacyPolicy: ['ds:5', 1, 2, 99, 0, 5, 2],
  developerInternalID: {
    path: ['ds:5', 1, 2, 68, 1, 4, 2],
    fun: (devUrl) => devUrl.split('id=')[1]
  },
  genre: ['ds:5', 1, 2, 79, 0, 0, 0],
  genreId: ['ds:5', 1, 2, 79, 0, 0, 2],
  familyGenre: ['ds:5', 0, 12, 13, 1, 0],
  familyGenreId: ['ds:5', 0, 12, 13, 1, 2],
  icon: ['ds:5', 1, 2, 95, 0, 3, 2],
  headerImage: ['ds:5', 1, 2, 96, 0, 3, 2],
  screenshots: {
    path: ['ds:5', 1, 2, 78, 0],
    fun: (screenshots) => {
      if (screenshots === null) return [];
      return screenshots.map(R.path([3, 2]));
    }
  },
  video: ['ds:5', 1, 2, 100, 0, 0, 3, 2],
  videoImage: ['ds:5', 1, 2, 100, 1, 0, 3, 2],
  contentRating: ['ds:5', 1, 2, 9, 0],
  contentRatingDescription: ['ds:5', 1, 2, 9, 2, 1],
  adSupported: {
    path: ['ds:5', 1, 2, 48],
    fun: Boolean
  },
  released: ['ds:5', 1, 2, 10, 0],
  updated: {
    path: ['ds:5', 1, 2, 145, 0, 1, 0],
    fun: (ts) => ts * 1000
  },
  version: {
    path: ['ds:5', 1, 2, 140, 0, 0, 0],
    fun: (val) => val || 'VARY'
  },
  recentChanges: ['ds:5', 1, 2, 144, 1, 1],
  comments: {
    path: ['ds:9', 0],
    isArray: true,
    fun: helper.extractComments
  }
};

module.exports = app;
<|endoftext|>


<commit_before>getVersion = function(){
  return 'v1.0.3';
}<commit_msg>Bump version: 1.0.4<commit_after>getVersion = function(){
  return 'v1.0.4';
}<|endoftext|>


<commit_before>import EventEmitter from 'events';

import { urlObjectToString } from '../../../react/features/base/util/uri';
import {
    PostMessageTransportBackend,
    Transport
} from '../../transport';

const logger = require('jitsi-meet-logger').getLogger(__filename);

const ALWAYS_ON_TOP_FILENAMES = [
    'css/all.css', 'libs/alwaysontop.min.js'
];

/**
 * Maps the names of the commands expected by the API with the name of the
 * commands expected by jitsi-meet
 */
const commands = {
    avatarUrl: 'avatar-url',
    displayName: 'display-name',
    email: 'email',
    hangup: 'video-hangup',
    toggleAudio: 'toggle-audio',
    toggleChat: 'toggle-chat',
    toggleContactList: 'toggle-contact-list',
    toggleFilmStrip: 'toggle-film-strip',
    toggleShareScreen: 'toggle-share-screen',
    toggleVideo: 'toggle-video'
};

/**
 * Maps the names of the events expected by the API with the name of the
 * events expected by jitsi-meet
 */
const events = {
    'audio-availability-changed': 'audioAvailabilityChanged',
    'audio-mute-status-changed': 'audioMuteStatusChanged',
    'display-name-change': 'displayNameChange',
    'incoming-message': 'incomingMessage',
    'outgoing-message': 'outgoingMessage',
    'participant-joined': 'participantJoined',
    'participant-left': 'participantLeft',
    'video-ready-to-close': 'readyToClose',
    'video-conference-joined': 'videoConferenceJoined',
    'video-conference-left': 'videoConferenceLeft',
    'video-availability-changed': 'videoAvailabilityChanged',
    'video-mute-status-changed': 'videoMuteStatusChanged'
};

/**
 * Last id of api object
 * @type {number}
 */
let id = 0;

/**
 * Adds given number to the numberOfParticipants property of given APIInstance.
 *
 * @param {JitsiMeetExternalAPI} APIInstance - The instance of the API.
 * @param {int} number - The number of participants to be added to
 * numberOfParticipants property (this parameter can be negative number if the
 * numberOfParticipants should be decreased).
 * @returns {void}
 */
function changeParticipantNumber(APIInstance, number) {
    APIInstance._numberOfParticipants += number;
}

/**
 * Generates the URL for the iframe.
 *
 * @param {string} domain - The domain name of the server that hosts the
 * conference.
 * @param {string} [options] - Another optional parameters.
 * @param {Object} [options.configOverwrite] - Object containing configuration
 * options defined in config.js to be overridden.
 * @param {Object} [options.interfaceConfigOverwrite] - Object containing
 * configuration options defined in interface_config.js to be overridden.
 * @param {string} [options.jwt] - The JWT token if needed by jitsi-meet for
 * authentication.
 * @param {boolean} [options.noSSL] - If the value is true https won't be used.
 * @param {string} [options.roomName] - The name of the room to join.
 * @returns {string} The URL.
 */
function generateURL(domain, options = {}) {
    return urlObjectToString({
        ...options,
        url:
            `${options.noSSL ? 'http' : 'https'}://${domain
                }/#jitsi_meet_external_api_id=${id}`
    });
}

/**
 * Parses the arguments passed to the constructor. If the old format is used
 * the function translates the arguments to the new format.
 *
 * @param {Array} args - The arguments to be parsed.
 * @returns {Object} JS object with properties.
 */
function parseArguments(args) {
    if (!args.length) {
        return {};
    }

    const firstArg = args[0];

    switch (typeof firstArg) {
    case 'string': // old arguments format
    case undefined: // eslint-disable-line no-case-declarations
    // not sure which format but we are trying to parse the old
    // format because if the new format is used everything will be undefined
    // anyway.
        const [
            roomName,
            width,
            height,
            parentNode,
            configOverwrite,
            interfaceConfigOverwrite,
            noSSL,
            jwt
        ] = args;

        return {
            roomName,
            width,
            height,
            parentNode,
            configOverwrite,
            interfaceConfigOverwrite,
            noSSL,
            jwt
        };
    case 'object': // new arguments format
        return args[0];
    default:
        throw new Error('Can\'t parse the arguments!');
    }
}

/**
 * Compute valid values for height and width. If a number is specified it's
 * treated as pixel units. If the value is expressed in px, em, pt or
 * percentage, it's used as is.
 *
 * @param {any} value - The value to be parsed.
 * @returns {string|undefined} The parsed value that can be used for setting
 * sizes through the style property. If invalid value is passed the method
 * retuns undefined.
 */
function parseSizeParam(value) {
    let parsedValue;

    // This regex parses values of the form 100px, 100em, 100pt or 100%.
    // Values like 100 or 100px are handled outside of the regex, and
    // invalid values will be ignored and the minimum will be used.
    const re = /([0-9]*\.?[0-9]+)(em|pt|px|%)$/;

    if (typeof value === 'string' && String(value).match(re) !== null) {
        parsedValue = value;
    } else if (typeof value === 'number') {
        parsedValue = `${value}px`;
    }

    return parsedValue;
}

/**
 * The IFrame API interface class.
 */
export default class JitsiMeetExternalAPI extends EventEmitter {
    /**
     * Constructs new API instance. Creates iframe and loads Jitsi Meet in it.
     *
     * @param {string} domain - The domain name of the server that hosts the
     * conference.
     * @param {Object} [options] - Optional arguments.
     * @param {string} [options.roomName] - The name of the room to join.
     * @param {number|string} [options.width] - Width of the iframe. Check
     * parseSizeParam for format details.
     * @param {number|string} [options.height] - Height of the iframe. Check
     * parseSizeParam for format details.
     * @param {DOMElement} [options.parentNode] - The node that will contain the
     * iframe.
     * @param {Object} [options.configOverwrite] - Object containing
     * configuration options defined in config.js to be overridden.
     * @param {Object} [options.interfaceConfigOverwrite] - Object containing
     * configuration options defined in interface_config.js to be overridden.
     * @param {boolean} [options.noSSL] - If the value is true https won't be
     * used.
     * @param {string} [options.jwt] - The JWT token if needed by jitsi-meet for
     * authentication.
     */
    constructor(domain, ...args) {
        super();
        const {
            roomName = '',
            width = '100%',
            height = '100%',
            parentNode = document.body,
            configOverwrite = {},
            interfaceConfigOverwrite = {},
            noSSL = false,
            jwt = undefined
        } = parseArguments(args);

        this._parentNode = parentNode;
        this._url = generateURL(domain, {
            configOverwrite,
            interfaceConfigOverwrite,
            jwt,
            noSSL,
            roomName
        });
        this._baseUrl = `${noSSL ? 'http' : 'https'}://${domain}/`;
        this._createIFrame(height, width);
        this._transport = new Transport({
            backend: new PostMessageTransportBackend({
                postisOptions: {
                    scope: `jitsi_meet_external_api_${id}`,
                    window: this._frame.contentWindow
                }
            })
        });
        this._numberOfParticipants = 1;
        this._setupListeners();
        id++;
    }

    /**
     * Creates the iframe element.
     *
     * @param {number|string} height - The height of the iframe. Check
     * parseSizeParam for format details.
     * @param {number|string} width - The with of the iframe. Check
     * parseSizeParam for format details.
     * @returns {void}
     *
     * @private
     */
    _createIFrame(height, width) {
        const frameName = `jitsiConferenceFrame${id}`;

        this._frame = document.createElement('iframe');
        this._frame.src = this._url;
        this._frame.name = frameName;
        this._frame.id = frameName;
        this._setSize(height, width);
        this._frame.setAttribute('allowFullScreen', 'true');
        this._frame.style.border = 0;
        this._frame = this._parentNode.appendChild(this._frame);
    }

    /**
     * Returns arrays with the all resources for the always on top feature.
     *
     * @returns {Array<string>}
     */
    _getAlwaysOnTopResources() {
        return ALWAYS_ON_TOP_FILENAMES.map(
            filename => this._baseUrl + filename
        );
    }

    /**
     * Sets the size of the iframe element.
     *
     * @param {number|string} height - The height of the iframe.
     * @param {number|string} width - The with of the iframe.
     * @returns {void}
     *
     * @private
     */
    _setSize(height, width) {
        const parsedHeight = parseSizeParam(height);
        const parsedWidth = parseSizeParam(width);

        if (parsedHeight !== undefined) {
            this._frame.style.height = parsedHeight;
        }

        if (parsedWidth !== undefined) {
            this._frame.style.width = parsedWidth;
        }
    }

    /**
     * Setups listeners that are used internally for JitsiMeetExternalAPI.
     *
     * @returns {void}
     *
     * @private
     */
    _setupListeners() {

        this._transport.on('event', ({ name, ...data }) => {
            if (name === 'participant-joined') {
                changeParticipantNumber(this, 1);
            } else if (name === 'participant-left') {
                changeParticipantNumber(this, -1);
            }

            const eventName = events[name];

            if (eventName) {
                this.emit(eventName, data);

                return true;
            }

            return false;
        });
    }

    /**
     * Adds event listener to Meet Jitsi.
     *
     * @param {string} event - The name of the event.
     * @param {Function} listener - The listener.
     * @returns {void}
     *
     * @deprecated
     * NOTE: This method is not removed for backward comatability purposes.
     */
    addEventListener(event, listener) {
        this.on(event, listener);
    }

    /**
     * Adds event listeners to Meet Jitsi.
     *
     * @param {Object} listeners - The object key should be the name of
     * the event and value - the listener.
     * Currently we support the following
     * events:
     * incomingMessage - receives event notifications about incoming
     * messages. The listener will receive object with the following structure:
     * {{
     *  'from': from,//JID of the user that sent the message
     *  'nick': nick,//the nickname of the user that sent the message
     *  'message': txt//the text of the message
     * }}
     * outgoingMessage - receives event notifications about outgoing
     * messages. The listener will receive object with the following structure:
     * {{
     *  'message': txt//the text of the message
     * }}
     * displayNameChanged - receives event notifications about display name
     * change. The listener will receive object with the following structure:
     * {{
     * jid: jid,//the JID of the participant that changed his display name
     * displayname: displayName //the new display name
     * }}
     * participantJoined - receives event notifications about new participant.
     * The listener will receive object with the following structure:
     * {{
     * jid: jid //the jid of the participant
     * }}
     * participantLeft - receives event notifications about the participant that
     * left the room.
     * The listener will receive object with the following structure:
     * {{
     * jid: jid //the jid of the participant
     * }}
     * video-conference-joined - receives event notifications about the local
     * user has successfully joined the video conference.
     * The listener will receive object with the following structure:
     * {{
     * roomName: room //the room name of the conference
     * }}
     * video-conference-left - receives event notifications about the local user
     * has left the video conference.
     * The listener will receive object with the following structure:
     * {{
     * roomName: room //the room name of the conference
     * }}
     * readyToClose - all hangup operations are completed and Jitsi Meet is
     * ready to be disposed.
     * @returns {void}
     *
     * @deprecated
     * NOTE: This method is not removed for backward comatability purposes.
     */
    addEventListeners(listeners) {
        for (const event in listeners) { // eslint-disable-line guard-for-in
            this.addEventListener(event, listeners[event]);
        }
    }

    /**
     * Removes the listeners and removes the Jitsi Meet frame.
     *
     * @returns {void}
     */
    dispose() {
        this._transport.dispose();
        this.removeAllListeners();
        if (this._frame) {
            this._frame.parentNode.removeChild(this._frame);
        }
    }

    /**
     * Executes command. The available commands are:
     * displayName - sets the display name of the local participant to the value
     * passed in the arguments array.
     * toggleAudio - mutes / unmutes audio with no arguments.
     * toggleVideo - mutes / unmutes video with no arguments.
     * toggleFilmStrip - hides / shows the filmstrip with no arguments.
     * If the command doesn't require any arguments the parameter should be set
     * to empty array or it may be omitted.
     *
     * @param {string} name - The name of the command.
     * @returns {void}
     */
    executeCommand(name, ...args) {
        if (!(name in commands)) {
            logger.error('Not supported command name.');

            return;
        }
        this._transport.sendEvent({
            data: args,
            name: commands[name]
        });
    }

    /**
     * Executes commands. The available commands are:
     * displayName - sets the display name of the local participant to the value
     * passed in the arguments array.
     * toggleAudio - mutes / unmutes audio. no arguments
     * toggleVideo - mutes / unmutes video. no arguments
     * toggleFilmStrip - hides / shows the filmstrip. no arguments
     * toggleChat - hides / shows chat. no arguments.
     * toggleContactList - hides / shows contact list. no arguments.
     * toggleShareScreen - starts / stops screen sharing. no arguments.
     *
     * @param {Object} commandList - The object with commands to be executed.
     * The keys of the object are the commands that will be executed and the
     * values are the arguments for the command.
     * @returns {void}
     */
    executeCommands(commandList) {
        for (const key in commandList) { // eslint-disable-line guard-for-in
            this.executeCommand(key, commandList[key]);
        }
    }

    /**
     * Check if the audio is available.
     *
     * @returns {Promise} - Resolves with true if the audio available, with
     * false if not and rejects on failure.
     */
    isAudioAvailable() {
        return this._transport.sendRequest({
            name: 'is-audio-available'
        });
    }

    /**
     * Returns the audio mute status.
     *
     * @returns {Promise} - Resolves with the audio mute status and rejects on
     * failure.
     */
    isAudioMuted() {
        return this._transport.sendRequest({
            name: 'is-audio-muted'
        });
    }

    /**
     * Returns the iframe that loads Jitsi Meet.
     *
     * @returns {HTMLElement} The iframe.
     */
    getIFrame() {
        return this._frame;
    }

    /**
     * Returns the number of participants in the conference. The local
     * participant is included.
     *
     * @returns {int} The number of participants in the conference.
     */
    getNumberOfParticipants() {
        return this._numberOfParticipants;
    }

    /**
     * Check if the video is available.
     *
     * @returns {Promise} - Resolves with true if the video available, with
     * false if not and rejects on failure.
     */
    isVideoAvailable() {
        return this._transport.sendRequest({
            name: 'is-video-available'
        });
    }

    /**
     * Returns the audio mute status.
     *
     * @returns {Promise} - Resolves with the audio mute status and rejects on
     * failure.
     */
    isVideoMuted() {
        return this._transport.sendRequest({
            name: 'is-video-muted'
        });
    }

    /**
     * Removes event listener.
     *
     * @param {string} event - The name of the event.
     * @returns {void}
     *
     * @deprecated
     * NOTE: This method is not removed for backward comatability purposes.
     */
    removeEventListener(event) {
        this.removeAllListeners(event);
    }

    /**
     * Removes event listeners.
     *
     * @param {Array<string>} eventList - Array with the names of the events.
     * @returns {void}
     *
     * @deprecated
     * NOTE: This method is not removed for backward comatability purposes.
     */
    removeEventListeners(eventList) {
        eventList.forEach(event => this.removeEventListener(event));
    }
}
<commit_msg>feat(external_api): set permissions for cross-origin iframe<commit_after>import EventEmitter from 'events';

import { urlObjectToString } from '../../../react/features/base/util/uri';
import {
    PostMessageTransportBackend,
    Transport
} from '../../transport';

const logger = require('jitsi-meet-logger').getLogger(__filename);

const ALWAYS_ON_TOP_FILENAMES = [
    'css/all.css', 'libs/alwaysontop.min.js'
];

/**
 * Maps the names of the commands expected by the API with the name of the
 * commands expected by jitsi-meet
 */
const commands = {
    avatarUrl: 'avatar-url',
    displayName: 'display-name',
    email: 'email',
    hangup: 'video-hangup',
    toggleAudio: 'toggle-audio',
    toggleChat: 'toggle-chat',
    toggleContactList: 'toggle-contact-list',
    toggleFilmStrip: 'toggle-film-strip',
    toggleShareScreen: 'toggle-share-screen',
    toggleVideo: 'toggle-video'
};

/**
 * Maps the names of the events expected by the API with the name of the
 * events expected by jitsi-meet
 */
const events = {
    'audio-availability-changed': 'audioAvailabilityChanged',
    'audio-mute-status-changed': 'audioMuteStatusChanged',
    'display-name-change': 'displayNameChange',
    'incoming-message': 'incomingMessage',
    'outgoing-message': 'outgoingMessage',
    'participant-joined': 'participantJoined',
    'participant-left': 'participantLeft',
    'video-ready-to-close': 'readyToClose',
    'video-conference-joined': 'videoConferenceJoined',
    'video-conference-left': 'videoConferenceLeft',
    'video-availability-changed': 'videoAvailabilityChanged',
    'video-mute-status-changed': 'videoMuteStatusChanged'
};

/**
 * Last id of api object
 * @type {number}
 */
let id = 0;

/**
 * Adds given number to the numberOfParticipants property of given APIInstance.
 *
 * @param {JitsiMeetExternalAPI} APIInstance - The instance of the API.
 * @param {int} number - The number of participants to be added to
 * numberOfParticipants property (this parameter can be negative number if the
 * numberOfParticipants should be decreased).
 * @returns {void}
 */
function changeParticipantNumber(APIInstance, number) {
    APIInstance._numberOfParticipants += number;
}

/**
 * Generates the URL for the iframe.
 *
 * @param {string} domain - The domain name of the server that hosts the
 * conference.
 * @param {string} [options] - Another optional parameters.
 * @param {Object} [options.configOverwrite] - Object containing configuration
 * options defined in config.js to be overridden.
 * @param {Object} [options.interfaceConfigOverwrite] - Object containing
 * configuration options defined in interface_config.js to be overridden.
 * @param {string} [options.jwt] - The JWT token if needed by jitsi-meet for
 * authentication.
 * @param {boolean} [options.noSSL] - If the value is true https won't be used.
 * @param {string} [options.roomName] - The name of the room to join.
 * @returns {string} The URL.
 */
function generateURL(domain, options = {}) {
    return urlObjectToString({
        ...options,
        url:
            `${options.noSSL ? 'http' : 'https'}://${domain
                }/#jitsi_meet_external_api_id=${id}`
    });
}

/**
 * Parses the arguments passed to the constructor. If the old format is used
 * the function translates the arguments to the new format.
 *
 * @param {Array} args - The arguments to be parsed.
 * @returns {Object} JS object with properties.
 */
function parseArguments(args) {
    if (!args.length) {
        return {};
    }

    const firstArg = args[0];

    switch (typeof firstArg) {
    case 'string': // old arguments format
    case undefined: // eslint-disable-line no-case-declarations
    // not sure which format but we are trying to parse the old
    // format because if the new format is used everything will be undefined
    // anyway.
        const [
            roomName,
            width,
            height,
            parentNode,
            configOverwrite,
            interfaceConfigOverwrite,
            noSSL,
            jwt
        ] = args;

        return {
            roomName,
            width,
            height,
            parentNode,
            configOverwrite,
            interfaceConfigOverwrite,
            noSSL,
            jwt
        };
    case 'object': // new arguments format
        return args[0];
    default:
        throw new Error('Can\'t parse the arguments!');
    }
}

/**
 * Compute valid values for height and width. If a number is specified it's
 * treated as pixel units. If the value is expressed in px, em, pt or
 * percentage, it's used as is.
 *
 * @param {any} value - The value to be parsed.
 * @returns {string|undefined} The parsed value that can be used for setting
 * sizes through the style property. If invalid value is passed the method
 * retuns undefined.
 */
function parseSizeParam(value) {
    let parsedValue;

    // This regex parses values of the form 100px, 100em, 100pt or 100%.
    // Values like 100 or 100px are handled outside of the regex, and
    // invalid values will be ignored and the minimum will be used.
    const re = /([0-9]*\.?[0-9]+)(em|pt|px|%)$/;

    if (typeof value === 'string' && String(value).match(re) !== null) {
        parsedValue = value;
    } else if (typeof value === 'number') {
        parsedValue = `${value}px`;
    }

    return parsedValue;
}

/**
 * The IFrame API interface class.
 */
export default class JitsiMeetExternalAPI extends EventEmitter {
    /**
     * Constructs new API instance. Creates iframe and loads Jitsi Meet in it.
     *
     * @param {string} domain - The domain name of the server that hosts the
     * conference.
     * @param {Object} [options] - Optional arguments.
     * @param {string} [options.roomName] - The name of the room to join.
     * @param {number|string} [options.width] - Width of the iframe. Check
     * parseSizeParam for format details.
     * @param {number|string} [options.height] - Height of the iframe. Check
     * parseSizeParam for format details.
     * @param {DOMElement} [options.parentNode] - The node that will contain the
     * iframe.
     * @param {Object} [options.configOverwrite] - Object containing
     * configuration options defined in config.js to be overridden.
     * @param {Object} [options.interfaceConfigOverwrite] - Object containing
     * configuration options defined in interface_config.js to be overridden.
     * @param {boolean} [options.noSSL] - If the value is true https won't be
     * used.
     * @param {string} [options.jwt] - The JWT token if needed by jitsi-meet for
     * authentication.
     */
    constructor(domain, ...args) {
        super();
        const {
            roomName = '',
            width = '100%',
            height = '100%',
            parentNode = document.body,
            configOverwrite = {},
            interfaceConfigOverwrite = {},
            noSSL = false,
            jwt = undefined
        } = parseArguments(args);

        this._parentNode = parentNode;
        this._url = generateURL(domain, {
            configOverwrite,
            interfaceConfigOverwrite,
            jwt,
            noSSL,
            roomName
        });
        this._baseUrl = `${noSSL ? 'http' : 'https'}://${domain}/`;
        this._createIFrame(height, width);
        this._transport = new Transport({
            backend: new PostMessageTransportBackend({
                postisOptions: {
                    scope: `jitsi_meet_external_api_${id}`,
                    window: this._frame.contentWindow
                }
            })
        });
        this._numberOfParticipants = 1;
        this._setupListeners();
        id++;
    }

    /**
     * Creates the iframe element.
     *
     * @param {number|string} height - The height of the iframe. Check
     * parseSizeParam for format details.
     * @param {number|string} width - The with of the iframe. Check
     * parseSizeParam for format details.
     * @returns {void}
     *
     * @private
     */
    _createIFrame(height, width) {
        const frameName = `jitsiConferenceFrame${id}`;

        this._frame = document.createElement('iframe');
        this._frame.allow = 'camera; microphone';
        this._frame.src = this._url;
        this._frame.name = frameName;
        this._frame.id = frameName;
        this._setSize(height, width);
        this._frame.setAttribute('allowFullScreen', 'true');
        this._frame.style.border = 0;
        this._frame = this._parentNode.appendChild(this._frame);
    }

    /**
     * Returns arrays with the all resources for the always on top feature.
     *
     * @returns {Array<string>}
     */
    _getAlwaysOnTopResources() {
        return ALWAYS_ON_TOP_FILENAMES.map(
            filename => this._baseUrl + filename
        );
    }

    /**
     * Sets the size of the iframe element.
     *
     * @param {number|string} height - The height of the iframe.
     * @param {number|string} width - The with of the iframe.
     * @returns {void}
     *
     * @private
     */
    _setSize(height, width) {
        const parsedHeight = parseSizeParam(height);
        const parsedWidth = parseSizeParam(width);

        if (parsedHeight !== undefined) {
            this._frame.style.height = parsedHeight;
        }

        if (parsedWidth !== undefined) {
            this._frame.style.width = parsedWidth;
        }
    }

    /**
     * Setups listeners that are used internally for JitsiMeetExternalAPI.
     *
     * @returns {void}
     *
     * @private
     */
    _setupListeners() {

        this._transport.on('event', ({ name, ...data }) => {
            if (name === 'participant-joined') {
                changeParticipantNumber(this, 1);
            } else if (name === 'participant-left') {
                changeParticipantNumber(this, -1);
            }

            const eventName = events[name];

            if (eventName) {
                this.emit(eventName, data);

                return true;
            }

            return false;
        });
    }

    /**
     * Adds event listener to Meet Jitsi.
     *
     * @param {string} event - The name of the event.
     * @param {Function} listener - The listener.
     * @returns {void}
     *
     * @deprecated
     * NOTE: This method is not removed for backward comatability purposes.
     */
    addEventListener(event, listener) {
        this.on(event, listener);
    }

    /**
     * Adds event listeners to Meet Jitsi.
     *
     * @param {Object} listeners - The object key should be the name of
     * the event and value - the listener.
     * Currently we support the following
     * events:
     * incomingMessage - receives event notifications about incoming
     * messages. The listener will receive object with the following structure:
     * {{
     *  'from': from,//JID of the user that sent the message
     *  'nick': nick,//the nickname of the user that sent the message
     *  'message': txt//the text of the message
     * }}
     * outgoingMessage - receives event notifications about outgoing
     * messages. The listener will receive object with the following structure:
     * {{
     *  'message': txt//the text of the message
     * }}
     * displayNameChanged - receives event notifications about display name
     * change. The listener will receive object with the following structure:
     * {{
     * jid: jid,//the JID of the participant that changed his display name
     * displayname: displayName //the new display name
     * }}
     * participantJoined - receives event notifications about new participant.
     * The listener will receive object with the following structure:
     * {{
     * jid: jid //the jid of the participant
     * }}
     * participantLeft - receives event notifications about the participant that
     * left the room.
     * The listener will receive object with the following structure:
     * {{
     * jid: jid //the jid of the participant
     * }}
     * video-conference-joined - receives event notifications about the local
     * user has successfully joined the video conference.
     * The listener will receive object with the following structure:
     * {{
     * roomName: room //the room name of the conference
     * }}
     * video-conference-left - receives event notifications about the local user
     * has left the video conference.
     * The listener will receive object with the following structure:
     * {{
     * roomName: room //the room name of the conference
     * }}
     * readyToClose - all hangup operations are completed and Jitsi Meet is
     * ready to be disposed.
     * @returns {void}
     *
     * @deprecated
     * NOTE: This method is not removed for backward comatability purposes.
     */
    addEventListeners(listeners) {
        for (const event in listeners) { // eslint-disable-line guard-for-in
            this.addEventListener(event, listeners[event]);
        }
    }

    /**
     * Removes the listeners and removes the Jitsi Meet frame.
     *
     * @returns {void}
     */
    dispose() {
        this._transport.dispose();
        this.removeAllListeners();
        if (this._frame) {
            this._frame.parentNode.removeChild(this._frame);
        }
    }

    /**
     * Executes command. The available commands are:
     * displayName - sets the display name of the local participant to the value
     * passed in the arguments array.
     * toggleAudio - mutes / unmutes audio with no arguments.
     * toggleVideo - mutes / unmutes video with no arguments.
     * toggleFilmStrip - hides / shows the filmstrip with no arguments.
     * If the command doesn't require any arguments the parameter should be set
     * to empty array or it may be omitted.
     *
     * @param {string} name - The name of the command.
     * @returns {void}
     */
    executeCommand(name, ...args) {
        if (!(name in commands)) {
            logger.error('Not supported command name.');

            return;
        }
        this._transport.sendEvent({
            data: args,
            name: commands[name]
        });
    }

    /**
     * Executes commands. The available commands are:
     * displayName - sets the display name of the local participant to the value
     * passed in the arguments array.
     * toggleAudio - mutes / unmutes audio. no arguments
     * toggleVideo - mutes / unmutes video. no arguments
     * toggleFilmStrip - hides / shows the filmstrip. no arguments
     * toggleChat - hides / shows chat. no arguments.
     * toggleContactList - hides / shows contact list. no arguments.
     * toggleShareScreen - starts / stops screen sharing. no arguments.
     *
     * @param {Object} commandList - The object with commands to be executed.
     * The keys of the object are the commands that will be executed and the
     * values are the arguments for the command.
     * @returns {void}
     */
    executeCommands(commandList) {
        for (const key in commandList) { // eslint-disable-line guard-for-in
            this.executeCommand(key, commandList[key]);
        }
    }

    /**
     * Check if the audio is available.
     *
     * @returns {Promise} - Resolves with true if the audio available, with
     * false if not and rejects on failure.
     */
    isAudioAvailable() {
        return this._transport.sendRequest({
            name: 'is-audio-available'
        });
    }

    /**
     * Returns the audio mute status.
     *
     * @returns {Promise} - Resolves with the audio mute status and rejects on
     * failure.
     */
    isAudioMuted() {
        return this._transport.sendRequest({
            name: 'is-audio-muted'
        });
    }

    /**
     * Returns the iframe that loads Jitsi Meet.
     *
     * @returns {HTMLElement} The iframe.
     */
    getIFrame() {
        return this._frame;
    }

    /**
     * Returns the number of participants in the conference. The local
     * participant is included.
     *
     * @returns {int} The number of participants in the conference.
     */
    getNumberOfParticipants() {
        return this._numberOfParticipants;
    }

    /**
     * Check if the video is available.
     *
     * @returns {Promise} - Resolves with true if the video available, with
     * false if not and rejects on failure.
     */
    isVideoAvailable() {
        return this._transport.sendRequest({
            name: 'is-video-available'
        });
    }

    /**
     * Returns the audio mute status.
     *
     * @returns {Promise} - Resolves with the audio mute status and rejects on
     * failure.
     */
    isVideoMuted() {
        return this._transport.sendRequest({
            name: 'is-video-muted'
        });
    }

    /**
     * Removes event listener.
     *
     * @param {string} event - The name of the event.
     * @returns {void}
     *
     * @deprecated
     * NOTE: This method is not removed for backward comatability purposes.
     */
    removeEventListener(event) {
        this.removeAllListeners(event);
    }

    /**
     * Removes event listeners.
     *
     * @param {Array<string>} eventList - Array with the names of the events.
     * @returns {void}
     *
     * @deprecated
     * NOTE: This method is not removed for backward comatability purposes.
     */
    removeEventListeners(eventList) {
        eventList.forEach(event => this.removeEventListener(event));
    }
}
<|endoftext|>


<commit_before>const express = require('express');
const ws = require('ws');
const Rule = require('./rule.js');
const logger = require('./logger.js');
const Acme = require('./acme');
const http = require('http');
const https = require('https');
const config = require('rc')('lroxy', {
  email: null,
  commonName: null,
  httpPort: 8080,
  httpsPort: null,
  rules: [],
});

const app = express();
const rules = [];
for (const rule of config.rules) {
  rules.push(new Rule(rule));
}
const names = {};
for (const rule of rules) {
  names[rule.getFromHost()] = true;
}
delete names[config.commonName];

const acme = new Acme(config.email, config.commonName, Object.keys(names));

app.use((req, res, next) => {
  next();
  logger.info('Handled request.', {
    remoteAddress: req.socket.remoteAddress,
    method: req.method,
    referer: req.headers.referer,
    userAgent: req.headers['user-agent'],
    url: `${req.secure ? 'https' : 'http'}://${req.hostname}${req.url}`,
    statusCode: res.statusCode,
    proxyTo: res.proxyToAddress,
  });
});

app.get('/.well-known/acme-challenge/:token', (req, res) => {
  const challenge = acme.getChallenge(req.params.token);
  res.write(challenge);
  res.end();
});

app.use((req, res) => {
  if (config.httpsPort != null && !req.secure) {
    return res.redirect('https://' + req.headers.host + req.url);
  }

  for (const rule of rules) {
    if (rule.match(req)) {
      rule.handle(req, res);
      return;
    }
  }

  // By default, reply service unavaliable if there is no matching reverse
  // proxy rule.
  res.writeHead(503, 'Service Unavaliable');
  res.end('Service Unavaliable');
});

module.exports = async function() {
  let server = http.createServer(app);
  let secure = false;
  server.listen(config.httpPort);
  logger.info(`lroxy (http) started on ${config.httpPort}.`);

  if (config.httpsPort != null) {
    secure= true;
    const sslInfo = await acme.run();
    server = https.createServer({
      key: sslInfo.key,
      cert: sslInfo.cert,
    }, app);
    server.listen(config.httpsPort);
    logger.info(`lroxy (https) started on ${config.httpsPort}.`);
  }

  const wsServer = new ws.Server({server});
  wsServer.on('connection', (socket, req) => {
    for (const rule of rules) {
      if (rule.match(req)) {
        rule.handleWs(socket, req);
        logger.info('Handled websocket request.', {
          remoteAddress: req.socket.remoteAddress,
          method: req.method,
          userAgent: req.headers['user-agent'],
          url: `${secure ? 'https' : 'http'}://${req.headers['host']}${req.url}`,
          proxyTo: rule.getToAddress(),
        });
        return;
      }
    }
    socket.close(1011, 'Service Unavaliable');
  });
};
<commit_msg>Kill the application if there is unhandled exception.<commit_after>const express = require('express');
const ws = require('ws');
const Rule = require('./rule.js');
const logger = require('./logger.js');
const Acme = require('./acme');
const http = require('http');
const https = require('https');
const config = require('rc')('lroxy', {
  email: null,
  commonName: null,
  httpPort: 8080,
  httpsPort: null,
  rules: [],
});

const app = express();
const rules = [];
for (const rule of config.rules) {
  rules.push(new Rule(rule));
}
const names = {};
for (const rule of rules) {
  names[rule.getFromHost()] = true;
}
delete names[config.commonName];

const acme = new Acme(config.email, config.commonName, Object.keys(names));

app.use((req, res, next) => {
  next();
  logger.info('Handled request.', {
    remoteAddress: req.socket.remoteAddress,
    method: req.method,
    referer: req.headers.referer,
    userAgent: req.headers['user-agent'],
    url: `${req.secure ? 'https' : 'http'}://${req.hostname}${req.url}`,
    statusCode: res.statusCode,
    proxyTo: res.proxyToAddress,
  });
});

app.get('/.well-known/acme-challenge/:token', (req, res) => {
  const challenge = acme.getChallenge(req.params.token);
  res.write(challenge);
  res.end();
});

app.use((req, res) => {
  if (config.httpsPort != null && !req.secure) {
    return res.redirect('https://' + req.headers.host + req.url);
  }

  for (const rule of rules) {
    if (rule.match(req)) {
      rule.handle(req, res);
      return;
    }
  }

  // By default, reply service unavaliable if there is no matching reverse
  // proxy rule.
  res.writeHead(503, 'Service Unavaliable');
  res.end('Service Unavaliable');
});

module.exports = async function() {
  try {
    let server = http.createServer(app);
    let secure = false;
    server.listen(config.httpPort);
    logger.info(`lroxy (http) started on ${config.httpPort}.`);

    if (config.httpsPort != null) {
      secure= true;
      const sslInfo = await acme.run();
      server = https.createServer({
        key: sslInfo.key,
        cert: sslInfo.cert,
      }, app);
      server.listen(config.httpsPort);
      logger.info(`lroxy (https) started on ${config.httpsPort}.`);
    }

    const wsServer = new ws.Server({server});
    wsServer.on('connection', (socket, req) => {
      for (const rule of rules) {
        if (rule.match(req)) {
          rule.handleWs(socket, req);
          logger.info('Handled websocket request.', {
            remoteAddress: req.socket.remoteAddress,
            method: req.method,
            userAgent: req.headers['user-agent'],
            url: `${secure ? 'https' : 'http'}://${req.headers['host']}${req.url}`,
            proxyTo: rule.getToAddress(),
          });
          return;
        }
      }
      socket.close(1011, 'Service Unavaliable');
    });
  } catch (e) {
    logger.error(e);
    process.exit(1);
  }
};
<|endoftext|>


<commit_before>/**
 * Adds dependency watching to the core fs.watchFile implementation.
 */
var _ = require('underscore'),
    fs = require('fs');

var watchedFiles = {};

function notifyWatch(filename, type, sourceChange, trigger) {
  var watchInfo = watchedFiles[filename];
  if (watchInfo) {
    var inQueue = _.find(watchInfo.queue, function(entry) {
      return entry.type === type
          && entry.filename === filename
          && entry.sourceChange === sourceChange;
    });

    if (!inQueue) {
      var entry = {type: type, filename: filename, sourceChange: sourceChange};
      watchInfo.queue.push(entry);

      function exec() {
        watchInfo.queue = _.without(watchInfo.queue, entry);

        if (watchInfo.callback) {
          watchInfo.callback(type, filename, sourceChange);
        }
        watchInfo.parents.forEach(function(parent) {
          notifyWatch(parent, type, sourceChange, trigger);
        });
      }

      if (trigger) {
        exec();
      } else {
        // Debounce so we don't output multiple instances of the same event on platforms
        // such as linux that may send multiple events on write, etc.
        _.defer(exec, 200);
      }
    }
  }
}

function watchFile(filename, callback, parent) {
    var watchInfo = {
      callback: callback,
      parents: [],
      queue: []
    };
    if (parent) {
      watchInfo.parents.push(parent);
    }
    watchedFiles[filename.virtual || filename] = watchInfo;

    if (!filename.virtual) {
      var hasRetried;

      (function watch(ignoreError) {
        try {
          var oldStat = fs.statSync(filename),
              lastType,
              rewatch;

          var changeHandler = _.debounce(function() {
            if (rewatch) {
              // Attempt to reattach on rename
              watchInfo.watch.close();
              watch(true);
            }
            notifyWatch(filename, lastType, filename);
          }, 1000);

          watchInfo.watch = fs.watch(filename, function(type) {
            try {
              var newStat = fs.statSync(filename);
              if (newStat.isDirectory()) {
                notifyWatch(filename, 'create', filename);
              } else if (newStat.size !== oldStat.size || newStat.mtime.getTime() > oldStat.mtime.getTime()) {
                oldStat = newStat;
                if (type === 'rename') {
                  rewatch = true;
                }
                lastType = type;
                changeHandler();
              }
            } catch (err) {
              if (err.code === 'ENOENT') {
                // The file was removed by the time we got to it. This could be a case of it actually being removed
                // or a race condtion with rewriting APIs.
                watchInfo.watch.close();

                // Pause a bit to see if this was a replace that we raced with...
                setTimeout(function() {
                  try {
                    fs.statSync(filename);    // No exception: file still exists, notify and restart the watch
                    notifyWatch(filename, 'change', filename);
                    watch(true);
                  } catch (err) {
                    // The file is really gone... or we just got hit with a race condtion twice. Give up.
                    notifyWatch(filename, 'remove', filename);
                  }
                }, 500);
              } else {
                throw err;
              }
            }
          });
        } catch (err) {
          if (!hasRetried && err.code === 'EMFILE') {
            hasRetried = true;
            setTimeout(function() {
              watch(ignoreError);
            }, 250);
          } else if (!ignoreError) {
            throw err;
          }
        }
      })();
    }
}

exports.watchFile = function(filename, dependencies, callback) {
  var watch = watchedFiles[filename.virtual || filename];
  if (!watch) {
    // Create a watch on this and all others
    watchFile(filename, callback);
  } else {
    watch.callback = callback;
  }

  filename = filename.virtual || filename;

  dependencies.forEach(function(depend) {
    var watch = watchedFiles[depend.virtual || depend];
    if (!watch) {
      watchFile(depend, undefined, filename);
    } else {
      if (!_.contains(watch.parents, filename)) {
        watch.parents.push(filename);
      }
    }
  });
};

exports.trigger = function(type, filename) {
  notifyWatch(filename, type, filename, true);
};

exports.unwatch = function(filename, dependencies) {
  var watch = watchedFiles[filename.virtual || filename];
  if (!watch) {
    return;
  }

  // Remove the callback
  if (!dependencies) {
    watch.callback = undefined;
  }

  // For each dependency remove the parent link
  filename = filename.virtual || filename;

  _.each(dependencies || watchedFiles, function(depend) {
    var watch = watchedFiles[depend.virtual || depend];
    if (!watch) {
      return;
    }

    watch.parents = _.without(watch.parents, filename);
  });

  // Kill this watch if it can't trigger or fire
  var canTrigger = watch.watch || _.some(watchedFiles, function(watch) {
    return _.contains(watch.parents, filename);
  });
  if (!watch.callback || !canTrigger) {
    unwatch(filename);
  }

  // Kill any other watches that might not be valid anymore
  _.each(_.clone(watchedFiles), function(watch, name) {
    if (!watch.callback && !watch.parents.length) {
      exports.unwatch(name);
    }
  });
};
exports.unwatchAll = function() {
  _.each(watchedFiles, unwatch);
};

function unwatch(name) {
  if (!watchedFiles[name]) {
    return;
  }

  watchedFiles[name].callback = undefined;
  if (watchedFiles[name].watch) {
    watchedFiles[name].watch.close();
  }
  delete watchedFiles[name];
}
<commit_msg>Fix unwatchAll implementation<commit_after>/**
 * Adds dependency watching to the core fs.watchFile implementation.
 */
var _ = require('underscore'),
    fs = require('fs');

var watchedFiles = {};

function notifyWatch(filename, type, sourceChange, trigger) {
  var watchInfo = watchedFiles[filename];
  if (watchInfo) {
    var inQueue = _.find(watchInfo.queue, function(entry) {
      return entry.type === type
          && entry.filename === filename
          && entry.sourceChange === sourceChange;
    });

    if (!inQueue) {
      var entry = {type: type, filename: filename, sourceChange: sourceChange};
      watchInfo.queue.push(entry);

      function exec() {
        watchInfo.queue = _.without(watchInfo.queue, entry);

        if (watchInfo.callback) {
          watchInfo.callback(type, filename, sourceChange);
        }
        watchInfo.parents.forEach(function(parent) {
          notifyWatch(parent, type, sourceChange, trigger);
        });
      }

      if (trigger) {
        exec();
      } else {
        // Debounce so we don't output multiple instances of the same event on platforms
        // such as linux that may send multiple events on write, etc.
        _.defer(exec, 200);
      }
    }
  }
}

function watchFile(filename, callback, parent) {
    var watchInfo = {
      callback: callback,
      parents: [],
      queue: []
    };
    if (parent) {
      watchInfo.parents.push(parent);
    }
    watchedFiles[filename.virtual || filename] = watchInfo;

    if (!filename.virtual) {
      var hasRetried;

      (function watch(ignoreError) {
        try {
          var oldStat = fs.statSync(filename),
              lastType,
              rewatch;

          var changeHandler = _.debounce(function() {
            if (rewatch) {
              // Attempt to reattach on rename
              watchInfo.watch.close();
              watch(true);
            }
            notifyWatch(filename, lastType, filename);
          }, 1000);

          watchInfo.watch = fs.watch(filename, function(type) {
            try {
              var newStat = fs.statSync(filename);
              if (newStat.isDirectory()) {
                notifyWatch(filename, 'create', filename);
              } else if (newStat.size !== oldStat.size || newStat.mtime.getTime() > oldStat.mtime.getTime()) {
                oldStat = newStat;
                if (type === 'rename') {
                  rewatch = true;
                }
                lastType = type;
                changeHandler();
              }
            } catch (err) {
              if (err.code === 'ENOENT') {
                // The file was removed by the time we got to it. This could be a case of it actually being removed
                // or a race condtion with rewriting APIs.
                watchInfo.watch.close();

                // Pause a bit to see if this was a replace that we raced with...
                setTimeout(function() {
                  try {
                    fs.statSync(filename);    // No exception: file still exists, notify and restart the watch
                    notifyWatch(filename, 'change', filename);
                    watch(true);
                  } catch (err) {
                    // The file is really gone... or we just got hit with a race condtion twice. Give up.
                    notifyWatch(filename, 'remove', filename);
                  }
                }, 500);
              } else {
                throw err;
              }
            }
          });
        } catch (err) {
          if (!hasRetried && err.code === 'EMFILE') {
            hasRetried = true;
            setTimeout(function() {
              watch(ignoreError);
            }, 250);
          } else if (!ignoreError) {
            throw err;
          }
        }
      })();
    }
}

exports.watchFile = function(filename, dependencies, callback) {
  var watch = watchedFiles[filename.virtual || filename];
  if (!watch) {
    // Create a watch on this and all others
    watchFile(filename, callback);
  } else {
    watch.callback = callback;
  }

  filename = filename.virtual || filename;

  dependencies.forEach(function(depend) {
    var watch = watchedFiles[depend.virtual || depend];
    if (!watch) {
      watchFile(depend, undefined, filename);
    } else {
      if (!_.contains(watch.parents, filename)) {
        watch.parents.push(filename);
      }
    }
  });
};

exports.trigger = function(type, filename) {
  notifyWatch(filename, type, filename, true);
};

exports.unwatch = function(filename, dependencies) {
  var watch = watchedFiles[filename.virtual || filename];
  if (!watch) {
    return;
  }

  // Remove the callback
  if (!dependencies) {
    watch.callback = undefined;
  }

  // For each dependency remove the parent link
  filename = filename.virtual || filename;

  _.each(dependencies || watchedFiles, function(depend) {
    var watch = watchedFiles[depend.virtual || depend];
    if (!watch) {
      return;
    }

    watch.parents = _.without(watch.parents, filename);
  });

  // Kill this watch if it can't trigger or fire
  var canTrigger = watch.watch || _.some(watchedFiles, function(watch) {
    return _.contains(watch.parents, filename);
  });
  if (!watch.callback || !canTrigger) {
    unwatch(filename);
  }

  // Kill any other watches that might not be valid anymore
  _.each(_.clone(watchedFiles), function(watch, name) {
    if (!watch.callback && !watch.parents.length) {
      exports.unwatch(name);
    }
  });
};
exports.unwatchAll = function() {
  _.each(watchedFiles, function(watch, name) {
    unwatch(name);
  });
};

function unwatch(name) {
  if (!watchedFiles[name]) {
    return;
  }

  watchedFiles[name].callback = undefined;
  if (watchedFiles[name].watch) {
    watchedFiles[name].watch.close();
  }
  delete watchedFiles[name];
}
<|endoftext|>


<commit_before>var Promise = require('bluebird');
var request = require('request-promise');
var cheerio = require('cheerio');
var playStoreUrl = 'https://play.google.com/store/apps/details';
var queryString = require('querystring');
var url = require('url');

var h = require('./helpers');

function app(id, lang) {
    lang = lang || 'en';

    return new Promise(function (resolve, reject) {
        var reqUrl = url.parse(playStoreUrl + '?' + queryString.stringify({id: id, hl: lang}));
        request(reqUrl.href)
            .then(cheerio.load, h.requestError)
            .then(parseFields)
            .then(function (app) {
                app.url = reqUrl.href;
                app.appId = id;
                resolve(app);
            })
            .catch(reject);
    });
}

function parseFields($) {
    var detailsInfo = $('.details-info');
    var title = detailsInfo.find('div.document-title').text().trim();
    var developer = detailsInfo.find('span[itemprop="name"]').text();
    var genreField = detailsInfo.find('span[itemprop="genre"]');
    var genre = [];
    var genreFieldLength = genreField.length;
    if ( genreFieldLength > 1 ) {
        genreField.each(function (i, elem) {
           genre[i] = $(this).text();
        });
        for (var i = 0; i < genre.length; i++) {
            if ( genre[i].indexOf("&") > -1 ) {
                var splitted = genre[i].split(" & ");
                genre.splice(i, 1);
                genre = genre.concat(splitted);
            }
        }
        genre = genre.filter(function(element, pos) {
            return genre.indexOf(element) == pos;
        });
    } else {
        genre = detailsInfo.find('span[itemprop="genre"]').text();
    }
    var price = detailsInfo.find('meta[itemprop=price]').attr('content');
    var icon = detailsInfo.find('img.cover-image').attr('src');

    var additionalInfo = $('.details-section-contents');
    var version = additionalInfo.find('div.content[itemprop="softwareVersion"]').text().trim();
    var updated = additionalInfo.find('div.content[itemprop="datePublished"]').text().trim();
    var requiredAndroidVersion = additionalInfo.find('div.content[itemprop="operatingSystems"]').text().trim();
    var contentRating = additionalInfo.find('div.content[itemprop="contentRating"]').text().trim();
    var size = additionalInfo.find('div.content[itemprop="fileSize"]').text().trim();
    var installs = additionalInfo.find('div.content[itemprop="numDownloads"]').text().trim();
    var minInstalls = cleanInt(installs.split(' - ')[0]);
    var maxInstalls = cleanInt(installs.split(' - ')[1]) || undefined;
    var comments = [];
    $('.quoted-review').each(function(i, elem){
      comments[i] = $(this).text();
    });
    var ratingBox = $('.rating-box');
    var reviews = cleanInt(ratingBox.find('span.reviews-num').text());

    var ratingHistogram = $('.rating-histogram');
    var histogram = {
        5: cleanInt(ratingHistogram.find('.five .bar-number').text()),
        4: cleanInt(ratingHistogram.find('.four .bar-number').text()),
        3: cleanInt(ratingHistogram.find('.three .bar-number').text()),
        2: cleanInt(ratingHistogram.find('.two .bar-number').text()),
        1: cleanInt(ratingHistogram.find('.one .bar-number').text())
    };
    //for other languages
    var score = parseFloat(ratingBox.find('div.score').text().replace(',', '.')) || 0;

    var video = $('.screenshots span.preview-overlay-container[data-video-url]').attr('data-video-url');
    if (video) {
        video = video.split('?')[0];
    }

    var description = $('.description div.id-app-orig-desc');

    var screens = $('.thumbnails').find('img').nextAll();
    var screenshots = [];
    for(var i = 0; i < screens.length; i++) {
        screenshots.push(screens[i].attribs.src);
    }
    return {
        title: title,
        icon: icon,
        minInstalls: minInstalls,
        maxInstalls: maxInstalls,
        score: score,
        reviews: reviews,
        histogram: histogram,
        description: description.text(),
        descriptionHTML: description.html(),
        developer: developer,
        updated: updated,
        version: version,
        size: size,
        requiredAndroidVersion: requiredAndroidVersion,
        contentRating: contentRating,
        genre: genre,
        price: price,
        free: price === '0',
        video: video,
        screenshots: screenshots
        comments : comments
    };
}

function cleanInt(number) {
    number = number || '0';
    //removes thousands separator
    number = number.replace(/\D/g, '');
    return parseInt(number);
}

module.exports = app;
<commit_msg>Getting screenshots and genres as an array for a game<commit_after>var Promise = require('bluebird');
var request = require('request-promise');
var cheerio = require('cheerio');
var playStoreUrl = 'https://play.google.com/store/apps/details';
var queryString = require('querystring');
var url = require('url');

var h = require('./helpers');

function app(id, lang) {
    lang = lang || 'en';

    return new Promise(function (resolve, reject) {
        var reqUrl = url.parse(playStoreUrl + '?' + queryString.stringify({id: id, hl: lang}));
        request(reqUrl.href)
            .then(cheerio.load, h.requestError)
            .then(parseFields)
            .then(function (app) {
                app.url = reqUrl.href;
                app.appId = id;
                resolve(app);
            })
            .catch(reject);
    });
}

function parseFields($) {
    var detailsInfo = $('.details-info');
    var title = detailsInfo.find('div.document-title').text().trim();
    var developer = detailsInfo.find('span[itemprop="name"]').text();
    var genre = [];
    detailsInfo.find('span[itemprop="genre"]').each(function(i, elem){
        genre[i] = $(this).text();
    });
    var price = detailsInfo.find('meta[itemprop=price]').attr('content');
    var icon = detailsInfo.find('img.cover-image').attr('src');

    var additionalInfo = $('.details-section-contents');
    var version = additionalInfo.find('div.content[itemprop="softwareVersion"]').text().trim();
    var updated = additionalInfo.find('div.content[itemprop="datePublished"]').text().trim();
    var requiredAndroidVersion = additionalInfo.find('div.content[itemprop="operatingSystems"]').text().trim();
    var contentRating = additionalInfo.find('div.content[itemprop="contentRating"]').text().trim();
    var size = additionalInfo.find('div.content[itemprop="fileSize"]').text().trim();
    var installs = additionalInfo.find('div.content[itemprop="numDownloads"]').text().trim();
    var minInstalls = cleanInt(installs.split(' - ')[0]);
    var maxInstalls = cleanInt(installs.split(' - ')[1]) || undefined;
    var comments = [];
    $('.quoted-review').each(function(i, elem){
      comments[i] = $(this).text();
    });
    var ratingBox = $('.rating-box');
    var reviews = cleanInt(ratingBox.find('span.reviews-num').text());

    var ratingHistogram = $('.rating-histogram');
    var histogram = {
        5: cleanInt(ratingHistogram.find('.five .bar-number').text()),
        4: cleanInt(ratingHistogram.find('.four .bar-number').text()),
        3: cleanInt(ratingHistogram.find('.three .bar-number').text()),
        2: cleanInt(ratingHistogram.find('.two .bar-number').text()),
        1: cleanInt(ratingHistogram.find('.one .bar-number').text())
    };
    //for other languages
    var score = parseFloat(ratingBox.find('div.score').text().replace(',', '.')) || 0;

    var video = $('.screenshots span.preview-overlay-container[data-video-url]').attr('data-video-url');
    if (video) {
        video = video.split('?')[0];
    }
    var description = $('.description div.id-app-orig-desc');

    var screenshots = [];
    $('.thumbnails').find('img').nextAll().each(function(i, elem){
        screenshots[i] = elem.attribs.src;
    });

    return {
        title: title,
        icon: icon,
        minInstalls: minInstalls,
        maxInstalls: maxInstalls,
        score: score,
        reviews: reviews,
        histogram: histogram,
        description: description.text(),
        descriptionHTML: description.html(),
        developer: developer,
        updated: updated,
        version: version,
        size: size,
        requiredAndroidVersion: requiredAndroidVersion,
        contentRating: contentRating,
        genre: genre,
        price: price,
        free: price === '0',
        video: video,
        screenshots: screenshots
        comments : comments
    };
}

function cleanInt(number) {
    number = number || '0';
    //removes thousands separator
    number = number.replace(/\D/g, '');
    return parseInt(number);
}

module.exports = app;
<|endoftext|>


<commit_before><commit_msg>moved caching off of the entire app and onto appropriate views<commit_after><|endoftext|>


<commit_before>var Slack = require('slack-client'),
  Kimbot = require('./kimbot'),
  Foursquare = require('./clients/foursquare'),
  OpenWeather = require('./clients/openweather');

var responses = [
 '안녕! It\'s a nice day! Why not head to the market? I recommend the ' +
   'Bibbimbap!',
 'สวัสดี! A busy day with lots of meetings, but there\'s always time for ' +
   'Thai!',
 'Chào bạn! Take a trip to Camden Town and sample the excellent ' +
   'Vietnamese food!',
 'Hola! Burritos all round!',
 'Let\'s try something new... what do you suggest?'
];

var foursquare = new Foursquare(process.env.FS_CLIENT_ID,
  process.env.FS_CLIENT_SECRET);

var weather = new OpenWeather(process.env.OW_APP_ID);

var slack = new Slack(process.env.SLACK_BOT_KEY, true, true);
var kimbot = new Kimbot(slack, responses, {
  foursquare: foursquare,
  weather: weather
});

slack.on('message', function (message) {
  // Inside AnonFn. to ensure not called with slacks 'this' scope.
  kimbot.respond(message);
});

slack.login();
<commit_msg>Stop kim responding to herself if there are multiple instances running.<commit_after>var Slack = require('slack-client'),
  Kimbot = require('./kimbot'),
  Foursquare = require('./clients/foursquare'),
  OpenWeather = require('./clients/openweather');

var responses = [
 '안녕! It\'s a nice day! Why not head to the market? I recommend the ' +
   'Bibbimbap!',
 'สวัสดี! A busy day with lots of meetings, but there\'s always time for ' +
   'Thai!',
 'Chào bạn! Take a trip to Camden Town and sample the excellent ' +
   'Vietnamese food!',
 'Hola! Burritos all round!',
 'Let\'s try something new... what do you suggest?'
];

var foursquare = new Foursquare(process.env.FS_CLIENT_ID,
  process.env.FS_CLIENT_SECRET);

var weather = new OpenWeather(process.env.OW_APP_ID);

var slack = new Slack(process.env.SLACK_BOT_KEY, true, true);
var kimbot = new Kimbot(slack, responses, {
  foursquare: foursquare,
  weather: weather
});

slack.on('message', function (message) {
  // Inside AnonFn. to ensure not called with slacks 'this' scope.
  if (message.user !== slack.self.id) {
    kimbot.respond(message);
  }
});

slack.login();
<|endoftext|>