<commit_before>// Miscellaneous utilities
/*jshint worker: true*/

import log from './log';
import Thread from './thread';
import WorkerBroker from './worker_broker';
import Geo from '../geo';

import yaml from 'js-yaml';

var Utils;
export default Utils = {};

WorkerBroker.addTarget('Utils', Utils);

// Basic Safari detection
// http://stackoverflow.com/questions/7944460/detect-safari-browser
Utils.isSafari = function () {
    return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
};

// Basic IE11 or Edge detection
Utils.isMicrosoft = function () {
    return /(Trident\/7.0|Edge[ /](\d+[\.\d]+))/i.test(navigator.userAgent);
};

Utils.io = function (url, timeout = 60000, responseType = 'text', method = 'GET', headers = {}) {
    if (Thread.is_worker && Utils.isMicrosoft()) {
        // Some versions of IE11 and Edge will hang web workers when performing XHR requests
        // These requests can be proxied through the main thread
        // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/9545866/
        log('debug', 'Proxying request for URL to worker', url);
        return WorkerBroker.postMessage('Utils.io', ...arguments);
    }
    else {
        var request = new XMLHttpRequest();
        var promise = new Promise((resolve, reject) => {
            request.open(method, url, true);
            request.timeout = timeout;
            request.responseType = responseType;
            request.onload = () => {
                if (request.status === 200) {
                    if (['text', 'json'].indexOf(request.responseType) > -1) {
                        resolve(request.responseText);
                    }
                    else {
                        resolve(request.response);
                    }
                } else {
                    reject(Error('Request error with a status of ' + request.statusText));
                }
            };
            request.onerror = (evt) => {
                reject(Error('There was a network error' + evt.toString()));
            };
            request.ontimeout = (evt) => {
                reject(Error('timeout '+ evt.toString()));
            };
            request.send();
        });

        Object.defineProperty(promise, 'request', {
            value: request
        });

        return promise;
    }
};

Utils.parseResource = function (body) {
    var data;
    try {
        // jsyaml 'json' option allows duplicate keys
        // Keeping this for backwards compatibility, but should consider migrating to requiring
        // unique keys, as this is YAML spec. But Tangram ES currently accepts dupe keys as well,
        // so should consider how best to unify.
        data = yaml.safeLoad(body, { json: true });
    } catch (e) {
        throw e;
    }
    return data;
};

Utils.loadResource = function (source) {
    return new Promise((resolve, reject) => {
        if (typeof source === 'string') {
            Utils.io(source).then((body) => {
                try {
                    let data = Utils.parseResource(body);
                    resolve(data);
                }
                catch(e) {
                    reject(e);
                }
            }, reject);
        } else {
            resolve(source);
        }
    });
};

// Needed for older browsers that still support WebGL (Safari 6 etc.)
Utils.requestAnimationFramePolyfill = function () {
    if (typeof window.requestAnimationFrame !== 'function') {
        window.requestAnimationFrame =
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame    ||
            window.oRequestAnimationFrame      ||
            window.msRequestAnimationFrame     ||
            function (cb) {
                setTimeout(cb, 1000 /60);
            };
    }
};

// Stringify an object into JSON, but convert functions to strings
Utils.serializeWithFunctions = function (obj) {
    var serialized = JSON.stringify(obj, function(k, v) {
        // Convert functions to strings
        if (typeof v === 'function') {
            return v.toString();
        }
        return v;
    });

    return serialized;
};

// Recursively parse an object, attempting to convert string properties that look like functions back into functions
Utils.stringsToFunctions = function(obj, wrap) {
    // Convert string
    if (typeof obj === 'string') {
        obj = Utils.stringToFunction(obj, wrap);
    }
    // Loop through object properties
    else if (obj != null && typeof obj === 'object') {
        for (let p in obj) {
            obj[p] = Utils.stringsToFunctions(obj[p], wrap);
        }
    }
    return obj;
};

// Convert string back into a function
Utils.stringToFunction = function(val, wrap) {
    // Parse function signature and body
    let fmatch =
        (typeof val === 'string') &&
        val.match(/^\s*function[^(]*\(([^)]*)\)\s*?\{([\s\S]*)\}$/m);

    if (fmatch && fmatch.length > 2) {
        try {
            let src = fmatch[2];
            let args = fmatch[1].length > 0 && fmatch[1].split(',').map(x => x.trim()).filter(x => x);
            args = args.length > 0 ? args : ['context']; // default to single 'context' argument

            if (typeof wrap === 'function') {
                return new Function(...args, wrap(src)); // jshint ignore:line
            }
            else {
                return new Function(...args, src); // jshint ignore:line
            }
        }
        catch (e) {
            // fall-back to original value if parsing failed
            return val;
        }
    }
    return val;
};

// Default to allowing high pixel density
// Returns true if display density changed
Utils.use_high_density_display = true;
Utils.updateDevicePixelRatio = function () {
    let prev = Utils.device_pixel_ratio;
    Utils.device_pixel_ratio = (Utils.use_high_density_display && window.devicePixelRatio) || 1;
    return Utils.device_pixel_ratio !== prev;
};

if (Thread.is_main) {
    Utils.updateDevicePixelRatio();
}

// Used for differentiating between power-of-2 and non-power-of-2 textures
// Via: http://stackoverflow.com/questions/19722247/webgl-wait-for-texture-to-load
Utils.isPowerOf2 = function(value) {
    return (value & (value - 1)) === 0;
};

// Interpolate 'x' along a series of control points
// 'points' is an array of control points in the form [x, y]
//
// Example:
//     Control points:
//         [0, 5]:  when x=0, y=5
//         [4, 10]: when x=4, y=10
//
//     Utils.interpolate(2, [[0, 5], [4, 10]]);
//     -> computes x=2, halfway between x=0 and x=4: (10 - 5) / 2 +5
//     -> returns 7.5
//
// TODO: add other interpolation methods besides linear
//
Utils.interpolate = function(x, points, transform) {
    // If this doesn't resemble a list of control points, just return the original value
    if (!Array.isArray(points) || !Array.isArray(points[0])) {
        return points;
    }
    else if (points.length < 1) {
        return points;
    }

    var x1, x2, d, y, y1, y2;

    // Min bounds
    if (x <= points[0][0]) {
        y = points[0][1];
        if (typeof transform === 'function') {
            y = transform(y);
        }
    }
    // Max bounds
    else if (x >= points[points.length-1][0]) {
        y = points[points.length-1][1];
        if (typeof transform === 'function') {
            y = transform(y);
        }
    }
    // Find which control points x is between
    else {
        for (var i=0; i < points.length - 1; i++) {
            if (x >= points[i][0] && x < points[i+1][0]) {
                // Linear interpolation
                x1 = points[i][0];
                x2 = points[i+1][0];

                // Multiple values
                if (Array.isArray(points[i][1])) {
                    y = [];
                    for (var c=0; c < points[i][1].length; c++) {
                        if (typeof transform === 'function') {
                            y1 = transform(points[i][1][c]);
                            y2 = transform(points[i+1][1][c]);
                            d = y2 - y1;
                            y[c] = d * (x - x1) / (x2 - x1) + y1;
                        }
                        else {
                            d = points[i+1][1][c] - points[i][1][c];
                            y[c] = d * (x - x1) / (x2 - x1) + points[i][1][c];
                        }
                    }
                }
                // Single value
                else {
                    if (typeof transform === 'function') {
                        y1 = transform(points[i][1]);
                        y2 = transform(points[i+1][1]);
                        d = y2 - y1;
                        y = d * (x - x1) / (x2 - x1) + y1;
                    }
                    else {
                        d = points[i+1][1] - points[i][1];
                        y = d * (x - x1) / (x2 - x1) + points[i][1];
                    }
                }
                break;
            }
        }
    }
    return y;
};

Utils.toCSSColor = function (color) {
    if (color[3] === 1) { // full opacity
        return `rgb(${color.slice(0, 3).map(c => Math.round(c * 255)).join(', ')})`;
    }
    // RGB is between [0, 255] opacity is between [0, 1]
    return `rgba(${color.map((c, i) => (i < 3 && Math.round(c * 255)) || c).join(', ')})`;
};

Utils.pointInTile = function (point) {
    return point[0] >= 0 && point[1] > -Geo.tile_scale && point[0] < Geo.tile_scale && point[1] <= 0;
};
<commit_msg>use transferable objects when proxying binary XHR responses to workers<commit_after>// Miscellaneous utilities
/*jshint worker: true*/

import log from './log';
import Thread from './thread';
import WorkerBroker from './worker_broker';
import Geo from '../geo';

import yaml from 'js-yaml';

var Utils;
export default Utils = {};

WorkerBroker.addTarget('Utils', Utils);

// Basic Safari detection
// http://stackoverflow.com/questions/7944460/detect-safari-browser
Utils.isSafari = function () {
    return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
};

// Basic IE11 or Edge detection
Utils.isMicrosoft = function () {
    return /(Trident\/7.0|Edge[ /](\d+[\.\d]+))/i.test(navigator.userAgent);
};

Utils.io = function (url, timeout = 60000, responseType = 'text', method = 'GET', headers = {}, proxy = false) {
    if (Thread.is_worker && Utils.isMicrosoft()) {
        // Some versions of IE11 and Edge will hang web workers when performing XHR requests
        // These requests can be proxied through the main thread
        // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/9545866/
        log('debug', 'Proxying request for URL to worker', url);
        return WorkerBroker.postMessage('Utils.io', url, timeout, responseType, method, headers, true);
    }
    else {
        var request = new XMLHttpRequest();
        var promise = new Promise((resolve, reject) => {
            request.open(method, url, true);
            request.timeout = timeout;
            request.responseType = responseType;
            request.onload = () => {
                if (request.status === 200) {
                    if (['text', 'json'].indexOf(request.responseType) > -1) {
                        resolve(request.responseText);
                    }
                    else {
                        resolve(request.response);
                    }
                } else {
                    reject(Error('Request error with a status of ' + request.statusText));
                }
            };
            request.onerror = (evt) => {
                reject(Error('There was a network error' + evt.toString()));
            };
            request.ontimeout = (evt) => {
                reject(Error('timeout '+ evt.toString()));
            };
            request.send();
        });

        Object.defineProperty(promise, 'request', {
            value: request
        });

        return promise.then(response => {
            return (proxy && WorkerBroker.withTransferables(response)) || response;
        });
    }
};

Utils.parseResource = function (body) {
    var data;
    try {
        // jsyaml 'json' option allows duplicate keys
        // Keeping this for backwards compatibility, but should consider migrating to requiring
        // unique keys, as this is YAML spec. But Tangram ES currently accepts dupe keys as well,
        // so should consider how best to unify.
        data = yaml.safeLoad(body, { json: true });
    } catch (e) {
        throw e;
    }
    return data;
};

Utils.loadResource = function (source) {
    return new Promise((resolve, reject) => {
        if (typeof source === 'string') {
            Utils.io(source).then((body) => {
                try {
                    let data = Utils.parseResource(body);
                    resolve(data);
                }
                catch(e) {
                    reject(e);
                }
            }, reject);
        } else {
            resolve(source);
        }
    });
};

// Needed for older browsers that still support WebGL (Safari 6 etc.)
Utils.requestAnimationFramePolyfill = function () {
    if (typeof window.requestAnimationFrame !== 'function') {
        window.requestAnimationFrame =
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame    ||
            window.oRequestAnimationFrame      ||
            window.msRequestAnimationFrame     ||
            function (cb) {
                setTimeout(cb, 1000 /60);
            };
    }
};

// Stringify an object into JSON, but convert functions to strings
Utils.serializeWithFunctions = function (obj) {
    var serialized = JSON.stringify(obj, function(k, v) {
        // Convert functions to strings
        if (typeof v === 'function') {
            return v.toString();
        }
        return v;
    });

    return serialized;
};

// Recursively parse an object, attempting to convert string properties that look like functions back into functions
Utils.stringsToFunctions = function(obj, wrap) {
    // Convert string
    if (typeof obj === 'string') {
        obj = Utils.stringToFunction(obj, wrap);
    }
    // Loop through object properties
    else if (obj != null && typeof obj === 'object') {
        for (let p in obj) {
            obj[p] = Utils.stringsToFunctions(obj[p], wrap);
        }
    }
    return obj;
};

// Convert string back into a function
Utils.stringToFunction = function(val, wrap) {
    // Parse function signature and body
    let fmatch =
        (typeof val === 'string') &&
        val.match(/^\s*function[^(]*\(([^)]*)\)\s*?\{([\s\S]*)\}$/m);

    if (fmatch && fmatch.length > 2) {
        try {
            let src = fmatch[2];
            let args = fmatch[1].length > 0 && fmatch[1].split(',').map(x => x.trim()).filter(x => x);
            args = args.length > 0 ? args : ['context']; // default to single 'context' argument

            if (typeof wrap === 'function') {
                return new Function(...args, wrap(src)); // jshint ignore:line
            }
            else {
                return new Function(...args, src); // jshint ignore:line
            }
        }
        catch (e) {
            // fall-back to original value if parsing failed
            return val;
        }
    }
    return val;
};

// Default to allowing high pixel density
// Returns true if display density changed
Utils.use_high_density_display = true;
Utils.updateDevicePixelRatio = function () {
    let prev = Utils.device_pixel_ratio;
    Utils.device_pixel_ratio = (Utils.use_high_density_display && window.devicePixelRatio) || 1;
    return Utils.device_pixel_ratio !== prev;
};

if (Thread.is_main) {
    Utils.updateDevicePixelRatio();
}

// Used for differentiating between power-of-2 and non-power-of-2 textures
// Via: http://stackoverflow.com/questions/19722247/webgl-wait-for-texture-to-load
Utils.isPowerOf2 = function(value) {
    return (value & (value - 1)) === 0;
};

// Interpolate 'x' along a series of control points
// 'points' is an array of control points in the form [x, y]
//
// Example:
//     Control points:
//         [0, 5]:  when x=0, y=5
//         [4, 10]: when x=4, y=10
//
//     Utils.interpolate(2, [[0, 5], [4, 10]]);
//     -> computes x=2, halfway between x=0 and x=4: (10 - 5) / 2 +5
//     -> returns 7.5
//
// TODO: add other interpolation methods besides linear
//
Utils.interpolate = function(x, points, transform) {
    // If this doesn't resemble a list of control points, just return the original value
    if (!Array.isArray(points) || !Array.isArray(points[0])) {
        return points;
    }
    else if (points.length < 1) {
        return points;
    }

    var x1, x2, d, y, y1, y2;

    // Min bounds
    if (x <= points[0][0]) {
        y = points[0][1];
        if (typeof transform === 'function') {
            y = transform(y);
        }
    }
    // Max bounds
    else if (x >= points[points.length-1][0]) {
        y = points[points.length-1][1];
        if (typeof transform === 'function') {
            y = transform(y);
        }
    }
    // Find which control points x is between
    else {
        for (var i=0; i < points.length - 1; i++) {
            if (x >= points[i][0] && x < points[i+1][0]) {
                // Linear interpolation
                x1 = points[i][0];
                x2 = points[i+1][0];

                // Multiple values
                if (Array.isArray(points[i][1])) {
                    y = [];
                    for (var c=0; c < points[i][1].length; c++) {
                        if (typeof transform === 'function') {
                            y1 = transform(points[i][1][c]);
                            y2 = transform(points[i+1][1][c]);
                            d = y2 - y1;
                            y[c] = d * (x - x1) / (x2 - x1) + y1;
                        }
                        else {
                            d = points[i+1][1][c] - points[i][1][c];
                            y[c] = d * (x - x1) / (x2 - x1) + points[i][1][c];
                        }
                    }
                }
                // Single value
                else {
                    if (typeof transform === 'function') {
                        y1 = transform(points[i][1]);
                        y2 = transform(points[i+1][1]);
                        d = y2 - y1;
                        y = d * (x - x1) / (x2 - x1) + y1;
                    }
                    else {
                        d = points[i+1][1] - points[i][1];
                        y = d * (x - x1) / (x2 - x1) + points[i][1];
                    }
                }
                break;
            }
        }
    }
    return y;
};

Utils.toCSSColor = function (color) {
    if (color[3] === 1) { // full opacity
        return `rgb(${color.slice(0, 3).map(c => Math.round(c * 255)).join(', ')})`;
    }
    // RGB is between [0, 255] opacity is between [0, 1]
    return `rgba(${color.map((c, i) => (i < 3 && Math.round(c * 255)) || c).join(', ')})`;
};

Utils.pointInTile = function (point) {
    return point[0] >= 0 && point[1] > -Geo.tile_scale && point[0] < Geo.tile_scale && point[1] <= 0;
};
<|endoftext|>


<commit_before>const expect = require('chai').expect;
const RG = require('../../../client/src/battles');
const RGTest = require('../../roguetest');

import Entity from '../../../client/src/entity';

const worldConf = require('../../../client/data/conf.world');

describe('Function: Creating game world from a file', function() {
    this.timeout(60000);
    it('can create world from external config object', () => {
        const fact = new RG.Factory.World();
        const world = fact.createWorld(worldConf);
        expect(world.getAreas()).to.have.length(worldConf.nAreas);
        expect(world.getName()).to.equal(worldConf.name);
        expect(world.getLevels()).to.have.length.above(0);
    });

    it('Can create World using config object', function() {
        const worldConf = {
            name: 'w1',
            nAreas: 2,
            area: [
                { name: 'a1', maxX: 2, maxY: 3, nDungeons: 1,
                    cols: 80, rows: 28,
                    dungeon: [
                        {x: 0, y: 0, name: 'd1.1', nBranches: 1,
                            branch: [ { name: 'b1', nLevels: 2,
                                entranceLevel: 0}]
                        }
                    ]
                },
                { name: 'a2', maxX: 1, maxY: 3, nMountains: 1,
                    cols: 80, rows: 28,
                    mountain: [{x: 0, y: 1, name: 'm2.1', nFaces: 1,
                        face: [{name: 'Steep', nLevels: 1, x: 50, y: 100,
                        entranceLevel: 0}]
                    }]
                }
            ]
        };
        const fact = new RG.Factory.World();
        const world = fact.createWorld(worldConf);
        expect(world.getName()).to.equal('w1');
        expect(world.getAreas()).to.have.length(2);
        expect(world.getAreas()[0].getDungeons()).to.have.length(1);
        expect(world.getDungeons(), 'Found 1 dungeon').to.have.length(1);
        expect(world.getMountains(), 'Found 1 mountain').to.have.length(1);

        expect(world.getDungeons()[0].getName(),
            'Dungeon name OK.').to.equal('d1.1');
    });

    it('converts a game containing world into JSON and back', () => {
        const fromJSON = new RG.Game.FromJSON();
        const fact = new RG.Factory.World();
        const conf = {name: 'Ice Kingdom', nAreas: 1,
            area: [{ name: 'Area51', maxX: 2, maxY: 2,
                nDungeons: 1,
                dungeon: [
                    { x: 0, y: 0, name: 'Dungeon1', nBranches: 1,
                        branch: [
                            {name: 'Branch1', nLevels: 2, entranceLevel: 0}
                        ]
                    }
                ],
                nMountains: 1,
                mountain: [
                    {x: 0, y: 0, name: 'Cliff', nFaces: 1,
                        face: [{name: 'north', nLevels: 1, x: 20, y: 20,
                        entranceLevel: 0}]
                    }
                ],
                nCities: 1,
                city: [
                    {x: 0, y: 0, name: 'City1', nQuarters: 1,
                        quarter: [{name: 'Q1', nLevels: 1, entranceLevel: 0}]
                    }
                ]
            }]
        };
        const numLevels = 2 * 2 + 2 + 1 + 1;

        // Create game, world and player first
        const game = new RG.Game.Main();
        const world = fact.createWorld(conf);
        const player = new RG.Actor.Rogue('MyPlayer');
        player.setType('player');
        player.setIsPlayer(true);
        game.addPlace(world);
        game.addPlayer(player, {place: 'Ice Kingdom'});
        let gameLevels = game.getLevels();

        // Verify correct number of levels before serialisation
        expect(gameLevels).to.have.length(numLevels);

        // Serialise and check ID counters
        const json = game.toJSON();
        expect(json.lastLevelID).to.equal(RG.Map.Level.prototype.idCount);
        expect(json.lastEntityID).to.equal(Entity.getIDCount());

        console.log('Creating new game now');

        // Create new game from JSON, verify place + levels have been restored
        const newGame = fromJSON.createGame(json);
        expect(Object.keys(newGame.getPlaces())).to.have.length(1);
        gameLevels = newGame.getLevels();
        expect(gameLevels).to.have.length(numLevels);

        console.log('Level IDs: ' +
            JSON.stringify(gameLevels.map(l => l.getID())));

        // Verify that world zones have been restored
        const newWorld = newGame.getPlaces()['Ice Kingdom'];
        const dungeons = newWorld.getDungeons();
        expect(dungeons, 'World has 1 dungeon').to.have.length(1);

        expect(newWorld.getZones()).to.have.length(3);

        // Verify stairs connectivity
        const allStairsInWorld = newWorld.getStairs();
        RGTest.verifyStairsConnectivity(allStairsInWorld);

        // Detailed checks that dungeon and branches restored OK
        const d1 = dungeons[0];
        expect(d1.getName(), 'Dungeon name OK').to.equal('Dungeon1');
        expect(d1.getEntrances()).to.have.length(1);

        const b1 = d1.getBranches()[0];
        expect(b1.getName()).to.equal('Branch1');
        expect(b1.getEntrance()).not.to.be.empty;
        expect(b1.getDungeon()).not.to.be.empty;

        // Verify mountain restoration
        const mountains = newWorld.getMountains();
        expect(mountains).to.have.length(1);
        const m1 = mountains[0];
        expect(m1.getName()).to.equal('Cliff');

        const f1 = m1.getFaces()[0];
        expect(f1.getEntrance()).not.to.be.empty;

        // Verify that city is restored correctly
        const cities = newWorld.getCities();
        expect(cities, 'World has one city').to.have.length(1);
        const c1 = cities[0];
        expect(c1.getEntrances()[0]).to.not.be.empty;

    });
});
<commit_msg>Fixed alinting error.<commit_after>
import Entity from '../../../client/src/entity';

const expect = require('chai').expect;
const RG = require('../../../client/src/battles');
const RGTest = require('../../roguetest');

const worldConf = require('../../../client/data/conf.world');

describe('Function: Creating game world from a file', function() {
    this.timeout(60000);
    it('can create world from external config object', () => {
        const fact = new RG.Factory.World();
        const world = fact.createWorld(worldConf);
        expect(world.getAreas()).to.have.length(worldConf.nAreas);
        expect(world.getName()).to.equal(worldConf.name);
        expect(world.getLevels()).to.have.length.above(0);
    });

    it('Can create World using config object', function() {
        const worldConf = {
            name: 'w1',
            nAreas: 2,
            area: [
                { name: 'a1', maxX: 2, maxY: 3, nDungeons: 1,
                    cols: 80, rows: 28,
                    dungeon: [
                        {x: 0, y: 0, name: 'd1.1', nBranches: 1,
                            branch: [ { name: 'b1', nLevels: 2,
                                entranceLevel: 0}]
                        }
                    ]
                },
                { name: 'a2', maxX: 1, maxY: 3, nMountains: 1,
                    cols: 80, rows: 28,
                    mountain: [{x: 0, y: 1, name: 'm2.1', nFaces: 1,
                        face: [{name: 'Steep', nLevels: 1, x: 50, y: 100,
                        entranceLevel: 0}]
                    }]
                }
            ]
        };
        const fact = new RG.Factory.World();
        const world = fact.createWorld(worldConf);
        expect(world.getName()).to.equal('w1');
        expect(world.getAreas()).to.have.length(2);
        expect(world.getAreas()[0].getDungeons()).to.have.length(1);
        expect(world.getDungeons(), 'Found 1 dungeon').to.have.length(1);
        expect(world.getMountains(), 'Found 1 mountain').to.have.length(1);

        expect(world.getDungeons()[0].getName(),
            'Dungeon name OK.').to.equal('d1.1');
    });

    it('converts a game containing world into JSON and back', () => {
        const fromJSON = new RG.Game.FromJSON();
        const fact = new RG.Factory.World();
        const conf = {name: 'Ice Kingdom', nAreas: 1,
            area: [{ name: 'Area51', maxX: 2, maxY: 2,
                nDungeons: 1,
                dungeon: [
                    { x: 0, y: 0, name: 'Dungeon1', nBranches: 1,
                        branch: [
                            {name: 'Branch1', nLevels: 2, entranceLevel: 0}
                        ]
                    }
                ],
                nMountains: 1,
                mountain: [
                    {x: 0, y: 0, name: 'Cliff', nFaces: 1,
                        face: [{name: 'north', nLevels: 1, x: 20, y: 20,
                        entranceLevel: 0}]
                    }
                ],
                nCities: 1,
                city: [
                    {x: 0, y: 0, name: 'City1', nQuarters: 1,
                        quarter: [{name: 'Q1', nLevels: 1, entranceLevel: 0}]
                    }
                ]
            }]
        };
        const numLevels = 2 * 2 + 2 + 1 + 1;

        // Create game, world and player first
        const game = new RG.Game.Main();
        const world = fact.createWorld(conf);
        const player = new RG.Actor.Rogue('MyPlayer');
        player.setType('player');
        player.setIsPlayer(true);
        game.addPlace(world);
        game.addPlayer(player, {place: 'Ice Kingdom'});
        let gameLevels = game.getLevels();

        // Verify correct number of levels before serialisation
        expect(gameLevels).to.have.length(numLevels);

        // Serialise and check ID counters
        const json = game.toJSON();
        expect(json.lastLevelID).to.equal(RG.Map.Level.prototype.idCount);
        expect(json.lastEntityID).to.equal(Entity.getIDCount());

        console.log('Creating new game now');

        // Create new game from JSON, verify place + levels have been restored
        const newGame = fromJSON.createGame(json);
        expect(Object.keys(newGame.getPlaces())).to.have.length(1);
        gameLevels = newGame.getLevels();
        expect(gameLevels).to.have.length(numLevels);

        console.log('Level IDs: ' +
            JSON.stringify(gameLevels.map(l => l.getID())));

        // Verify that world zones have been restored
        const newWorld = newGame.getPlaces()['Ice Kingdom'];
        const dungeons = newWorld.getDungeons();
        expect(dungeons, 'World has 1 dungeon').to.have.length(1);

        expect(newWorld.getZones()).to.have.length(3);

        // Verify stairs connectivity
        const allStairsInWorld = newWorld.getStairs();
        RGTest.verifyStairsConnectivity(allStairsInWorld);

        // Detailed checks that dungeon and branches restored OK
        const d1 = dungeons[0];
        expect(d1.getName(), 'Dungeon name OK').to.equal('Dungeon1');
        expect(d1.getEntrances()).to.have.length(1);

        const b1 = d1.getBranches()[0];
        expect(b1.getName()).to.equal('Branch1');
        expect(b1.getEntrance()).not.to.be.empty;
        expect(b1.getDungeon()).not.to.be.empty;

        // Verify mountain restoration
        const mountains = newWorld.getMountains();
        expect(mountains).to.have.length(1);
        const m1 = mountains[0];
        expect(m1.getName()).to.equal('Cliff');

        const f1 = m1.getFaces()[0];
        expect(f1.getEntrance()).not.to.be.empty;

        // Verify that city is restored correctly
        const cities = newWorld.getCities();
        expect(cities, 'World has one city').to.have.length(1);
        const c1 = cities[0];
        expect(c1.getEntrances()[0]).to.not.be.empty;

    });
});
<|endoftext|>


<commit_before>'use strict';

var _ = require('lodash')
var validator = require('validator')
var validUrl = require('valid-url')

function isStringCheck(string){
  if(!_.isString(string)){
    throw new Error('requestDataValidation error: is not a string')
  }
}

function checkIfValidUrl(url){
  if(!validUrl.isWebUri(url)){
    throw new Error('requestDataValidation error: not a valid url')
  }
}

function requestDataValidation(req){
  /****
   * req.params.pageUrl is used in api.js for:
   *    getSinglePage on route /api/get/:pageUrl,
   *    addPage on route /api/add/:pageUrl,
   *    deletePage on route /api/remove/:pageUrl
   *
   * req.params.pageUrl is used in index.js for:
   *    scrapeAndAddPage on route /frontendapi/scrapeAndAdd/:pageUrl,
   *    deletePage on route /frontendapi/remove/:pageUrl,
   *    openUrlInBrowser on route /frontendapi/openUrlInBrowser/:pageUrl
   *
   */
  if(_.get(req, 'params.pageUrl')){
    isStringCheck(req.params.pageUrl)
    req.params.pageUrl = req.params.pageUrl.toLowerCase()
    checkIfValidUrl(req.params.pageUrl)
  }
  /****
   * search data from /frontendapi/search/:searchTerms route in index.js
   * & from /api/search/:searchTerms route in api.js
   * req.params.searchTerms
   * knex does sql injection checks for bound values so I think I don't have
   * to do anything more with req.params.searchTerms
   */
  if(_.get(req, 'params.searchTerms')){
    isStringCheck(req.params.searchTerms)
  }
  /****
   * the updateMarkSearchSettings validation is done in appSettings.js
   */
  /****
   * changePagesDBlocation & checkIfFileIsBinary use parse-filepath, so I think they're ok.
   */
  /****
   * emailBookmarklet data from /frontendapi/settings/emailBookmarklet/ route in api.js
   * req.body.email
   * We use the mailgun api in emailBookmarklet to validate the email, so here just check that
   * it's a string.
   */
  if(_.get(req, 'body.email')){
    isStringCheck(req.body.email)
  }
  /****
   * addPage data from /api/add/:pageUrl route in api.js
   * req.params.pageUrl - checked above
   * req.body.pageTitle
   * req.body.pageText
   * req.body.pageDescription
   * validator.escape() replaces <, >, &, ', " and / with HTML entities
   */
  if(_.get(req, 'body.pageTitle')){
    isStringCheck(req.body.pageTitle)
    req.body.pageTitle = validator.escape(req.body.pageTitle)
  }
  if(_.get(req, 'body.pageText')){
    isStringCheck(req.body.pageText)
    req.body.pageText = validator.escape(req.body.pageText)
  }
  if(_.get(req, 'body.pageDescription')){
    isStringCheck(req.body.pageDescription)
    req.body.pageDescription = validator.escape(req.body.pageDescription)
  }

  return req
}

module.exports = requestDataValidation<commit_msg>Finished data in validation<commit_after>'use strict';

var _ = require('lodash')
var validator = require('validator')
var validUrl = require('valid-url')
var inspector = require('schema-inspector')

/****
 * req.params.pageUrl is used in api.js for:
 *    getSinglePage on route /api/get/:pageUrl,
 *    addPage on route /api/add/:pageUrl,
 *    deletePage on route /api/remove/:pageUrl
 *
 * req.params.pageUrl is used in index.js for:
 *    scrapeAndAddPage on route /frontendapi/scrapeAndAdd/:pageUrl,
 *    deletePage on route /frontendapi/remove/:pageUrl,
 *    openUrlInBrowser on route /frontendapi/openUrlInBrowser/:pageUrl
 *
 * We lowercase the pageUrl just so an uppercase url isn't thought of
 * as a different url to a lowercase version of the same url.
 *
 * search data from /frontendapi/search/:searchTerms route in index.js
 * & from /api/search/:searchTerms route in api.js
 * req.params.searchTerms
 * knex does sql injection checks for bound values so I think I don't have
 * to do anything more with req.params.searchTerms
 *
 * the updateMarkSearchSettings validation is done in appSettings.js
 *
 * changePagesDBlocation & checkIfFileIsBinary use parse-filepath, so I think they're ok.
 *
 * emailBookmarklet data from /frontendapi/settings/emailBookmarklet/ route in api.js
 * req.body.email
 * We use the mailgun api in emailBookmarklet to validate the email, so here just check that
 * it's a string.
 *
 * addPage data from /api/add/:pageUrl route in api.js
 * req.params.pageUrl - checked above
 * req.body.pageTitle
 * req.body.pageText
 * req.body.pageDescription
 * validator.escape() replaces <, >, &, ', " and / with HTML entities
 */

var reqParamsSanitization = {
  type: 'object',
  properties: {
    pageUrl: {
      type: 'string',
      optional: true,
      rules: ['lower']
    }
  }
}

var reqParamsValidation = {
  type: 'object',
  properties: {
    pageUrl: {
      type: 'string',
      optional: true,
      pattern: 'lowerString',
      maxLength: 2000,
      exec: function(scheme, post){
        if(!validUrl.isWebUri(post)){
          this.report('req.params.pageUrl is not a valid web url')
        }
      }
    },
    searchTerms: {
      type: 'string',
      optional: true
    }
  }
}

var reqBodySanitization = {
  type: 'object',
  properties: {
    pageTitle: {
      type: 'string',
      optional: true,
      exec: (schema, post) => validator.escape(post)

    },
    pageText: {
      type: 'string',
      optional: true,
      exec: (schema, post) => validator.escape(post)
    },
    pageDescription: {
      type: 'string',
      optional: true,
      exec: (schema, post) => validator.escape(post)
    }
  }
}

var reqBodyValidation = {
  type: 'object',
  properties: {
    email: {
      type: 'string',
      optional: true
    },
    pageTitle: {
      type: 'string',
      optional: true
    },
    pageText: {
      type: 'string',
      optional: true
    },
    pageDescription: {
      type: 'string',
      optional: true
    }
  }
}

function requestDataValidation(req){

  inspector.sanitize(reqParamsSanitization, req.params)
  inspector.sanitize(reqBodySanitization, req.body)

  var validReqParams = inspector.validate(reqParamsValidation, req.params)
  if(!validReqParams.valid){
    let errMessage = `Error(s) with the req.params data in requestDataValidation : ${validReqParams.format()}`
    console.error(errMessage)
    throw new Error(errMessage)
  }

  var validReqBody = inspector.validate(reqBodyValidation, req.body)
  if(!validReqBody.valid){
    let errMessage = `Error(s) with the req.body data in requestDataValidation : ${validReqBody.format()}`
    console.error(errMessage)
    throw new Error(errMessage)
  }

  return req
}

module.exports = requestDataValidation<|endoftext|>


<commit_before>{%import "executePythonDisplayMacro.js" as display with context%}
{%import "commonExecuteCallback.js" as commons with context%}
var pixiedust = (function(){
    return {
        {#
            executeDisplay helper method: run a new display command
            displayCallback:{
                options: dictionary of new options to add to the display command
                onDisplayDone: callback function called when the display run is done executing
                targetDivId: id of div that will receive the output html, none means the default output
            }
        #}
        executeDisplay:function(pd_controls, user_controls){
            pd_controls = pd_controls || {};
            user_controls = user_controls || {"options":{}};
            var options = $.extend({}, pd_controls.options || {}, user_controls.options || {} );
            function onDisplayDone(){
                if (user_controls.onDisplayDone){
                    user_controls.onDisplayDone();
                }
            }
            var pd_prefix = pd_controls.prefix;
            var $targetDivId = user_controls.targetDivId;
            {%include "pd_executeDisplay.js"%}
        },
        executeInDialog:function(pd_controls, user_controls){
            pd_controls = pd_controls || {};
            user_controls = user_controls || {"options":{}};
            var displayOptions = $.extend({}, pd_controls.options || {}, user_controls.options || {} );
            var global={};
            require(['base/js/dialog'],function(dialog){
                var modal = dialog.modal;
                var attr_pd_ctrl = JSON.stringify(pd_controls).trim()
                    .replace(/&/g, '&')
                    .replace(/'/g, ''')
                    .replace(/"/g, '"')
                    .replace(/</g, '<')
                    .replace(/>/g, '>');
                var dialogRoot = "dialog" + pd_controls.prefix + "root";
                var options = {
                    title: "Pixiedust: " + (displayOptions.title || "Dialog"),
                    body: '

',
                    sanitize:false,
                    notebook: IPython.notebook,
                    keyboard_manager: IPython.notebook.keyboard_manager,
                    buttons: {
                        OK: {
                            class : "btn-primary btn-ok",
                            click: function() {
                                var dlg = $("#" + dialogRoot + " > pd_dialog");
                                try{
                                    return new Function('global', 'modal_obj', dlg.find("> pd_ok").text().trim())(global, modal_obj);
                                }catch(e){
                                    console.error(e);
                                    return false;
                                }
                            }
                        },
                        Cancel: {
                            class : "btn-cancel",
                            click: function(){
                            }
                        }
                    }
                };

                function resizeDialog() {
                    global.modalBodyStyle = $('.pixiedust .modal-body').attr('style');
                    global.modalFooterStyle = $('.pixiedust .modal-footer').attr('style');
                    $('.pixiedust .modal-body').attr('style', global.modalBodyStyle ? global.modalBodyStyle + ';padding:5px 20px !important;' : 'padding:5px 20px !important;');
                    $('.pixiedust .modal-footer').attr('style', 'display:none !important;');
                };

                function resetDialog() {
                    if (global.modalBodyStyle) {
                        $('.pixiedust .modal-body').attr('style', global.modalBodyStyle);
                    } else {
                        $('.pixiedust .modal-body').removeAttr('style');
                    }
                    if (global.modalFooterStyle) {
                        $('.pixiedust .modal-footer').attr('style', global.modalFooterStyle);
                    } else {
                        $('.pixiedust .modal-footer').removeAttr('style');
                    }
                };

                var modal_obj = modal(options);
                modal_obj.addClass('pixiedust pixiedust-app');
                modal_obj.on('shown.bs.modal', function(){
                    resizeDialog();
                    var isFF = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
                    if( isFF && options.keyboard_manager){
                        {#Only on FF, blur event issue, hard disable keyboard manager#}
                        var KeyboardManager = require('notebook/js/keyboardmanager').KeyboardManager;
                        global.KMEnableProto = KeyboardManager.prototype.enable;
                        KeyboardManager.prototype.enable = function () {
                            this.enabled = false;
                        };
                    }
                    IPython.keyboard_manager.register_events(modal_obj);
                    user_controls.options.targetDivId = user_controls.targetDivId = dialogRoot;
                    if ( user_controls.options.dialog == 'true'){
                        user_controls.onDisplayDone = function(){
                            var dlg = $("#" + dialogRoot + " > pd_dialog")
                            try{
                                new Function('global', 'modal_obj', dlg.find("> pd_onload").text().trim())(global, modal_obj);
                            }catch(e){
                                console.error(e);
                            }
                        }
                    }
                    pixiedust.dialogRoot = dialogRoot;
                    pixiedust.executeDisplay(pd_controls, user_controls);
                });
                modal_obj.on("hidden.bs.modal", function () {
                    resetDialog();
                    if ( global.KMEnableProto ){
                        var KeyboardManager = require('notebook/js/keyboardmanager').KeyboardManager;
                        KeyboardManager.prototype.enable = global.KMEnableProto;
                        delete global.KMEnableProto;
                    }
                    pixiedust.dialogRoot = null;
                });
            })
        }
    }
})();

function resolveScriptMacros(script){
    script = script.replace(/\$val\(\"?(\w*)\"?\)/g, function(a,b){
        var v = $("#" + b ).val();
        if (!v){
            console.log("Warning: Unable to resolve value for element ", b);
            return a;
        }
        return "\"" +v.split('"').join('"').split('\n').join('\\n') + "\"";
    });
    return script;
}

function getParentScript(element){
    var scripts = [];
    {#Get all parent scripts#}
    $(element).parents("[pd_script]").each(function(){
        scripts.unshift(this.getAttribute("pd_script"));
    });

    {#merge#}
    var script = "";
    $.each( scripts, function(index, value){
        if (value){
            script += "\n" + value;
        }
    });
    return script;
}

function preRun(element){
    var preRunCode = null;
    $(element).find("> pd_script").each(function(){
        var type = this.getAttribute("type");
        if (type=="preRun"){
            preRunCode = $(this).text();
        }
    });
    if (!preRunCode ){
        return true;
    }
    return new Function(preRunCode.trim())();
}

function addOptions(command, options, override=true){
    function getStringRep(v) {
        return "'" + v + "'";
    }
    for (var key in (options||{})){
        var value = options[key];
        var hasValue = value != null && typeof value !== 'undefined' && value !== '';
        var replaceValue = hasValue ? (key+"=" + getStringRep(value) ) : "";
        var pattern = (hasValue?"":",")+"\\s*" + key + "\\s*=\\s*'(\\\\'|[^'])*'";
        var rpattern=new RegExp(pattern);
        var n = command.search(rpattern);
        if ( n >= 0 ){
            if (override){
                command = command.replace(rpattern, replaceValue);
            }
        }else if (hasValue){
            var n = command.lastIndexOf(")");
            command = [command.slice(0, n), (command[n-1]=="("? "":",") + replaceValue, command.slice(n)].join('')
        }
    }
    return command;
}

function readExecInfo(pd_controls, element){
    var execInfo = {}
    execInfo.options = {}
    var hasOptions = false;
    $.each( element.attributes, function(){
        if (this.name.startsWith("option_")){
            hasOptions = true;
            execInfo.options[this.name.replace("option_", "")] = this.value || null;
        }
    });
    var pd_options = element.getAttribute("pd_options");
    if (pd_options){
        var parts = pd_options.split(";");
        $.each( parts, function(){
            var index = this.indexOf("=");
            if ( index > 1){
                hasOptions = true;
                execInfo.options[this.substring(0, index)] = this.substring(index+1);
            }
        });
    }
    execInfo.options.nostore_figureOnly = true;
    execInfo.options.targetDivId = execInfo.targetDivId = element.getAttribute("pd_target");
    if (execInfo.options.targetDivId){
        execInfo.options.no_margin=true;
    }

    execInfo.options.widget = element.getAttribute("pd_widget");

    // unhide parents temporarily to properly calculate width/height
    var parentStyles = [];
    var hiddenBlockStyle = 'visibility: hidden !important; display: block !important;';
    var tDiv = $("#" + execInfo.targetDivId);
    var tDivParents = tDiv.parents().addBack().filter(':hidden');
    tDivParents.each(function() {
        var currentStyle = $(this).attr('style');
        parentStyles.push(currentStyle);
        $(this).attr('style', currentStyle ? currentStyle + ';' + hiddenBlockStyle : hiddenBlockStyle);
    });

    // calculate width/height
    w = tDiv.width()
    if (w) {
        execInfo.options.nostore_cw= w;
    }
    if ($(element).parents(".modal-dialog").length > 0 ) {
        h = tDiv.height()
        if (h) {
            execInfo.options.nostore_ch = h-10;
        }
    }

    // re-hide parents
    tDivParents.each(function(i) {
        if (parentStyles[i] === undefined) {
            $(this).removeAttr('style');
        } else {
            $(this).attr('style', parentStyles[i]);
        }
    });

    execInfo.script = element.getAttribute("pd_script");
    if (!execInfo.script){
        $(element).find("> pd_script").each(function(){
            var type = this.getAttribute("type");
            if (!type || type=="python"){
                execInfo.script = $(this).text();
            }
        })
    }

    execInfo.refresh = element.hasAttribute("pd_refresh");
    execInfo.entity = element.hasAttribute("pd_entity") ? element.getAttribute("pd_entity") || "pixieapp_entity" : null;

    function applyEntity(c, e, doptions){
        if (!e){
            return addOptions(c, doptions);
        }
        c = c.replace(/\((\w*),/, "($1." + e + ",")
        return addOptions(c, doptions);
    }

    if (!hasOptions && (execInfo.refresh || execInfo.options.widget) && !execInfo.script){
        execInfo.script = "#refresh";
    }

    var dialog = (execInfo.options.dialog == "true");
    if ( dialog ){
        execInfo.script = execInfo.script || "#refresh";
        execInfo.refresh = true;
    }

    if (execInfo.script){
        execInfo.script = execInfo.script.trim()
        {#set up the self variable#}
        var match = pd_controls.command.match(/display\((\w*),/)
        if (match){
            var entity = match[1]
            console.log("Inject self with entity", entity)
            execInfo.script = "from pixiedust.utils.shellAccess import ShellAccess\n"+
                "self=ShellAccess['" + entity + "']\n" +
                resolveScriptMacros( getParentScript(element) ) + '\n' +
                resolveScriptMacros(execInfo.script);

            if ( ( (!dialog && !execInfo.targetDivId) || execInfo.refresh || execInfo.entity) && $(element).children("target[pd_target]").length == 0){
                {#include a refresh of the whole screen#}
                execInfo.script += "\n" + applyEntity(pd_controls.command, execInfo.entity, execInfo.options)
            }else{
                {#make sure we have a targetDivId#}
                execInfo.targetDivId=execInfo.targetDivId || "dummy";
            }
        }else{
            console.log("Unable to extract entity variable from command", pd_controls.command);
        }
    }

    if (!hasOptions && !execInfo.targetDivId && !execInfo.script){
        return element.hasAttribute("pixiedust")?null:readExecInfo(pd_controls, element.parentElement);
    }

    if (!execInfo.script){
        execInfo.script = applyEntity(pd_controls.command, execInfo.entity, execInfo.options);
    }

    {#pixieapps never write their metadata on the cell #}
    execInfo.nostoreMedatadata = true;

    {#Adjust the targetDivId if in a dialog#}
    if ( pixiedust.dialogRoot ){
        execInfo.targetDivId = execInfo.targetDivId || pixiedust.dialogRoot;
    }

    execInfo.execute = function(){
        {#check if we have a pre-run client side script #}
        if (!preRun(element)){
            return;
        }
        if ( this.options.dialog == 'true' ){
            pixiedust.executeInDialog(pd_controls, this);
        }else{
            pixiedust.executeDisplay(pd_controls, this);
        }
    }

    console.log("execution info: ", execInfo);
    return execInfo;
}

function runElement(element){
    var pd_controls = element.getAttribute("pixiedust");
    if (!pd_controls){
        $(element).parents("[pixiedust]").each(function(){
            pd_controls = pd_controls || this.getAttribute("pixiedust");
        });
    }
    var execQueue = [];
    if (pd_controls){
        pd_controls = JSON.parse(pd_controls);
        {#read the current element#}
        execQueue.push( readExecInfo(pd_controls, element) );

        {#get other execution targets if any#}
        $(element).children("target[pd_target]").each(function(){
            execQueue.push( readExecInfo(pd_controls, this))
        });
    }
    return execQueue;
}

function filterNonTargetElements(element){
    if (element && element.tagName == "I"){
        return filterNonTargetElements(element.parentElement);
    }
    return element;
}

{#Dynamically add click handler on the pixiedust chrome menus#}
$(document).on( "click", "[pixiedust]", function(event){
    execQueue = runElement(filterNonTargetElements(event.target));
    {#execute#}
    $.each( execQueue, function(index, value){
        if (value){
            event.stopImmediatePropagation();
            value.execute();
        }
    });
});

$(document).on( "DOMNodeInserted", "[pd_widget]", function(event){
    event.stopImmediatePropagation();
    execQueue = runElement(filterNonTargetElements(event.target));
    {#execute#}
    $.each( execQueue, function(index, value){
        if (value){
            value.targetDivId = $(event.target).uniqueId().attr('id');
            $(event.target).removeAttr("pd_widget");
            value.execute();
        }
    });
});

{#handler for customer pd_event#}
$(document).on("pd_event", function(event, eventInfo){
    targetDivId = eventInfo.targetDivId;
    if (targetDivId){
        eventHandlers = $("pd_event_handler").filter(function(){
            if (this.getAttribute("pd_target") == targetDivId){
                return true;
            }
            {#Find a parent with pd_target attribute#}
            return $(this).parents("[pd_target]").filter(function(){
                return this.getAttribute("pd_target") == targetDivId;
            }).length > 0;
        });
        eventHandlers.each(function(){
            execQueue = runElement(this);
            $.each( execQueue, function(index, value){
                if (value){
                    {#Inject eventInfo#}
                    if (value.script){
                        value.script = "true=True\nfalse=False\neventInfo="+JSON.stringify(eventInfo) + "\n" + value.script;
                    }
                    value.execute();
                }
            });
        });
    }else{
        console.log("Warning: got a pd_event with no targetDivId", eventInfo);
    }
});<commit_msg>Added pd_norefresh option<commit_after>{%import "executePythonDisplayMacro.js" as display with context%}
{%import "commonExecuteCallback.js" as commons with context%}
var pixiedust = (function(){
    return {
        {#
            executeDisplay helper method: run a new display command
            displayCallback:{
                options: dictionary of new options to add to the display command
                onDisplayDone: callback function called when the display run is done executing
                targetDivId: id of div that will receive the output html, none means the default output
            }
        #}
        executeDisplay:function(pd_controls, user_controls){
            pd_controls = pd_controls || {};
            user_controls = user_controls || {"options":{}};
            var options = $.extend({}, pd_controls.options || {}, user_controls.options || {} );
            function onDisplayDone(){
                if (user_controls.onDisplayDone){
                    user_controls.onDisplayDone();
                }
            }
            var pd_prefix = pd_controls.prefix;
            var $targetDivId = user_controls.targetDivId;
            {%include "pd_executeDisplay.js"%}
        },
        executeInDialog:function(pd_controls, user_controls){
            pd_controls = pd_controls || {};
            user_controls = user_controls || {"options":{}};
            var displayOptions = $.extend({}, pd_controls.options || {}, user_controls.options || {} );
            var global={};
            require(['base/js/dialog'],function(dialog){
                var modal = dialog.modal;
                var attr_pd_ctrl = JSON.stringify(pd_controls).trim()
                    .replace(/&/g, '&')
                    .replace(/'/g, ''')
                    .replace(/"/g, '"')
                    .replace(/</g, '<')
                    .replace(/>/g, '>');
                var dialogRoot = "dialog" + pd_controls.prefix + "root";
                var options = {
                    title: "Pixiedust: " + (displayOptions.title || "Dialog"),
                    body: '

',
                    sanitize:false,
                    notebook: IPython.notebook,
                    keyboard_manager: IPython.notebook.keyboard_manager,
                    buttons: {
                        OK: {
                            class : "btn-primary btn-ok",
                            click: function() {
                                var dlg = $("#" + dialogRoot + " > pd_dialog");
                                try{
                                    return new Function('global', 'modal_obj', dlg.find("> pd_ok").text().trim())(global, modal_obj);
                                }catch(e){
                                    console.error(e);
                                    return false;
                                }
                            }
                        },
                        Cancel: {
                            class : "btn-cancel",
                            click: function(){
                            }
                        }
                    }
                };

                function resizeDialog() {
                    global.modalBodyStyle = $('.pixiedust .modal-body').attr('style');
                    global.modalFooterStyle = $('.pixiedust .modal-footer').attr('style');
                    $('.pixiedust .modal-body').attr('style', global.modalBodyStyle ? global.modalBodyStyle + ';padding:5px 20px !important;' : 'padding:5px 20px !important;');
                    $('.pixiedust .modal-footer').attr('style', 'display:none !important;');
                };

                function resetDialog() {
                    if (global.modalBodyStyle) {
                        $('.pixiedust .modal-body').attr('style', global.modalBodyStyle);
                    } else {
                        $('.pixiedust .modal-body').removeAttr('style');
                    }
                    if (global.modalFooterStyle) {
                        $('.pixiedust .modal-footer').attr('style', global.modalFooterStyle);
                    } else {
                        $('.pixiedust .modal-footer').removeAttr('style');
                    }
                };

                var modal_obj = modal(options);
                modal_obj.addClass('pixiedust pixiedust-app');
                modal_obj.on('shown.bs.modal', function(){
                    resizeDialog();
                    var isFF = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
                    if( isFF && options.keyboard_manager){
                        {#Only on FF, blur event issue, hard disable keyboard manager#}
                        var KeyboardManager = require('notebook/js/keyboardmanager').KeyboardManager;
                        global.KMEnableProto = KeyboardManager.prototype.enable;
                        KeyboardManager.prototype.enable = function () {
                            this.enabled = false;
                        };
                    }
                    IPython.keyboard_manager.register_events(modal_obj);
                    user_controls.options.targetDivId = user_controls.targetDivId = dialogRoot;
                    if ( user_controls.options.dialog == 'true'){
                        user_controls.onDisplayDone = function(){
                            var dlg = $("#" + dialogRoot + " > pd_dialog")
                            try{
                                new Function('global', 'modal_obj', dlg.find("> pd_onload").text().trim())(global, modal_obj);
                            }catch(e){
                                console.error(e);
                            }
                        }
                    }
                    pixiedust.dialogRoot = dialogRoot;
                    pixiedust.executeDisplay(pd_controls, user_controls);
                });
                modal_obj.on("hidden.bs.modal", function () {
                    resetDialog();
                    if ( global.KMEnableProto ){
                        var KeyboardManager = require('notebook/js/keyboardmanager').KeyboardManager;
                        KeyboardManager.prototype.enable = global.KMEnableProto;
                        delete global.KMEnableProto;
                    }
                    pixiedust.dialogRoot = null;
                });
            })
        }
    }
})();

function resolveScriptMacros(script){
    script = script.replace(/\$val\(\"?(\w*)\"?\)/g, function(a,b){
        var v = $("#" + b ).val();
        if (!v){
            console.log("Warning: Unable to resolve value for element ", b);
            return a;
        }
        return "\"" +v.split('"').join('"').split('\n').join('\\n') + "\"";
    });
    return script;
}

function getParentScript(element){
    var scripts = [];
    {#Get all parent scripts#}
    $(element).parents("[pd_script]").each(function(){
        scripts.unshift(this.getAttribute("pd_script"));
    });

    {#merge#}
    var script = "";
    $.each( scripts, function(index, value){
        if (value){
            script += "\n" + value;
        }
    });
    return script;
}

function preRun(element){
    var preRunCode = null;
    $(element).find("> pd_script").each(function(){
        var type = this.getAttribute("type");
        if (type=="preRun"){
            preRunCode = $(this).text();
        }
    });
    if (!preRunCode ){
        return true;
    }
    return new Function(preRunCode.trim())();
}

function addOptions(command, options, override=true){
    function getStringRep(v) {
        return "'" + v + "'";
    }
    for (var key in (options||{})){
        var value = options[key];
        var hasValue = value != null && typeof value !== 'undefined' && value !== '';
        var replaceValue = hasValue ? (key+"=" + getStringRep(value) ) : "";
        var pattern = (hasValue?"":",")+"\\s*" + key + "\\s*=\\s*'(\\\\'|[^'])*'";
        var rpattern=new RegExp(pattern);
        var n = command.search(rpattern);
        if ( n >= 0 ){
            if (override){
                command = command.replace(rpattern, replaceValue);
            }
        }else if (hasValue){
            var n = command.lastIndexOf(")");
            command = [command.slice(0, n), (command[n-1]=="("? "":",") + replaceValue, command.slice(n)].join('')
        }
    }
    return command;
}

function readExecInfo(pd_controls, element){
    var execInfo = {}
    execInfo.options = {}
    var hasOptions = false;
    $.each( element.attributes, function(){
        if (this.name.startsWith("option_")){
            hasOptions = true;
            execInfo.options[this.name.replace("option_", "")] = this.value || null;
        }
    });
    var pd_options = element.getAttribute("pd_options");
    if (pd_options){
        var parts = pd_options.split(";");
        $.each( parts, function(){
            var index = this.indexOf("=");
            if ( index > 1){
                hasOptions = true;
                execInfo.options[this.substring(0, index)] = this.substring(index+1);
            }
        });
    }
    execInfo.options.nostore_figureOnly = true;
    execInfo.options.targetDivId = execInfo.targetDivId = element.getAttribute("pd_target");
    if (execInfo.options.targetDivId){
        execInfo.options.no_margin=true;
    }

    execInfo.options.widget = element.getAttribute("pd_widget");

    // unhide parents temporarily to properly calculate width/height
    var parentStyles = [];
    var hiddenBlockStyle = 'visibility: hidden !important; display: block !important;';
    var tDiv = $("#" + execInfo.targetDivId);
    var tDivParents = tDiv.parents().addBack().filter(':hidden');
    tDivParents.each(function() {
        var currentStyle = $(this).attr('style');
        parentStyles.push(currentStyle);
        $(this).attr('style', currentStyle ? currentStyle + ';' + hiddenBlockStyle : hiddenBlockStyle);
    });

    // calculate width/height
    w = tDiv.width()
    if (w) {
        execInfo.options.nostore_cw= w;
    }
    if ($(element).parents(".modal-dialog").length > 0 ) {
        h = tDiv.height()
        if (h) {
            execInfo.options.nostore_ch = h-10;
        }
    }

    // re-hide parents
    tDivParents.each(function(i) {
        if (parentStyles[i] === undefined) {
            $(this).removeAttr('style');
        } else {
            $(this).attr('style', parentStyles[i]);
        }
    });

    execInfo.script = element.getAttribute("pd_script");
    if (!execInfo.script){
        $(element).find("> pd_script").each(function(){
            var type = this.getAttribute("type");
            if (!type || type=="python"){
                execInfo.script = $(this).text();
            }
        })
    }

    execInfo.refresh = element.hasAttribute("pd_refresh");
    execInfo.norefresh = element.hasAttribute("pd_norefresh");
    execInfo.entity = element.hasAttribute("pd_entity") ? element.getAttribute("pd_entity") || "pixieapp_entity" : null;

    function applyEntity(c, e, doptions){
        if (!e){
            return addOptions(c, doptions);
        }
        c = c.replace(/\((\w*),/, "($1." + e + ",")
        return addOptions(c, doptions);
    }

    if (!hasOptions && (execInfo.refresh || execInfo.options.widget) && !execInfo.script){
        execInfo.script = "#refresh";
    }

    var dialog = (execInfo.options.dialog == "true");
    if ( dialog ){
        execInfo.script = execInfo.script || "#refresh";
        execInfo.refresh = true;
    }

    if (execInfo.script){
        execInfo.script = execInfo.script.trim()
        {#set up the self variable#}
        var match = pd_controls.command.match(/display\((\w*),/)
        if (match){
            var entity = match[1]
            console.log("Inject self with entity", entity)
            execInfo.script = "from pixiedust.utils.shellAccess import ShellAccess\n"+
                "self=ShellAccess['" + entity + "']\n" +
                resolveScriptMacros( getParentScript(element) ) + '\n' +
                resolveScriptMacros(execInfo.script);

            if ( ( (!dialog && !execInfo.targetDivId) || execInfo.refresh || execInfo.entity) && !execInfo.norefresh && $(element).children("target[pd_target]").length == 0){
                {#include a refresh of the whole screen#}
                execInfo.script += "\n" + applyEntity(pd_controls.command, execInfo.entity, execInfo.options)
            }else{
                {#make sure we have a targetDivId#}
                execInfo.targetDivId=execInfo.targetDivId || "dummy";
            }
        }else{
            console.log("Unable to extract entity variable from command", pd_controls.command);
        }
    }

    if (!hasOptions && !execInfo.targetDivId && !execInfo.script){
        return element.hasAttribute("pixiedust")?null:readExecInfo(pd_controls, element.parentElement);
    }

    if (!execInfo.script){
        execInfo.script = applyEntity(pd_controls.command, execInfo.entity, execInfo.options);
    }

    {#pixieapps never write their metadata on the cell #}
    execInfo.nostoreMedatadata = true;

    {#Adjust the targetDivId if in a dialog#}
    if ( pixiedust.dialogRoot ){
        execInfo.targetDivId = execInfo.targetDivId || pixiedust.dialogRoot;
    }

    execInfo.execute = function(){
        {#check if we have a pre-run client side script #}
        if (!preRun(element)){
            return;
        }
        if ( this.options.dialog == 'true' ){
            pixiedust.executeInDialog(pd_controls, this);
        }else{
            pixiedust.executeDisplay(pd_controls, this);
        }
    }

    console.log("execution info: ", execInfo);
    return execInfo;
}

function runElement(element){
    var pd_controls = element.getAttribute("pixiedust");
    if (!pd_controls){
        $(element).parents("[pixiedust]").each(function(){
            pd_controls = pd_controls || this.getAttribute("pixiedust");
        });
    }
    var execQueue = [];
    if (pd_controls){
        pd_controls = JSON.parse(pd_controls);
        {#read the current element#}
        execQueue.push( readExecInfo(pd_controls, element) );

        {#get other execution targets if any#}
        $(element).children("target[pd_target]").each(function(){
            execQueue.push( readExecInfo(pd_controls, this))
        });
    }
    return execQueue;
}

function filterNonTargetElements(element){
    if (element && element.tagName == "I"){
        return filterNonTargetElements(element.parentElement);
    }
    return element;
}

{#Dynamically add click handler on the pixiedust chrome menus#}
$(document).on( "click", "[pixiedust]", function(event){
    execQueue = runElement(filterNonTargetElements(event.target));
    {#execute#}
    $.each( execQueue, function(index, value){
        if (value){
            event.stopImmediatePropagation();
            value.execute();
        }
    });
});

$(document).on( "DOMNodeInserted", "[pd_widget]", function(event){
    event.stopImmediatePropagation();
    execQueue = runElement(filterNonTargetElements(event.target));
    {#execute#}
    $.each( execQueue, function(index, value){
        if (value){
            value.targetDivId = $(event.target).uniqueId().attr('id');
            $(event.target).removeAttr("pd_widget");
            value.execute();
        }
    });
});

{#handler for customer pd_event#}
$(document).on("pd_event", function(event, eventInfo){
    targetDivId = eventInfo.targetDivId;
    if (targetDivId){
        eventHandlers = $("pd_event_handler").filter(function(){
            if (this.getAttribute("pd_target") == targetDivId){
                return true;
            }
            {#Find a parent with pd_target attribute#}
            return $(this).parents("[pd_target]").filter(function(){
                return this.getAttribute("pd_target") == targetDivId;
            }).length > 0;
        });
        eventHandlers.each(function(){
            execQueue = runElement(this);
            $.each( execQueue, function(index, value){
                if (value){
                    {#Inject eventInfo#}
                    if (value.script){
                        value.script = "true=True\nfalse=False\neventInfo="+JSON.stringify(eventInfo) + "\n" + value.script;
                    }
                    value.execute();
                }
            });
        });
    }else{
        console.log("Warning: got a pd_event with no targetDivId", eventInfo);
    }
});<|endoftext|>


<commit_before>const PFSequelize = require('./PFSequelize.js');

const DEFAULT_TIME_OUT = 15 * 60; // 15 minutes
const DEFAULT_REFRESH_PERIOD = 1 * 60 // 1 minute

const TYPE_LFG = 'lfg';
const TYPE_LFM = 'lfm';

const ROLE_SPEC = {
    'attack': {
        name: 'Attacker',
        regex: /attack/i
    },

    'break': {
        name: 'Breaker',
        regex: /break/i
    },

    'defend': {
        name: 'Defender',
        regex: /defend/i
    },

    'support': {
        name: 'Support',
        regex: /support/i
    }
}

module.exports = class PartyFinder {

    /**
     *
     * @param {Sequelize} sequelizeClient
     */
    constructor(sequelizeClient) {
        this.db = new PFSequelize(sequelizeClient);
        this.monitoredChannels = {};
        this.monitoredPartyChannels = {};
        this.partyMemberDB = {};
        this.pfUpdater = {};

        this.roleSpecs = ROLE_SPEC;
        this.TYPE_LFG = TYPE_LFG;
        this.TYPE_LFM = TYPE_LFM;

        this.db.init()
            .then(() => {
                return this.db.fetchAllChannel();
            })
            .then((channelList) => {
                channelList.forEach((channel) => {
                    this.monitoredChannels[channel.channel] = channel;
                    this.partyMemberDB[channel.channel] = {};
                    this.populatePartyChannelMonitor(channel);
                });
            })
            .catch((err) => {
                console.error('Unable to initialize party finder database', err);
                throw err;
            });
    }

    /**
     *
     * @param {PFChannel} pfChannel
     * @param {String} partyChannelId
     */
    populatePartyChannelMonitor(pfChannel, partyChannelId) {
        if (partyChannelId == undefined) {
            for (var partyChannelId of pfChannel.partyChannel) {
                this.addPartyChannelMonitor(pfChannel.channel, partyChannelId);
            }
        } else {
            this.addPartyChannelMonitor(pfChannel.channel, partyChannelId);
        }
    }

    /**
     *
     * @param {String} pfChannelId
     * @param {String} partyChannelId
     */
    addPartyChannelMonitor(pfChannelId, partyChannelId) {
        if (this.monitoredPartyChannels[partyChannelId] == undefined)
            this.monitoredPartyChannels[partyChannelId] = [];
        if (this.monitoredPartyChannels[partyChannelId].indexOf(pfChannelId) < 0)
            this.monitoredPartyChannels[partyChannelId].push(pfChannelId);
    }

    /**
     *
     * @param {PFChannel} pfChannel
     */
    clearPartyChannelMonitor(pfChannel) {
        for (var partyChannelId of pfChannel.partyChannel) {
            const channelList = this.monitoredPartyChannels[partyChannelId];
            if (channelList != undefined && channelList.indexOf(pfChannel.channel) >= 0) {
                channelList.splice(channelList.indexOf(pfChannel.channel), 1);
            }
        }
        pfChannel.partyChannel = [];
    }

    /**
     *
     * @param {Channel} channel Discord JS Channel object
     * @param {int} [opts_timeOut] Time out duration in seconds.
     * @param {int} [opts_refreshPeriod] Refresh period in seconds.
     */
    async registerChannel(channel, opts_timeOut, opts_refreshPeriod) {
        const channelList = this.monitoredChannels;
        if (channelList[channel.id] != undefined)
            return "This channel has already been registered.";

        var timeOut = (opts_timeOut == undefined ? DEFAULT_TIME_OUT : opts_timeOut);
        var refreshPeriod = (opts_refreshPeriod == undefined ? DEFAULT_REFRESH_PERIOD : opts_refreshPeriod);
        return await Promise.resolve(channel)
            .then(() => {
                return this.db.registerPFChannel(channel.id, [], timeOut, refreshPeriod)
            })
            .then((pfChannel) => {
                channelList[pfChannel.channel] = pfChannel;
                pfChannel.partyChannel = [];
                return "Channel registered successfully.";
            })
            .catch((err) => {
                console.error("Error while registering PF channel: " + err);
                return "Error while registering PF channel: " + err;
            })
    }

    /**
     *
     * @param {Channel} channel Discord JS Channel object
     */
    async deregisterChannel(channel) {
        const channelList = this.monitoredChannels;
        if (channelList[channel.id] == undefined)
            return "This channel has not yet been registered.";

        return await Promise.resolve(channel)
            .then(() => {
                this.db.removePFChannel(channel.id)
            })
            .then(() => {
                delete channelList[channel.id];
                return "Channel deregistered successfully.";
            })
            .catch((err) => {
                console.error("Error while deregistering PF channel: " + err);
                return "Error while deregistering PF channel: " + err;
            })
    }

    /**
     *
     * @param {Channel} pfChannel
     * @param {Channel} partyChannel
     */
    async addPartyChannel(pfChannel, partyChannel) {
        const channelList = this.monitoredChannels;
        if (channelList[pfChannel.id] == undefined)
            return "This channel has not yet been registered.";

        var partyChannelList = channelList[pfChannel.id].partyChannel;
        if (partyChannelList.indexOf(partyChannel.id) < 0) {
            partyChannelList.push(partyChannel.id);
            return await Promise.resolve()
                .then(() => {
                    this.db.updatePFChannel(pfChannel.id, 'partyChannel', partyChannelList);
                })
                .then(() => {
                    this.addPartyChannelMonitor(pfChannel.id, partyChannel.id);
                })
                .catch((err) => {
                    partyChannelList.splice(partyChannelList.indexOf(partyChannel.id), 1);
                    console.error("Error while adding party channel: " + err);
                    return "Error while adding party channel: " + err;
                })
        } else
            return "This party channel has already been added.";
    }

    /**
     *
     * @param {Channel} pfChannel
     */
    async clearPartyChannel(pfChannel) {
        const channelList = this.monitoredChannels;
        if (channelList[pfChannel.id] == undefined)
            return "This channel has not yet been registered.";

        return await Promise.resolve()
            .then(() => {
                this.db.updatePFChannel(pfChannel.id, 'partyChannel', []);
            })
            .then(() => {
                this.clearPartyChannelMonitor(channelList[pfChannel.id]);
                //channelList[pfChannel.id].partyChannel = [];
                return "Party channels cleared.";
            })
            .catch((err) => {
                console.error("Error while clearing party channel: " + err);
                return "Error while clearing party channel: " + err;
            })
    }

    /**
     *
     * @param {Channel} channel
     */
    isMonitored(channel) {
        return (this.monitoredChannels[channel.id] != undefined);
    }

    /**
     * refresh period in seconds
     *
     * @param {Channel} channel
     */
    getChannelRefreshRate(channel) {
        return (this.monitoredChannels[channel.id].refreshPeriod);
    }

    /**
     *
     * @param {User} user Discord JS User object
     * @param {Channel} channel Discord JS Channel object
     * @param {Array} roleList Array of roles requested
     */
    updateUser(user, channel, roleList, type) {
        // if (this.partyMemberDB[channel.id] == undefined)
        //     this.partyMemberDB[channel.id] = {};

        const channelConfig = this.monitoredChannels[channel.id];
        const now = (new Date()).getTime();
        const timeOutThreshold = now - channelConfig.timeOut * 1000;
        const userDB = this.partyMemberDB[channel.id];
        const userKey = user.id;

        if (userDB[userKey] != undefined) {
            var userPF = userDB[userKey];
            if (userPF.queueTime < timeOutThreshold)
                userPF.queueTime = now;
            userPF.roleList = roleList;
            userPF.type = type;
        } else {
            userDB[userKey] = {
                name: user.username,
                queueTime: now,
                roleList: roleList,
                type: type
            }
        }

        if (this.pfUpdater[channel.id] != undefined) {
            this.pfUpdater[channel.id].update();
            this.pfUpdater[channel.id].replaceRequired = true;
        }
    }

    /**
     *
     * @param {Message} msg
     */
    monitorPartyChannel(msg) {
        const channel = msg.channel;
        if (this.monitoredPartyChannels[channel.id] != undefined) {
            // if this channel is being monitored
            for (var pfChannelId of this.monitoredPartyChannels[channel.id]) {
                // check against all PF channel that this party channel belongs to
                const user = msg.author;
                const userDB = this.partyMemberDB[pfChannelId];
                if (userDB[user.id] != undefined) {
                    if (userDB[user.id].type == TYPE_LFM) {
                        // if the one LFG does not post a room number, keep him
                        if (/^[0-9]{4,8}$/.exec(msg.content) == null)
                            return;
                    }
                    delete userDB[user.id];
                    if (this.pfUpdater[pfChannelId] != undefined) {
                        this.pfUpdater[pfChannelId].update();
                        this.pfUpdater[pfChannelId].replaceRequired = true;
                    }
                    console.log('Removing user ' + user.username + ' from PF');
                }
            }
        }
    }

    /**
     *
     * @param {Channel} channel Discord JS Channel object
     */
    retrievePFUsers(channel) {
        const channelConfig = this.monitoredChannels[channel.id];
        const now = (new Date()).getTime();
        const timeOutThreshold = now - channelConfig.timeOut * 1000;
        const userDB = this.partyMemberDB[channel.id];

        var ret = {};
        ret[TYPE_LFG] = {};
        ret[TYPE_LFM] = [];

        for (var userId in userDB) {
            const userPF = userDB[userId];

            if (userPF.queueTime > timeOutThreshold) {
                if (userPF.type == TYPE_LFM) {
                    ret[TYPE_LFM].push(userPF.name);
                } else {
                    for (var role of userPF.roleList) {
                        if (ret[userPF.type][role] == undefined)
                            ret[userPF.type][role] = [];
                        ret[userPF.type][role].push(userPF.name);
                    }
                }
            }
        }

        var sortFunc = function (left, right) {
            return userDB[left] > userDB[right];
        }

        for (var role in ret[TYPE_LFG])
            ret[TYPE_LFG][role].sort(sortFunc);
        ret[TYPE_LFM].sort(sortFunc);

        return ret;
    }

}<commit_msg>Fix small inconsistency<commit_after>const PFSequelize = require('./PFSequelize.js');

const DEFAULT_TIME_OUT = 15 * 60; // 15 minutes
const DEFAULT_REFRESH_PERIOD = 1 * 60 // 1 minute

const TYPE_LFG = 'lfg';
const TYPE_LFM = 'lfm';

const ROLE_SPEC = {
    'attack': {
        name: 'Attacker',
        regex: /attack/i
    },

    'break': {
        name: 'Breaker',
        regex: /break/i
    },

    'defend': {
        name: 'Defender',
        regex: /defend/i
    },

    'support': {
        name: 'Support',
        regex: /support/i
    }
}

module.exports = class PartyFinder {

    /**
     *
     * @param {Sequelize} sequelizeClient
     */
    constructor(sequelizeClient) {
        this.db = new PFSequelize(sequelizeClient);
        this.monitoredChannels = {};
        this.monitoredPartyChannels = {};
        this.partyMemberDB = {};
        this.pfUpdater = {};

        this.roleSpecs = ROLE_SPEC;
        this.TYPE_LFG = TYPE_LFG;
        this.TYPE_LFM = TYPE_LFM;

        this.db.init()
            .then(() => {
                return this.db.fetchAllChannel();
            })
            .then((channelList) => {
                channelList.forEach((channel) => {
                    this.monitoredChannels[channel.channel] = channel;
                    this.partyMemberDB[channel.channel] = {};
                    this.populatePartyChannelMonitor(channel);
                });
            })
            .catch((err) => {
                console.error('Unable to initialize party finder database', err);
                throw err;
            });
    }

    /**
     *
     * @param {PFChannel} pfChannel
     * @param {String} partyChannelId
     */
    populatePartyChannelMonitor(pfChannel, partyChannelId) {
        if (partyChannelId == undefined) {
            for (var partyChannelId of pfChannel.partyChannel) {
                this.addPartyChannelMonitor(pfChannel.channel, partyChannelId);
            }
        } else {
            this.addPartyChannelMonitor(pfChannel.channel, partyChannelId);
        }
    }

    /**
     *
     * @param {String} pfChannelId
     * @param {String} partyChannelId
     */
    addPartyChannelMonitor(pfChannelId, partyChannelId) {
        if (this.monitoredPartyChannels[partyChannelId] == undefined)
            this.monitoredPartyChannels[partyChannelId] = [];
        if (this.monitoredPartyChannels[partyChannelId].indexOf(pfChannelId) < 0)
            this.monitoredPartyChannels[partyChannelId].push(pfChannelId);
    }

    /**
     *
     * @param {PFChannel} pfChannel
     */
    clearPartyChannelMonitor(pfChannel) {
        for (var partyChannelId of pfChannel.partyChannel) {
            const channelList = this.monitoredPartyChannels[partyChannelId];
            if (channelList != undefined && channelList.indexOf(pfChannel.channel) >= 0) {
                channelList.splice(channelList.indexOf(pfChannel.channel), 1);
            }
        }
        pfChannel.partyChannel = [];
    }

    /**
     *
     * @param {Channel} channel Discord JS Channel object
     * @param {int} [opts_timeOut] Time out duration in seconds.
     * @param {int} [opts_refreshPeriod] Refresh period in seconds.
     */
    async registerChannel(channel, opts_timeOut, opts_refreshPeriod) {
        const channelList = this.monitoredChannels;
        if (channelList[channel.id] != undefined)
            return "This channel has already been registered.";

        var timeOut = (opts_timeOut == undefined ? DEFAULT_TIME_OUT : opts_timeOut);
        var refreshPeriod = (opts_refreshPeriod == undefined ? DEFAULT_REFRESH_PERIOD : opts_refreshPeriod);
        return await Promise.resolve(channel)
            .then(() => {
                return this.db.registerPFChannel(channel.id, [], timeOut, refreshPeriod)
            })
            .then((pfChannel) => {
                channelList[pfChannel.channel] = pfChannel;
                pfChannel.partyChannel = [];
                return "Channel registered successfully.";
            })
            .catch((err) => {
                console.error("Error while registering PF channel: " + err);
                return "Error while registering PF channel: " + err;
            })
    }

    /**
     *
     * @param {Channel} channel Discord JS Channel object
     */
    async deregisterChannel(channel) {
        const channelList = this.monitoredChannels;
        if (channelList[channel.id] == undefined)
            return "This channel has not yet been registered.";

        return await Promise.resolve(channel)
            .then(() => {
                this.db.removePFChannel(channel.id)
            })
            .then(() => {
                delete channelList[channel.id];
                return "Channel deregistered successfully.";
            })
            .catch((err) => {
                console.error("Error while deregistering PF channel: " + err);
                return "Error while deregistering PF channel: " + err;
            })
    }

    /**
     *
     * @param {Channel} pfChannel
     * @param {Channel} partyChannel
     */
    async addPartyChannel(pfChannel, partyChannel) {
        const channelList = this.monitoredChannels;
        if (channelList[pfChannel.id] == undefined)
            return "This channel has not yet been registered.";

        var partyChannelList = channelList[pfChannel.id].partyChannel;
        if (partyChannelList.indexOf(partyChannel.id) < 0) {
            partyChannelList.push(partyChannel.id);
            return await Promise.resolve()
                .then(() => {
                    this.db.updatePFChannel(pfChannel.id, 'partyChannel', partyChannelList);
                })
                .then(() => {
                    this.addPartyChannelMonitor(pfChannel.id, partyChannel.id);
                })
                .catch((err) => {
                    partyChannelList.splice(partyChannelList.indexOf(partyChannel.id), 1);
                    console.error("Error while adding party channel: " + err);
                    return "Error while adding party channel: " + err;
                })
        } else
            return "This party channel has already been added.";
    }

    /**
     *
     * @param {Channel} pfChannel
     */
    async clearPartyChannel(pfChannel) {
        const channelList = this.monitoredChannels;
        if (channelList[pfChannel.id] == undefined)
            return "This channel has not yet been registered.";

        return await Promise.resolve()
            .then(() => {
                this.db.updatePFChannel(pfChannel.id, 'partyChannel', []);
            })
            .then(() => {
                this.clearPartyChannelMonitor(channelList[pfChannel.id]);
                //channelList[pfChannel.id].partyChannel = [];
                return "Party channels cleared.";
            })
            .catch((err) => {
                console.error("Error while clearing party channel: " + err);
                return "Error while clearing party channel: " + err;
            })
    }

    /**
     *
     * @param {Channel} channel
     */
    isMonitored(channel) {
        return (this.monitoredChannels[channel.id] != undefined);
    }

    /**
     * refresh period in seconds
     *
     * @param {Channel} channel
     */
    getChannelRefreshRate(channel) {
        return (this.monitoredChannels[channel.id].refreshPeriod);
    }

    /**
     *
     * @param {User} user Discord JS User object
     * @param {Channel} channel Discord JS Channel object
     * @param {Array} roleList Array of roles requested
     */
    updateUser(user, channel, roleList, type) {
        const channelConfig = this.monitoredChannels[channel.id];
        const now = (new Date()).getTime();
        const timeOutThreshold = now - channelConfig.timeOut * 1000;
        const userDB = this.partyMemberDB[channel.id];
        const userKey = user.id;

        if (userDB[userKey] != undefined) {
            var userPF = userDB[userKey];
            if (userPF.queueTime < timeOutThreshold)
                userPF.queueTime = now;
            userPF.roleList = roleList;
            userPF.type = type;
        } else {
            userDB[userKey] = {
                name: user.username,
                queueTime: now,
                roleList: roleList,
                type: type
            }
        }

        if (this.pfUpdater[channel.id] != undefined) {
            this.pfUpdater[channel.id].update();
            this.pfUpdater[channel.id].replaceRequired = true;
        }
    }

    /**
     *
     * @param {Message} msg
     */
    monitorPartyChannel(msg) {
        const channel = msg.channel;
        if (this.monitoredPartyChannels[channel.id] != undefined) {
            // if this channel is being monitored
            for (var pfChannelId of this.monitoredPartyChannels[channel.id]) {
                // check against all PF channel that this party channel belongs to
                const user = msg.author;
                const userDB = this.partyMemberDB[pfChannelId];
                if (userDB[user.id] != undefined) {
                    if (userDB[user.id].type == TYPE_LFM) {
                        // if the one LFG does not post a room number, keep him
                        if (/^[0-9]{4,8}$/.exec(msg.content) == null)
                            return;
                    }
                    delete userDB[user.id];
                    if (this.pfUpdater[pfChannelId] != undefined) {
                        this.pfUpdater[pfChannelId].update();
                        this.pfUpdater[pfChannelId].replaceRequired = true;
                    }
                    console.log('Removing user ' + user.username + ' from PF');
                }
            }
        }
    }

    /**
     *
     * @param {Channel} channel Discord JS Channel object
     */
    retrievePFUsers(channel) {
        const channelConfig = this.monitoredChannels[channel.id];
        const now = (new Date()).getTime();
        const timeOutThreshold = now - channelConfig.timeOut * 1000;
        const userDB = this.partyMemberDB[channel.id];

        var ret = {};
        ret[TYPE_LFG] = {};
        ret[TYPE_LFM] = [];

        for (var userId in userDB) {
            const userPF = userDB[userId];

            if (userPF.queueTime > timeOutThreshold) {
                if (userPF.type == TYPE_LFM) {
                    ret[TYPE_LFM].push(userPF.name);
                } else {
                    for (var role of userPF.roleList) {
                        if (ret[userPF.type][role] == undefined)
                            ret[userPF.type][role] = [];
                        ret[userPF.type][role].push(userPF.name);
                    }
                }
            }
        }

        var sortFunc = function (left, right) {
            return userDB[left] > userDB[right];
        }

        for (var role in ret[TYPE_LFG])
            ret[TYPE_LFG][role].sort(sortFunc);
        ret[TYPE_LFM].sort(sortFunc);

        return ret;
    }

}<|endoftext|>