<commit_before>// requireDir.js
// See README.md for details.

var FS = require('fs');
var Path = require('path');
var requireJade = require("require-jade");
console.log(require.extensions);

// make a note of the calling file's path, so that we can resolve relative
// paths. this only works if a fresh version of this module is run on every
// require(), so important: we clear the require() cache each time!
var parent = module.parent;
var parentFile = parent.filename;
var parentDir = Path.dirname(parentFile);
delete require.cache[__filename];

module.exports = function requireDir(dir, opts) {
    // default arguments:
    dir = dir || '.';
    opts = opts || {};

    // resolve the path to an absolute one:
    dir = Path.resolve(parentDir, dir);

    // read the directory's files:
    // note that this'll throw an error if the path isn't a directory.
    var files = FS.readdirSync(dir);

    // to prioritize between multiple files with the same basename, we'll
    // first derive all the basenames and create a map from them to files:
    var filesForBase = {};

    for (var i = 0; i < files.length; i++) {
        var file = files[i];
        var ext = Path.extname(file);
        var base = Path.basename(file, ext);

        (filesForBase[base] = filesForBase[base] || []).push(file);
    }

    // then we'll go through each basename, and first check if any of the
    // basenames' files are directories, since directories take precedence if
    // we're recursing and can be ignored if we're not. if a basename has no
    // directory, then we'll follow Node's own require() algorithm of going
    // through and trying the require.extension keys in order. in the process,
    // we create and return a map from basename to require()'d contents! and
    // if duplicates are asked for, we'll never short-circuit; we'll just add
    // to the map using the full filename as a key also.
    var map = {};

    for (var base in filesForBase) {
        // protect against enumerable object prototype extensions:
        if (!filesForBase.hasOwnProperty(base)) {
            continue;
        }

        // go through the files for this base and check for directories. we'll
        // also create a hash "set" of the non-dir files so that we can
        // efficiently check for existence in the next step:
        var files = filesForBase[base];
        var filesMinusDirs = {};

        for (var i = 0; i < files.length; i++) {
            var file = files[i];
            var path = Path.resolve(dir, file);

            // ignore the calling file:
            if (path === parentFile) {
                continue;
            }

            if (FS.statSync(path).isDirectory()) {
                if (opts.recurse) {
                    if (base === 'node_modules') {
                        continue;
                    }

                    map[base] = requireDir(path, opts);

                    // if duplicates are wanted, key off the full name too:
                    if (opts.duplicates) {
                        map[file] = map[base];
                    }
                }
            } else {

                // before adding a path to fileMinusDirs list we
                // check, if it is not ignored
                if (opts.ignore) {

                    // string options are wrapped with an array
                    // in order to make the algorithm clearer
                    if (!Array.isArray(opts.ignore)) {
                        opts.ignore = [opts.ignore];
                    }

                    // every ignore pattern is interpreted as RegExp,
                    // a file path is to be tested against
                    var isIgnored=false;
                    opts.ignore.forEach(function(value){
                        if(path.match(new RegExp(value))){
                            isIgnored=true;
                            return;
                        }
                    });

                    if (!isIgnored) {
                        filesMinusDirs[file] = path;
                    }

                } else {
                    filesMinusDirs[file] = path;
                }

            }
        }

        // if we're recursing and we already encountered a directory for this
        // basename, we're done for this base if we're ignoring duplicates:
        if (map[base] && !opts.duplicates) {
            continue;
        }

        // otherwise, go through and try each require.extension key!
        for (ext in require.extensions) {
            // again protect against enumerable object prototype extensions:
            if (!require.extensions.hasOwnProperty(ext)) {
                continue;
            }

            // if a file exists with this extension, we'll require() it:
            var file = base + ext;
            var path = filesMinusDirs[file];

            if (path) {
                // if duplicates are wanted, key off the full name always, and
                // also the base if it hasn't been taken yet (since this ext
                // has higher priority than any that follow it). if duplicates
                // aren't wanted, we're done with this basename.
                if (opts.duplicates) {
                    map[file] = require(path);
                    if (!map[base]) {
                        map[base] = map[file];
                    }
                } else {
                  if (ext == '.jade') map[base + ext] = true;
                    else map[base] = require(path);
                    break;
                }
            }
        }
    }

    if (opts.camelcase) {
        for (var base in map) {
            // protect against enumerable object prototype extensions:
            if (!map.hasOwnProperty(base)) {
                continue;
            }

            map[toCamelCase(base)] = map[base];
        }
    }

    return map;
};

function toCamelCase(str) {
    return str.replace(/[_-][a-z]/ig, function (s) {
        return s.substring(1).toUpperCase();
    });
}
<commit_msg>jade stuff<commit_after>// requireDir.js
// See README.md for details.

var FS = require('fs');
var Path = require('path');
var requireJade = require("require-jade");
var jade = require("jade");
console.log(require.extensions);

// make a note of the calling file's path, so that we can resolve relative
// paths. this only works if a fresh version of this module is run on every
// require(), so important: we clear the require() cache each time!
var parent = module.parent;
var parentFile = parent.filename;
var parentDir = Path.dirname(parentFile);
delete require.cache[__filename];

module.exports = function requireDir(dir, opts) {
    // default arguments:
    dir = dir || '.';
    opts = opts || {};

    // resolve the path to an absolute one:
    dir = Path.resolve(parentDir, dir);

    // read the directory's files:
    // note that this'll throw an error if the path isn't a directory.
    var files = FS.readdirSync(dir);

    // to prioritize between multiple files with the same basename, we'll
    // first derive all the basenames and create a map from them to files:
    var filesForBase = {};

    for (var i = 0; i < files.length; i++) {
        var file = files[i];
        var ext = Path.extname(file);
        var base = Path.basename(file, ext);

        (filesForBase[base] = filesForBase[base] || []).push(file);
    }

    // then we'll go through each basename, and first check if any of the
    // basenames' files are directories, since directories take precedence if
    // we're recursing and can be ignored if we're not. if a basename has no
    // directory, then we'll follow Node's own require() algorithm of going
    // through and trying the require.extension keys in order. in the process,
    // we create and return a map from basename to require()'d contents! and
    // if duplicates are asked for, we'll never short-circuit; we'll just add
    // to the map using the full filename as a key also.
    var map = {};

    for (var base in filesForBase) {
        // protect against enumerable object prototype extensions:
        if (!filesForBase.hasOwnProperty(base)) {
            continue;
        }

        // go through the files for this base and check for directories. we'll
        // also create a hash "set" of the non-dir files so that we can
        // efficiently check for existence in the next step:
        var files = filesForBase[base];
        var filesMinusDirs = {};

        for (var i = 0; i < files.length; i++) {
            var file = files[i];
            var path = Path.resolve(dir, file);

            // ignore the calling file:
            if (path === parentFile) {
                continue;
            }

            if (FS.statSync(path).isDirectory()) {
                if (opts.recurse) {
                    if (base === 'node_modules') {
                        continue;
                    }

                    map[base] = requireDir(path, opts);

                    // if duplicates are wanted, key off the full name too:
                    if (opts.duplicates) {
                        map[file] = map[base];
                    }
                }
            } else {

                // before adding a path to fileMinusDirs list we
                // check, if it is not ignored
                if (opts.ignore) {

                    // string options are wrapped with an array
                    // in order to make the algorithm clearer
                    if (!Array.isArray(opts.ignore)) {
                        opts.ignore = [opts.ignore];
                    }

                    // every ignore pattern is interpreted as RegExp,
                    // a file path is to be tested against
                    var isIgnored=false;
                    opts.ignore.forEach(function(value){
                        if(path.match(new RegExp(value))){
                            isIgnored=true;
                            return;
                        }
                    });

                    if (!isIgnored) {
                        filesMinusDirs[file] = path;
                    }

                } else {
                    filesMinusDirs[file] = path;
                }

            }
        }

        // if we're recursing and we already encountered a directory for this
        // basename, we're done for this base if we're ignoring duplicates:
        if (map[base] && !opts.duplicates) {
            continue;
        }

        // otherwise, go through and try each require.extension key!
        for (ext in require.extensions) {
            // again protect against enumerable object prototype extensions:
            if (!require.extensions.hasOwnProperty(ext)) {
                continue;
            }

            // if a file exists with this extension, we'll require() it:
            var file = base + ext;
            var path = filesMinusDirs[file];

            if (path) {
                // if duplicates are wanted, key off the full name always, and
                // also the base if it hasn't been taken yet (since this ext
                // has higher priority than any that follow it). if duplicates
                // aren't wanted, we're done with this basename.
                if (opts.duplicates) {
                    map[file] = require(path);
                    if (!map[base]) {
                        map[base] = map[file];
                    }
                } else {
                  if (ext == '.jade') {
                    map[base + ext] = FS.readFileSync(path, { encoding: 'utf8' });
                    //jade.compileFile(path, {name: Path.basename(path,'.jade') + "ViewTemplate"});
                    //
                  }
                  else map[base] = require(path);
                  break;
                }
            }
        }
    }

    if (opts.camelcase) {
        for (var base in map) {
            // protect against enumerable object prototype extensions:
            if (!map.hasOwnProperty(base)) {
                continue;
            }

            map[toCamelCase(base)] = map[base];
        }
    }

    return map;
};

function toCamelCase(str) {
    return str.replace(/[_-][a-z]/ig, function (s) {
        return s.substring(1).toUpperCase();
    });
}
<|endoftext|>


<commit_before>// Inspired by (but complete rewrite of) requirejs-metagen
var _           = require('lodash/fp'),
    Promise     = require('bluebird'),
    readDir     = Promise.promisify(require('recursive-readdir')),
    fs          = Promise.promisifyAll(require('fs'));

// Path Utils
var filesRelative = (dir, ex) => readDir(dir, ex).map(x => x.slice(dir.length));
var noExt = file => file.slice(0, _.lastIndexOf('.', file));

// Core
var metagen = dir => filesRelative(dir.path, dir.exclusions || [dir.output || 'all.js']).then(files =>
    fs.writeFileAsync(dir.path + (dir.output || 'all.js'), dir.format(files, dir))
);

// Formats
metagen.formats = {};
metagen.formats.commonJS = files => `define(function(require) {
    return {
        ${ files.map(file => `'${ noExt(file) }': require('./${ noExt(file) }')`).join(',\n        ') }
    };
});`;
metagen.formats.amd = files => `define([
    ${ files.map(file => `'${ noExt(file) }'`).join(',\n    ') }
], function() {
    return {
        ${ files.map((file, i) => `'${ noExt(file) }': arguments[${ i }]`).join(',\n        ') }
    }
});`;

// use FP when https://github.com/lodash/lodash/pull/2503 is released
var zipObjectDeep = require('lodash/zipObjectDeep');

// Deep Formats
var deepKeys    = _.map(_.flow(noExt, _.replace('/', '.'))),
    stringify   = x => JSON.stringify(x, null, 4),
    indent      = _.replace(/\n/g, '\n    '),
    unquote     = _.replace(/"/g, ''),
    deepify     = _.flow(zipObjectDeep, stringify, indent, unquote);

metagen.formats.deepCommonJS = files => `define(function(require) {
    return ${ deepify(deepKeys(files), files.map(file => `require('${ noExt(file) }')`))};
});`;
metagen.formats.deepAMD = files => `define([
    ${ files.map(file => `'${ noExt(file) }'`).join(',\n    ') }
], function() {
    return ${ deepify(deepKeys(files), files.map((file, i) => `arguments[${ i }]`))};
});`;

module.exports = metagen;<commit_msg>tweak regexes and wrap nested keys in quotes<commit_after>// Inspired by (but complete rewrite of) requirejs-metagen
var _           = require('lodash/fp'),
    Promise     = require('bluebird'),
    readDir     = Promise.promisify(require('recursive-readdir')),
    fs          = Promise.promisifyAll(require('fs'));

// Path Utils
var filesRelative = (dir, ex) => readDir(dir, ex).map(x => x.slice(dir.length));
var noExt = file => file.slice(0, _.lastIndexOf('.', file));

// Core
var metagen = dir => filesRelative(dir.path, dir.exclusions || [dir.output || 'all.js']).then(files =>
    fs.writeFileAsync(dir.path + (dir.output || 'all.js'), dir.format(files, dir))
);

// Formats
metagen.formats = {};
metagen.formats.commonJS = files => `define(function(require) {
    return {
        ${ files.map(file => `'${ noExt(file) }': require('./${ noExt(file) }')`).join(',\n        ') }
    };
});`;
metagen.formats.amd = files => `define([
    ${ files.map(file => `'${ noExt(file) }'`).join(',\n    ') }
], function() {
    return {
        ${ files.map((file, i) => `'${ noExt(file) }': arguments[${ i }]`).join(',\n        ') }
    }
});`;

// use FP when https://github.com/lodash/lodash/pull/2503 is released
var zipObjectDeep = require('lodash/zipObjectDeep');

// Deep Formats
var deepKeys    = _.map(_.flow(noExt, _.replace(/\//g, '.'))),
    stringify   = x => JSON.stringify(x, null, 4),
    indent      = _.replace(/\n/g, '\n    '),
    unquote     = _.replace(/"require(.*)'\)"/g, "require$1')"),
    deepify     = _.flow(zipObjectDeep, stringify, indent, unquote);

metagen.formats.deepCommonJS = files => `define(function(require) {
    return ${ deepify(deepKeys(files), files.map(file => `require('${ noExt(file) }')`))};
});`;
metagen.formats.deepAMD = files => `define([
    ${ files.map(file => `'${ noExt(file) }'`).join(',\n    ') }
], function() {
    return ${ deepify(deepKeys(files), files.map((file, i) => `arguments[${ i }]`))};
});`;

module.exports = metagen;<|endoftext|>


<commit_before>var config = {};

/**
 * Url protocol that gets registered in operating systems
 * and used to communicate beetween browsers extensions and native application
 * @type {String}
 */
config.urlProtocol = 'lolbuilder';

/**
 * List of League of Legends build guide website url's that are supported
 * @type {Array}
 */
config.supportedBuildSites = [
  {
    id: 'mobafire',
    hostname: 'mobafire.com'
    pathnames: [
      '/league-of-legends/build/:title',
    ]
  }
];

module.exports = config;<commit_msg>Update supported sites object<commit_after>var config = {};

/**
 * Url protocol that gets registered in operating systems
 * and used to communicate beetween browsers extensions and native application
 * @type {String}
 */
config.urlProtocol = 'lolbuilder';

/**
 * List of League of Legends build guide website url's that are supported
 * @type {Array}
 */
config.supportedBuildSites = [
  {
    hostname: 'mobafire.com'
    pathnames: [
      {
        module: 'mobafire',
        path: '/league-of-legends/build/:title'
      }
    ]
  }
];

module.exports = config;<|endoftext|>


<commit_before>/**
 * Manages a simulation of physical forces acting on bodies and springs.
 */
module.exports = physicsSimulator;

function physicsSimulator(settings) {
  var Spring = require('./lib/spring');
  var createQuadTree = require('ngraph.quadtreebh');
  var createDragForce = require('./lib/dragForce');
  var createSpringForce = require('./lib/springForce');
  var integrate = require('./lib/eulerIntegrator');
  var merge = require('ngraph.merge');

  settings = merge(settings, {
      /**
       * Ideal length for links (springs in physical model).
       */
      springLength: 80,

      /**
       * Hook's law coefficient. 1 - solid spring.
       */
      springCoeff: 0.0002,

      /**
       * Coulomb's law coefficient. It's used to repel nodes thus should be negative
       * if you make it positive nodes start attract each other :).
       */
      gravity: -1.2,

      /**
       * Theta coeffiecient from Barnes Hut simulation. Ranged between (0, 1).
       * The closer it's to 1 the more nodes algorithm will have to go through.
       * Setting it to one makes Barnes Hut simulation no different from
       * brute-force forces calculation (each node is considered).
       */
      theta: 0.8,

      /**
       * Drag force coefficient. Used to slow down system, thus should be less than 1.
       * The closer it is to 0 the less tight system will be.
       */
      dragCoeff: 0.02,

      /**
       * Default time step (dt) for forces integration
       */
      timeStep : 20
  });

  var bodies = [], // Bodies in this simulation.
      springs = [], // Springs in this simulation.
      quadTree = createQuadTree(),
      springForce = createSpringForce(),
      dragForce = createDragForce();

  var publicApi = {
    /**
     * Array of bodies, registered with current simulator
     *
     * Note: To add new body, use addBody() method. This property is only
     * exposed for testing/performance purposes.
     */
    bodies: bodies,

    /**
     * Performs one step of force simulation.
     *
     * @returns {Number} Total movement of the system. Calculated as:
     *   (total distance traveled by bodies)^2/(total # of bodies)
     */
    step: function () {
      // I'm reluctant to check timeStep here, since this method is going to be
      // super hot, I don't want to add more complexity to it
      accumulateForces();
      return integrate(bodies, settings.timeStep);
    },

    /**
     * Adds body to the system
     *
     * @param {ngraph.physics.primitives.Body} body physical body
     *
     * @returns {ngraph.physics.primitives.Body} added body
     */
    addBody: function (body) {
      if (!body) {
        throw new Error('Body is required');
      }
      bodies.push(body);

      return body;
    },

    /**
     * Removes body from the system
     *
     * @param {ngraph.physics.primitives.Body} body to remove
     *
     * @returns {Boolean} true if body found and removed. falsy otherwise;
     */
    removeBody: function (body) {
      if (!body) { return; }
      var idx = bodies.indexOf(body);
      if (idx > -1) {
        bodies.splice(idx, 1);
        return true;
      }
    },

    /**
     * Adds a spring to this simulation.
     *
     * @returns {Object} - a handle for a spring. If you want to later remove
     * spring pass it to removeSpring() method.
     */
    addSpring: function (body1, body2, springLength, springWeight, springCoefficient) {
      if (!body1 || !body2) {
        throw new Error('Cannot add null spring to force simulator');
      }

      if (typeof springLength !== 'number') {
        springLength = -1; // assume global configuration
      }

      var spring = new Spring(body1, body2, springLength, springCoefficient >= 0 ? springCoefficient : -1, springWeight);
      springs.push(spring);

      // TODO: could mark simulator as dirty.
      return spring;
    },

    /**
     * Removes spring from the system
     *
     * @param {Object} spring to remove. Spring is an object returned by addSpring
     *
     * @returns {Boolean} true if spring found and removed. falsy otherwise;
     */
    removeSpring: function (spring) {
      if (!spring) { return; }
      var idx = springs.indexOf(spring);
      if (idx > -1) {
        springs.splice(idx, 1);
        return true;
      }
    },

    // expose api to change simulator settings:
    springLength: function (value) {
      if (value !== undefined) {
        settings.springLength = value;
      }
    }
  }

  exposeSettings(publicApi);

  return publicApi;

  /**
   * Augment our public API with setting accessor/modifier methods
   * Each setting in the 'settings' object gets identical function name on the
   * publicly exposed API
   *
   * E.g. simulator.springLength() will return current settings.springLength
   * and simulator.springLength(20) will set current spring length to 20
   */
  function exposeSettings(target) {
    for (var key in settings) {
      if (settings.hasOwnProperty(key)) {
        target[key] = function (value) {
          if (value !== undefined) {
            settings[key] = value;
            return target;
          }
          return settings[key];
        }
      }
    }
  }

  function accumulateForces() {
    // Accumulate forces acting on bodies.
    var body,
        i = bodies.length;

    if (i) {
      // only add bodies if there the array is not empty:
      quadTree.insertBodies(bodies); // performance: O(n * log n)
      while (i--) {
        body = bodies[i];
        body.force.x = 0;
        body.force.y = 0;

        quadTree.updateBodyForce(body);
        dragForce.update(body);
      }
    }

    i = springs.length;
    while(i--) {
      springForce.update(springs[i]);
    }
  }
};
<commit_msg>Removed redundant method<commit_after>/**
 * Manages a simulation of physical forces acting on bodies and springs.
 */
module.exports = physicsSimulator;

function physicsSimulator(settings) {
  var Spring = require('./lib/spring');
  var createQuadTree = require('ngraph.quadtreebh');
  var createDragForce = require('./lib/dragForce');
  var createSpringForce = require('./lib/springForce');
  var integrate = require('./lib/eulerIntegrator');
  var merge = require('ngraph.merge');

  settings = merge(settings, {
      /**
       * Ideal length for links (springs in physical model).
       */
      springLength: 80,

      /**
       * Hook's law coefficient. 1 - solid spring.
       */
      springCoeff: 0.0002,

      /**
       * Coulomb's law coefficient. It's used to repel nodes thus should be negative
       * if you make it positive nodes start attract each other :).
       */
      gravity: -1.2,

      /**
       * Theta coeffiecient from Barnes Hut simulation. Ranged between (0, 1).
       * The closer it's to 1 the more nodes algorithm will have to go through.
       * Setting it to one makes Barnes Hut simulation no different from
       * brute-force forces calculation (each node is considered).
       */
      theta: 0.8,

      /**
       * Drag force coefficient. Used to slow down system, thus should be less than 1.
       * The closer it is to 0 the less tight system will be.
       */
      dragCoeff: 0.02,

      /**
       * Default time step (dt) for forces integration
       */
      timeStep : 20
  });

  var bodies = [], // Bodies in this simulation.
      springs = [], // Springs in this simulation.
      quadTree = createQuadTree(),
      springForce = createSpringForce(),
      dragForce = createDragForce();

  var publicApi = {
    /**
     * Array of bodies, registered with current simulator
     *
     * Note: To add new body, use addBody() method. This property is only
     * exposed for testing/performance purposes.
     */
    bodies: bodies,

    /**
     * Performs one step of force simulation.
     *
     * @returns {Number} Total movement of the system. Calculated as:
     *   (total distance traveled by bodies)^2/(total # of bodies)
     */
    step: function () {
      // I'm reluctant to check timeStep here, since this method is going to be
      // super hot, I don't want to add more complexity to it
      accumulateForces();
      return integrate(bodies, settings.timeStep);
    },

    /**
     * Adds body to the system
     *
     * @param {ngraph.physics.primitives.Body} body physical body
     *
     * @returns {ngraph.physics.primitives.Body} added body
     */
    addBody: function (body) {
      if (!body) {
        throw new Error('Body is required');
      }
      bodies.push(body);

      return body;
    },

    /**
     * Removes body from the system
     *
     * @param {ngraph.physics.primitives.Body} body to remove
     *
     * @returns {Boolean} true if body found and removed. falsy otherwise;
     */
    removeBody: function (body) {
      if (!body) { return; }
      var idx = bodies.indexOf(body);
      if (idx > -1) {
        bodies.splice(idx, 1);
        return true;
      }
    },

    /**
     * Adds a spring to this simulation.
     *
     * @returns {Object} - a handle for a spring. If you want to later remove
     * spring pass it to removeSpring() method.
     */
    addSpring: function (body1, body2, springLength, springWeight, springCoefficient) {
      if (!body1 || !body2) {
        throw new Error('Cannot add null spring to force simulator');
      }

      if (typeof springLength !== 'number') {
        springLength = -1; // assume global configuration
      }

      var spring = new Spring(body1, body2, springLength, springCoefficient >= 0 ? springCoefficient : -1, springWeight);
      springs.push(spring);

      // TODO: could mark simulator as dirty.
      return spring;
    },

    /**
     * Removes spring from the system
     *
     * @param {Object} spring to remove. Spring is an object returned by addSpring
     *
     * @returns {Boolean} true if spring found and removed. falsy otherwise;
     */
    removeSpring: function (spring) {
      if (!spring) { return; }
      var idx = springs.indexOf(spring);
      if (idx > -1) {
        springs.splice(idx, 1);
        return true;
      }
    }
  }

  exposeSettings(publicApi);

  return publicApi;

  /**
   * Augment our public API with setting accessor/modifier methods
   * Each setting in the 'settings' object gets identical function name on the
   * publicly exposed API
   *
   * E.g. simulator.springLength() will return current settings.springLength
   * and simulator.springLength(20) will set current spring length to 20
   */
  function exposeSettings(target) {
    for (var key in settings) {
      if (settings.hasOwnProperty(key)) {
        target[key] = function (value) {
          if (value !== undefined) {
            settings[key] = value;
            return target;
          }
          return settings[key];
        }
      }
    }
  }

  function accumulateForces() {
    // Accumulate forces acting on bodies.
    var body,
        i = bodies.length;

    if (i) {
      // only add bodies if there the array is not empty:
      quadTree.insertBodies(bodies); // performance: O(n * log n)
      while (i--) {
        body = bodies[i];
        body.force.x = 0;
        body.force.y = 0;

        quadTree.updateBodyForce(body);
        dragForce.update(body);
      }
    }

    i = springs.length;
    while(i--) {
      springForce.update(springs[i]);
    }
  }
};
<|endoftext|>


<commit_before>var fs = require('fs');
var redis = require('redis');
var slack = require('slack-notify')(process.env.SLACK_URL);

// ------------------

var SpotifyWebApi = require('spotify-web-api-node');
var spotifyApi = new SpotifyWebApi({
  clientId: process.env.SPOTIFY_CLIENT_ID,
  clientSecret: process.env.SPOTIFY_CLIENT_SECRET
});
var spotifyUser = process.env.SPOTIFY_USERNAME;
var spotifyPlaylistId = process.env.SPOTIFY_PLAYLIST;
var redisUrl = process.env.REDISTOGO_URL;
var runWebServer = process.env.VCAP_APP || false;

// ------------------
var lastDate;
var redisClient;

if (redisUrl) {
  var rtg = require('url').parse(redisUrl);
  redisClient = redis.createClient(rtg.port, rtg.hostname);
  redisClient.auth(rtg.auth.split(":")[1]);

  redisClient.on('error', function (err) {
    console.log("Redis - Error " + err);
  });
  redisClient.get("lastDate", function(err, value) {
    if (!err) {
      lastDate = new Date(value);
    }
  });
} else {
  fs.readFile('./last_date.txt', {encoding: 'UTF-8'}, function(err, date){
     lastDate = date ? new Date(date) : void 0;
  });
}

var start = false;
function grantClient() {
  spotifyApi.clientCredentialsGrant()
    .then(function(data) {
      console.log('Spotify - got new access token, valid for', data.body.expires_in, 'seconds');

      spotifyApi.setAccessToken(data.body.access_token);
      start = true;

      setTimeout(grantClient, data.body.expires_in*1000);
    }, function(err) {
      console.log('Spotify - Error retrieving an access token using:', process.env.SPOTIFY_CLIENT_SECRET, err);
      process.exit(1);
    });
}

function writeLastDate(date) {
  lastDate = date

  if (redisUrl) {
      redisClient.set('lastDate', date);
  } else {
     fs.writeFile('./last_date.txt', date, function() {});
  }
}

var playlistName;
var playlistUrl;
function fetchPlaylistInfo() {
  if (!start) {
    return;
  }
  console.log('Spotify - Fetch playlist info');
  spotifyApi.getPlaylist(spotifyUser, spotifyPlaylistId, {fields: 'name,external_urls.spotify'})
    .then(function(data) {
      playlistName = data.body.name;
      playlistUrl = data.body.external_urls.spotify;
    }, function(err) {
      console.log('Spotify - Error retrieving playlist info:', err);
    });
}

function fetchPlaylistTracks(offset) {
  if (!start || playlistUrl === undefined) {
    return;
  }

  if (offset === undefined) {
    offset = 0;
  }

  console.log('Playlist last known song added at:', lastDate);
  spotifyApi.getPlaylistTracks(spotifyUser, spotifyPlaylistId, { offset: offset,
      fields: 'total,items(added_by.id,added_at,track(name,artists.name,album.name))'})
    .then(function(data) {
      var date = 0;
      for (var i in data.body.tracks.items) {
        date = new Date(data.body.tracks.items[i].added_at);
        if((lastDate === undefined) || (date > lastDate)) {
          post(playlistName, playlistUrl,
            data.body.tracks.items[i].added_by ? data.body.tracks.items[i].added_by.id : 'Unknown',
            data.body.tracks.items[i].track.name,
            data.body.tracks.items[i].track.artists);
        }
      }
      if((lastDate === undefined) || (date > lastDate)) {
        console.log('Spotify - last date in playlist', date);
        writeLastDate(date);
      }
      if(data.body.total > offset + 100) {
        fetchPlaylistTracks(offset + 100)
      }
    }, function(err) {
      console.log('Spotify - Error retrieving playlist:', err);
    });
}

slack.onError = function (err) {
  console.log('Slack - Error:', err);
};

var slacker = slack.extend({
  username: 'spotify-playlist',
  icon_url: 'http://icons.iconarchive.com/icons/xenatt/the-circle/256/App-Spotify-icon.png',
  unfurl_media : false
});

function post(list_name, list_url, added_by, trackname, artists) {
  var text = 'New track added by ' + added_by + ' - *' + trackname+'* with '+artists[0].name+' in list <'+list_url+'|'+list_name+'>';
  console.log(text);
  slacker({text: text});
}

function startWebServer() {
  var http = require('http');
  var host = process.env.VCAP_APP_HOST || 'localhost';
  var port = process.env.VCAP_APP_PORT || 1337

  http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.end('No functions and features at this place...' + process.version);
  }).listen(port, null);

  console.log('Server running to provide incoming network connetion for Bluemix at http://' + host + ':' + port + '/');

  // set the current date as the initial date to avoid writing the whole song history to the slack channel
  now = new Date();
  writeLastDate(now);
}

if(runWebServer) {
  startWebServer();
}
grantClient();
setInterval(fetchPlaylistInfo, 1000 * 10);
setInterval(fetchPlaylistTracks, 1000 * 10);
<commit_msg>Fix start order<commit_after>var fs = require('fs');
var redis = require('redis');
var slack = require('slack-notify')(process.env.SLACK_URL);

// ------------------

var SpotifyWebApi = require('spotify-web-api-node');
var spotifyApi = new SpotifyWebApi({
  clientId: process.env.SPOTIFY_CLIENT_ID,
  clientSecret: process.env.SPOTIFY_CLIENT_SECRET
});
var spotifyUser = process.env.SPOTIFY_USERNAME;
var spotifyPlaylistId = process.env.SPOTIFY_PLAYLIST;
var redisUrl = process.env.REDISTOGO_URL;
var runWebServer = process.env.VCAP_APP || false;

// ------------------
var lastDate;
var redisClient;

if (redisUrl) {
  var rtg = require('url').parse(redisUrl);
  redisClient = redis.createClient(rtg.port, rtg.hostname);
  redisClient.auth(rtg.auth.split(":")[1]);

  redisClient.on('error', function (err) {
    console.log("Redis - Error " + err);
  });
  redisClient.get("lastDate", function(err, value) {
    if (!err) {
      lastDate = new Date(value);
    }
  });
} else {
  fs.readFile('./last_date.txt', {encoding: 'UTF-8'}, function(err, date){
     lastDate = date ? new Date(date) : void 0;
  });
}

var start = false;
function grantClient() {
  spotifyApi.clientCredentialsGrant()
    .then(function(data) {
      console.log('Spotify - got new access token, valid for', data.body.expires_in, 'seconds');

      spotifyApi.setAccessToken(data.body.access_token);
      start = true;

      setTimeout(grantClient, data.body.expires_in*1000);
    }, function(err) {
      console.log('Spotify - Error retrieving an access token using:', process.env.SPOTIFY_CLIENT_SECRET, err);
      process.exit(1);
    });
}

function writeLastDate(date) {
  lastDate = date

  if (redisUrl) {
      redisClient.set('lastDate', date);
  } else {
     fs.writeFile('./last_date.txt', date, function() {});
  }
}

var playlistName;
var playlistUrl;
function fetchPlaylistInfo() {
  if (!start) {
    setTimeout(fetchPlaylistInfo, 1000);
  }
  console.log('Spotify - Fetch playlist info');
  spotifyApi.getPlaylist(spotifyUser, spotifyPlaylistId, {fields: 'name,external_urls.spotify'})
    .then(function(data) {
      playlistName = data.body.name;
      playlistUrl = data.body.external_urls.spotify;
    }, function(err) {
      console.log('Spotify - Error retrieving playlist info:', err);
    });
}

function fetchPlaylistTracks(offset) {
  if (!start || playlistUrl === undefined) {
    return;
  }

  if (offset === undefined) {
    offset = 0;
  }

  console.log('Playlist last known song added at:', lastDate);
  spotifyApi.getPlaylistTracks(spotifyUser, spotifyPlaylistId, { offset: offset,
      fields: 'total,items(added_by.id,added_at,track(name,artists.name,album.name))'})
    .then(function(data) {
      console.log('Spotify - Fetched playlist with offset:', offset, "and got", data.body.total);
      var date = 0;
      for (var i in data.body.tracks.items) {
        date = new Date(data.body.tracks.items[i].added_at);
        if((lastDate === undefined) || (date > lastDate)) {
          post(playlistName, playlistUrl,
            data.body.tracks.items[i].added_by ? data.body.tracks.items[i].added_by.id : 'Unknown',
            data.body.tracks.items[i].track.name,
            data.body.tracks.items[i].track.artists);
        }
      }
      if((lastDate === undefined) || (date > lastDate)) {
        console.log('Spotify - last date in playlist', date);
        writeLastDate(date);
      }
      if(data.body.total > offset + 100) {
        fetchPlaylistTracks(offset + 100)
      }
    }, function(err) {
      console.log('Spotify - Error retrieving playlist:', err);
    });
}

slack.onError = function (err) {
  console.log('Slack - Error:', err);
};

var slacker = slack.extend({
  username: 'spotify-playlist',
  icon_url: 'http://icons.iconarchive.com/icons/xenatt/the-circle/256/App-Spotify-icon.png',
  unfurl_media : false
});

function post(list_name, list_url, added_by, trackname, artists) {
  var text = 'New track added by ' + added_by + ' - *' + trackname+'* with '+artists[0].name+' in list <'+list_url+'|'+list_name+'>';
  console.log(text);
  slacker({text: text});
}

function startWebServer() {
  var http = require('http');
  var host = process.env.VCAP_APP_HOST || 'localhost';
  var port = process.env.VCAP_APP_PORT || 1337

  http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.end('No functions and features at this place...' + process.version);
  }).listen(port, null);

  console.log('Server running to provide incoming network connetion for Bluemix at http://' + host + ':' + port + '/');

  // set the current date as the initial date to avoid writing the whole song history to the slack channel
  now = new Date();
  writeLastDate(now);
}

if(runWebServer) {
  startWebServer();
}
grantClient();
setTimeout(fetchPlaylistInfo, 1000);
setInterval(fetchPlaylistTracks, 1000 * 10);
<|endoftext|>


<commit_before>'use strict';
const config = require("./config.js");
const pokemonNames = require("./pokemon_names.js");
const EventEmitter = require("events");
const request = require('request');
const Pokespotter = require("pokespotter");
const TelegramBot = require("node-telegram-bot-api");

// 為 TelegramBot 加入 sendVenue 功能
class TelegramBotWithVenus extends TelegramBot {
	constructor(token, options = {}) {
		super(token, options);
	}

	sendVenue(chatId, latitude, longitude, title, address, form = {}) {
		form.chat_id = chatId;
		form.latitude = latitude;
		form.longitude = longitude;
		form.title = title;
		form.address = address;
		return this._request('sendVenue', { form });
	}
}

const event = new EventEmitter();
var telegramBot = new TelegramBotWithVenus(config.telegramBotToken, { polling: true });
var pokespotter = Pokespotter(config.account);

var telegramAdminUsernames = config.telegramAdminUsernames;	// 管理員名單
var blacklist = config.blacklist;	// 寶可夢黑名單
var centerLocation = config.initCenterLocation;	// 搜尋中心位置
var spotterOptional = {
	steps: config.searchSteps,			// 搜尋範圍
	requestDelay: config.searchDelay,	// 搜尋延遲
	currentTime: Date.now()
}

var isPatrolling = false;	// 巡邏執行狀態
var pokemons = [];			// 儲存的寶可夢
var activeChatIDs = [];		// 啟動中的 Telegram ChatID

// 巡邏
event.on("patrol", function() {
	// 執行 pokespotter 尋找附近寶可夢
	spotterOptional.currentTime = Date.now();
	console.log("------------------------- 開始巡邏 " + getHHMMSS(spotterOptional.currentTime) + " -------------------------");
	pokespotter.get(centerLocation, spotterOptional).then(function(nearbyPokemons) {

		//console.log("找到 #", np.pokemonId, pokemonNames[np.pokemonId], np.spawnPointId, getHHMMSS(np.expirationTime));
		var newPokemonCount = 0;
		// 比對每隻發現的將新發現的寶可夢儲存至 pokemons
		nearbyPokemons.forEach(function(np) {
			var isNeedSave = true;

			if (blacklist.indexOf(np.pokemonId) >= 0) {
				// 找到黑名單，不儲存
				isNeedSave = false;
			} else {
				// 不在黑名單中，尋找是否已儲存
				pokemons.forEach(function(p) {
					if (np.spawnPointId == p.spawnPointId && np.pokemonId == p.pokemonId) {
						// 已存在，不儲存
						isNeedSave = false;
					}
				});
			}

			if (isNeedSave) {
				np.isInformed = false;
				pokemons.push(np);
				console.log("發現 #", np.pokemonId, pokemonNames[np.pokemonId], np.spawnPointId, getHHMMSS(np.expirationTime));
				newPokemonCount++;
			}
		});

		console.log("本次巡邏發現 " + nearbyPokemons.length + " 隻，新增 " + newPokemonCount + " 隻");

		// 檢查 pokemons 中的每隻寶可夢剩餘時間
		event.emit("checkLastTime");
	}).catch(function(err) {
		console.error("錯誤，將在 10 秒後重新啟動");
		console.error(err);
		pokespotter = null;

		setTimeout(function() {
			pokespotter = Pokespotter(config.account);
			event.emit("patrol");
		}, 10000);
	});
});

// 檢查 pokemons 中的每隻寶可夢剩餘時間，若未到期且尚未通知則執行通知，若到期則刪除
event.on("checkLastTime", function() {
	for (var i = pokemons.length - 1; i >= 0; i--) {
		var lastTime = getLastTime(pokemons[i].expirationTime);
		if (lastTime > 0) {
			// 尚未結束，確認是否未通知
			if (!pokemons[i].isInformed) {
				// 尚未通知，執行通知
				event.emit("informToActiveUsers", pokemons[i], lastTime);
				pokemons[i].isInformed = true;
			}
		} else {
			// 已結束，刪除
			pokemons.splice(i, 1);
		}
	}

	// 判斷是否還有人在使用，有的話繼續下一次巡邏，否則不再觸發巡邏
	doNextPatrol();
});

// 將寶可夢通知給所有啟動中的使用者
event.on("informToActiveUsers", function(pokemon, lastTime) {
	for (var i = 0; i < activeChatIDs.length; i++) {
		sendPokemon(activeChatIDs[i], pokemon, lastTime);
	}
});

// 畫地圖，傳給下指令者
event.on("getmap", function(chatId) {
	if (pokemons.length > 0) {
		telegramBot.sendMessage(chatId, "地圖製作中，請稍候...");
		var mapUrl = Pokespotter.getMapsUrl(centerLocation, pokemons, "512x512");

		// 將地圖圖檔下載傳給使用者
		request({url:mapUrl, encoding:null}, function (error, response, body) {
			if (!error && response.statusCode == 200) {
				var imageBuffer = Buffer.from(body);
				// 將地圖傳給使用者
				telegramBot.sendPhoto(chatId, imageBuffer);

				var message = "";
				var index = 1;
				pokemons.forEach(function(p) {
					var lastTime = getLastTime(p.expirationTime);
					if (lastTime > 0) {
						message = message + "[" + index + "] #" + p.pokemonId + " " + pokemonNames[p.pokemonId] +
						" 剩餘:" + getMMSS(lastTime) + " 結束:" + getHHMMSS(p.expirationTime) + "\n";
					} else {
						message = message + "[" + index + "] #" + p.pokemonId + " " + pokemonNames[p.pokemonId] +
						" 已結束:" + getHHMMSS(p.expirationTime) + "\n";
					}

					index++;
				});

				// 傳送寶可夢編號和資訊
				telegramBot.sendMessage(chatId, message);
			} else {
				// 請求失敗
				telegramBot.sendMessage(chatId, "地圖圖檔請求失敗(狀態：" + response.statusCode + ")");
				console.log(error);
			}
		});
	} else {
		telegramBot.sendMessage(chatId, "目前無資料");
	}
});

// 檢查是否為頻道模式
const channelID = config.telegramChannelID;
if (channelID != null) {
	console.log("廣播模式啟動");
	// 將頻道ID存入 activeChatIDs
	activeChatIDs = [channelID];
	// 觸發第一次巡邏
	event.emit("patrol");
	// 更改執行狀態
	isPatrolling = true;
} else {
	console.log("機器人模式啟動，請在 Telegram 聊天中傳送指令");
	// Bot 收到訊息，處理指令
	telegramBot.on("message", function(msg) {
		var chatId = msg.chat.id;
		var isAdmin = telegramAdminUsernames.indexOf(msg.from.username) >= 0;
		var isInActiveChatID = activeChatIDs.indexOf(chatId) >= 0;
		var command = "";
		if (typeof msg.text !== "undefined") {
			command = msg.text.split("@")[0];
			// 發送說明
			if (command == "/help") {
				telegramBot.sendMessage(
					chatId,
					"說明：\n" +
					"以指定的位置為中心進行巡邏（範圍約半徑100多公尺），將遇到的寶可夢通知給使用者\n" +
					"一般指令：\n" +
					"/getmap 取得附近寶可夢地圖\n\n" +
					"管理員專用：\n" +
					"/help 查看說明\n" +
					"/start 開始巡邏\n" +
					"/stop 停止巡邏\n" +
					"傳送位置訊息可更改巡邏中心位置"
				);
			}

			// 登錄chatId，若巡邏為執行則觸發巡邏
			if (command == "/start" && isAdmin) {
				// 若 chatId 不在清單中，加進去
				if (!isInActiveChatID) {
					activeChatIDs.push(chatId);
					console.log("新增使用者：" + chatId + "，目前使用者：" + activeChatIDs);
					telegramBot.sendMessage(chatId, "管理員啟動通知");
				}

				// 若原本沒在執行中，觸發巡邏並更改執行狀態
				if (!isPatrolling) {
					// 觸發第一次巡邏
					event.emit("patrol");
					// 更改執行狀態
					isPatrolling = true;
					telegramBot.sendMessage(chatId, "開使巡邏");
				} else {
					// 本來就在巡邏了
					telegramBot.sendMessage(chatId, "巡邏進行中");
				}
			}

			// 從清單中移除chatId，該chatId將不再被通知
			if (command == "/stop" && isAdmin) {
				// 從 activeChatIDs 中移除
				var index = activeChatIDs.indexOf(chatId);
				if (index >= 0) {
					activeChatIDs.splice(index, 1);
				}

				telegramBot.sendMessage(chatId, "管理員停止通知");
			}

			// 判斷有在ActiveChatID才能使用，才不會被路人亂加機器人好用亂用
			if (command == "/getmap" && isInActiveChatID) {
				// 取得附近寶可夢的地圖圖檔
				event.emit("getmap", chatId);
			}
		}
	});

	telegramBot.on("location", function(msg) {
		// 判斷是否為管理員
		if (telegramAdminUsernames.indexOf(msg.from.username) >= 0) {
			// 更改座標
			centerLocation = msg.location;
			// 通知
			telegramBot.sendMessage(msg.chat.id, "已更改巡邏中心位置");
		}
	});
}

function sendPokemon(chatId, pokemon, lastTime) {
	telegramBot.sendVenue(
		chatId,
		pokemon.latitude,
		pokemon.longitude,
		"#" + pokemon.pokemonId + " " + pokemonNames[pokemon.pokemonId] + " " + pokemon.distance + "m",
		"剩餘 " + getMMSS(lastTime) + " 結束 " + getHHMMSS(pokemon.expirationTime)
	);
}

// 判斷是否還有人在使用，有的話繼續下一次巡邏，否則不在觸發巡邏
function doNextPatrol() {
	if (activeChatIDs.length > 0) {
		// 觸發下一次巡邏
		event.emit("patrol");
	} else {
		// 更改執行狀態
		isPatrolling = false;
		console.log("無使用者，已停止巡邏");
	}
}

// 取得剩餘時間 timestamps，若為負數表示已結束
function getLastTime(endTime) {
	return endTime - Date.now();
}

// 取得 時:分:秒
function getHHMMSS(time) {
	var date = new Date(time);
	var hours = date.getHours();
	var minutes = "0" + date.getMinutes();
	var seconds = "0" + date.getSeconds();
	return hours + ':' + minutes.substr(-2) + ':' + seconds.substr(-2);
}

// 取得 分:秒
function getMMSS(time) {
	var date = new Date(time);
	var minutes = date.getMinutes();
	var seconds = "0" + date.getSeconds();
	return minutes + ':' + seconds.substr(-2);
}
<commit_msg>建立新的重新起動機制<commit_after>'use strict';
const config = require("./config.js");
const pokemonNames = require("./pokemon_names.js");
const EventEmitter = require("events");
const request = require('request');
const Pokespotter = require("pokespotter");
const TelegramBot = require("node-telegram-bot-api");

// 為 TelegramBot 加入 sendVenue 功能
class TelegramBotWithVenus extends TelegramBot {
	constructor(token, options = {}) {
		super(token, options);
	}

	sendVenue(chatId, latitude, longitude, title, address, form = {}) {
		form.chat_id = chatId;
		form.latitude = latitude;
		form.longitude = longitude;
		form.title = title;
		form.address = address;
		return this._request('sendVenue', { form });
	}
}

const event = new EventEmitter();
var telegramBot = new TelegramBotWithVenus(config.telegramBotToken, { polling: true });
var pokespotters = [];	// 儲存 Spotter 用
// 建立第一個 Spotter
pokespotters[0] = Pokespotter(config.account);
var runningSpotterId = 0;
var isWattingRestart = false;

var telegramAdminUsernames = config.telegramAdminUsernames;	// 管理員名單
var blacklist = config.blacklist;	// 寶可夢黑名單
var centerLocation = config.initCenterLocation;	// 搜尋中心位置
var spotterOptional = {
	steps: config.searchSteps,			// 搜尋範圍
	requestDelay: config.searchDelay,	// 搜尋延遲
	currentTime: Date.now()
}

var isPatrolling = false;	// 巡邏執行狀態
var pokemons = [];			// 儲存的寶可夢
var activeChatIDs = [];		// 啟動中的 Telegram ChatID

// 巡邏
event.on("patrol", function(thisSpotterId) {

	// 執行 spotter 尋找附近寶可夢
	spotterOptional.currentTime = Date.now();
	console.log("------------------------- 開始巡邏 " + getHHMMSS(spotterOptional.currentTime) + " -------------------------");
	pokespotters[runningSpotterId].get(centerLocation, spotterOptional).then(function(nearbyPokemons) {

		//console.log("找到 #", np.pokemonId, pokemonNames[np.pokemonId], np.spawnPointId, getHHMMSS(np.expirationTime));
		var newPokemonCount = 0;
		// 比對每隻發現的將新發現的寶可夢儲存至 pokemons
		nearbyPokemons.forEach(function(np) {
			var isNeedSave = true;

			if (blacklist.indexOf(np.pokemonId) >= 0) {
				// 找到黑名單，不儲存
				isNeedSave = false;
			} else {
				// 不在黑名單中，尋找是否已儲存
				pokemons.forEach(function(p) {
					if (np.spawnPointId == p.spawnPointId && np.pokemonId == p.pokemonId) {
						// 已存在，不儲存
						isNeedSave = false;
					}
				});
			}

			if (isNeedSave) {
				np.isInformed = false;
				pokemons.push(np);
				console.log("發現 #", np.pokemonId, pokemonNames[np.pokemonId], np.spawnPointId, getHHMMSS(np.expirationTime));
				newPokemonCount++;
			}
		});

		// 確認 Spotter 沒死才送訊息
		if (thisSpotterId == runningSpotterId) {
			console.log("本次巡邏發現 " + nearbyPokemons.length + " 隻，新增 " + newPokemonCount + " 隻");
		}

		// 檢查 pokemons 中的每隻寶可夢剩餘時間
		event.emit("checkLastTime", thisSpotterId);

	}).catch(function(err) {
		console.error("@@錯誤@@");
		console.error(err);

		// 不在重啟中狀態，可以重新啟動。若使已再重啟就不用管了等他啟動就好
		if (!isWattingRestart) {
			restart();
		}
	});
});

// 檢查 pokemons 中的每隻寶可夢剩餘時間，若未到期且尚未通知則執行通知，若到期則刪除
event.on("checkLastTime", function(thisSpotterId) {
	// 檢查ID正確才繼續跑，否則代表這個 Spotter 已經死了
	if (thisSpotterId == runningSpotterId) {
		for (var i = pokemons.length - 1; i >= 0; i--) {
		var lastTime = getLastTime(pokemons[i].expirationTime);
		if (lastTime > 0) {
			// 尚未結束，確認是否未通知
			if (!pokemons[i].isInformed) {
				// 尚未通知，執行通知
				event.emit("informToActiveUsers", pokemons[i], lastTime);
				pokemons[i].isInformed = true;
			}
		} else {
			// 已結束，刪除
			pokemons.splice(i, 1);
		}
	}

	// 判斷是否還有人在使用，有的話繼續下一次巡邏，否則不再觸發巡邏
	doNextPatrol(thisSpotterId);
	}
});

// 將寶可夢通知給所有啟動中的使用者
event.on("informToActiveUsers", function(pokemon, lastTime) {
	for (var i = 0; i < activeChatIDs.length; i++) {
		sendPokemon(activeChatIDs[i], pokemon, lastTime);
	}
});

// 畫地圖，傳給下指令者
event.on("getmap", function(chatId) {
	if (pokemons.length > 0) {
		telegramBot.sendMessage(chatId, "地圖製作中，請稍候...");
		var mapUrl = Pokespotter.getMapsUrl(centerLocation, pokemons, "512x512");

		// 將地圖圖檔下載傳給使用者
		request({url:mapUrl, encoding:null}, function (error, response, body) {
			if (!error && response.statusCode == 200) {
				var imageBuffer = Buffer.from(body);
				// 將地圖傳給使用者
				telegramBot.sendPhoto(chatId, imageBuffer);

				var message = "";
				var index = 1;
				pokemons.forEach(function(p) {
					var lastTime = getLastTime(p.expirationTime);
					if (lastTime > 0) {
						message = message + "[" + index + "] #" + p.pokemonId + " " + pokemonNames[p.pokemonId] +
						" 剩餘:" + getMMSS(lastTime) + " 結束:" + getHHMMSS(p.expirationTime) + "\n";
					} else {
						message = message + "[" + index + "] #" + p.pokemonId + " " + pokemonNames[p.pokemonId] +
						" 已結束:" + getHHMMSS(p.expirationTime) + "\n";
					}

					index++;
				});

				// 傳送寶可夢編號和資訊
				telegramBot.sendMessage(chatId, message);
			} else {
				// 請求失敗
				telegramBot.sendMessage(chatId, "地圖圖檔請求失敗(狀態：" + response.statusCode + ")");
				console.log(error);
			}
		});
	} else {
		telegramBot.sendMessage(chatId, "目前無資料");
	}
});

// 檢查是否為頻道模式
const channelID = config.telegramChannelID;
if (channelID != null) {
	console.log("廣播模式啟動");
	// 將頻道ID存入 activeChatIDs
	activeChatIDs = [channelID];
	// 觸發第一次巡邏
	event.emit("patrol");
	// 更改執行狀態
	isPatrolling = true;
} else {
	console.log("機器人模式啟動，請在 Telegram 聊天中傳送指令");
	// Bot 收到訊息，處理指令
	telegramBot.on("message", function(msg) {
		var chatId = msg.chat.id;
		var isAdmin = telegramAdminUsernames.indexOf(msg.from.username) >= 0;
		var isInActiveChatID = activeChatIDs.indexOf(chatId) >= 0;
		var command = "";
		if (typeof msg.text !== "undefined") {
			command = msg.text.split("@")[0];
			// 發送說明
			if (command == "/help") {
				telegramBot.sendMessage(
					chatId,
					"說明：\n" +
					"以指定的位置為中心進行巡邏（範圍約半徑100多公尺），將遇到的寶可夢通知給使用者\n" +
					"一般指令：\n" +
					"/getmap 取得附近寶可夢地圖\n\n" +
					"管理員專用：\n" +
					"/help 查看說明\n" +
					"/start 開始巡邏\n" +
					"/stop 停止巡邏\n" +
					"傳送位置訊息可更改巡邏中心位置"
				);
			}

			// 登錄chatId，若巡邏為執行則觸發巡邏
			if (command == "/start" && isAdmin) {
				// 若 chatId 不在清單中，加進去
				if (!isInActiveChatID) {
					activeChatIDs.push(chatId);
					console.log("新增使用者：" + chatId + "，目前使用者：" + activeChatIDs);
					telegramBot.sendMessage(chatId, "管理員啟動通知");
				}

				// 若原本沒在執行中，觸發巡邏並更改執行狀態
				if (!isPatrolling) {
					// 觸發第一次巡邏
					event.emit("patrol");
					// 更改執行狀態
					isPatrolling = true;
					telegramBot.sendMessage(chatId, "開使巡邏");
				} else {
					// 本來就在巡邏了
					telegramBot.sendMessage(chatId, "巡邏進行中");
				}
			}

			// 從清單中移除chatId，該chatId將不再被通知
			if (command == "/stop" && isAdmin) {
				// 從 activeChatIDs 中移除
				var index = activeChatIDs.indexOf(chatId);
				if (index >= 0) {
					activeChatIDs.splice(index, 1);
				}

				telegramBot.sendMessage(chatId, "管理員停止通知");
			}

			// 判斷有在ActiveChatID才能使用，才不會被路人亂加機器人好用亂用
			if (command == "/getmap" && isInActiveChatID) {
				// 取得附近寶可夢的地圖圖檔
				event.emit("getmap", chatId);
			}

			// 強制重啟
			if (command == "/restart" && isAdmin) {
				if (isWattingRestart) {
					// 已經再重啟中了
					telegramBot.sendMessage(chatId, "正在重啟中");
				} else {
					// 不在重啟中狀態，可以重新啟動
					restart();
				}
			}
		}
	});

	telegramBot.on("location", function(msg) {
		// 判斷是否為管理員
		if (telegramAdminUsernames.indexOf(msg.from.username) >= 0) {
			// 更改座標
			centerLocation = msg.location;
			// 通知
			telegramBot.sendMessage(msg.chat.id, "已更改巡邏中心位置");
		}
	});
}

function sendPokemon(chatId, pokemon, lastTime) {
	telegramBot.sendVenue(
		chatId,
		pokemon.latitude,
		pokemon.longitude,
		"#" + pokemon.pokemonId + " " + pokemonNames[pokemon.pokemonId] + " " + pokemon.distance + "m",
		"剩餘 " + getMMSS(lastTime) + " 結束 " + getHHMMSS(pokemon.expirationTime)
	);
}

// 判斷是否還有人在使用，有的話繼續下一次巡邏，否則不在觸發巡邏
function doNextPatrol(thisSpotterId) {
	// 確認還有人在用
	if (activeChatIDs.length > 0) {
		if (thisSpotterId == runningSpotterId) {
			// Spotter 沒死掉 觸發下一次巡邏
			event.emit("patrol", runningSpotterId);
		} else {
			console.log("thisSpotterId = " + thisSpotterId);
			console.log("runningSpotterId = " + runningSpotterId);
		}
	} else {
		// 更改執行狀態
		isPatrolling = false;
		console.log("無使用者，已停止巡邏");
	}
}

// 遇到錯誤或呼叫 /restart 時使用
function restart() {
	isWattingRestart = true;	// 設為重啟中狀態，避免重複呼叫指令造成多個 Spotter 被啟動
	runningSpotterId = runningSpotterId + 1;	// 更新執行中的 Spotter Id
	pokespotters[runningSpotterId] = null;		// 捨棄舊的 Spotter

	console.log("10秒後重新開使巡邏");
	setTimeout(function() {
		// 建立下一個 Spotter
		pokespotters[runningSpotterId] = Pokespotter(config.account);
		// 觸發巡邏
		event.emit("patrol", runningSpotterId);
		isWattingRestart = false;	// 取消啟動中狀態
	}, 10000);
}

// 取得剩餘時間 timestamps，若為負數表示已結束
function getLastTime(endTime) {
	return endTime - Date.now();
}

// 取得 時:分:秒
function getHHMMSS(time) {
	var date = new Date(time);
	var hours = date.getHours();
	var minutes = "0" + date.getMinutes();
	var seconds = "0" + date.getSeconds();
	return hours + ':' + minutes.substr(-2) + ':' + seconds.substr(-2);
}

// 取得 分:秒
function getMMSS(time) {
	var date = new Date(time);
	var minutes = date.getMinutes();
	var seconds = "0" + date.getSeconds();
	return minutes + ':' + seconds.substr(-2);
}
<|endoftext|>


<commit_before>var q = require('q'),
    path = require('path'),
    glob = require('glob'),
    debug = require('debug')('protractor-cucumber-framework'),
    Cucumber = require('cucumber'),
    state = require('./lib/runState');

/**
 * Execute the Runner's test cases through Cucumber.
 *
 * @param {Runner} runner The current Protractor Runner.
 * @param {Array} specs Array of Directory Path Strings.
 * @return {q.Promise} Promise resolved with the test results
 */
exports.run = function(runner, specs) {
  var results = {}

  return runner.runTestPreparer().then(function() {
    var opts = runner.getConfig().cucumberOpts;
    state.initialize(runner, results, opts.strict);

    return q.promise(function(resolve, reject) {
      var cliArguments = convertOptionsToCliArguments(opts);
      cliArguments.push('--require', path.resolve(__dirname, 'lib', 'resultsCapturer.js'));
      cliArguments = cliArguments.concat(specs);

      debug('cucumber command: "' + cliArguments.join(' ') + '"');

      Cucumber.Cli(cliArguments).run(function (isSuccessful) {
        try {
          if (runner.getConfig().onComplete) {
            runner.getConfig().onComplete();
          }
          resolve(results);
        } catch (err) {
          reject(err);
        }
      });
    });
  });

  function convertOptionsToCliArguments(options) {
    var cliArguments = ['node', 'cucumberjs'];

    for (var option in options) {
      var cliArgumentValues = convertOptionValueToCliValues(option, options[option]);

      if (Array.isArray(cliArgumentValues)) {
        cliArgumentValues.forEach(function (value) {
          cliArguments.push('--' + option, value);
        });
      } else if (cliArgumentValues) {
        cliArguments.push('--' + option);
      }
    }

    return cliArguments;
  }

  function convertRequireOptionValuesToCliValues(values) {
    var configDir = runner.getConfig().configDir;

    return toArray(values).map(function(path) {
      // Handle glob matching
      return glob.sync(path, {cwd: configDir});
    }).reduce(function(opts, globPaths) {
      // Combine paths into flattened array
      return opts.concat(globPaths);
    }, []).map(function(requirePath) {
      // Resolve require absolute path
      return path.resolve(configDir, requirePath)
    }).filter(function(item, pos, orig) {
      // Make sure requires are unique
      return orig.indexOf(item) == pos;
    });
  }

  function convertGenericOptionValuesToCliValues(values) {
    if (values === true || !values) {
      return values;
    } else {
      return toArray(values);
    }
  }

  function convertOptionValueToCliValues(option, values) {
    if (option === 'require') {
      return convertRequireOptionValuesToCliValues(values);
    } else {
      return convertGenericOptionValuesToCliValues(values);
    }
  }

  function toArray(values) {
    return Array.isArray(values) ? values : [values];
  }
};
<commit_msg>Allow config.onComplete to return a promise.<commit_after>var q = require('q'),
    path = require('path'),
    glob = require('glob'),
    debug = require('debug')('protractor-cucumber-framework'),
    Cucumber = require('cucumber'),
    state = require('./lib/runState');

/**
 * Execute the Runner's test cases through Cucumber.
 *
 * @param {Runner} runner The current Protractor Runner.
 * @param {Array} specs Array of Directory Path Strings.
 * @return {q.Promise} Promise resolved with the test results
 */
exports.run = function(runner, specs) {
  var results = {}

  return runner.runTestPreparer().then(function() {
    var opts = runner.getConfig().cucumberOpts;
    state.initialize(runner, results, opts.strict);

    return q.promise(function(resolve, reject) {
      var cliArguments = convertOptionsToCliArguments(opts);
      cliArguments.push('--require', path.resolve(__dirname, 'lib', 'resultsCapturer.js'));
      cliArguments = cliArguments.concat(specs);

      debug('cucumber command: "' + cliArguments.join(' ') + '"');

      Cucumber.Cli(cliArguments).run(function (isSuccessful) {
        try {
          var complete = q();
          if (runner.getConfig().onComplete) {
            complete = q(runner.getConfig().onComplete());
          }
          complete.then(function() {
            resolve(results);
          });
        } catch (err) {
          reject(err);
        }
      });
    });
  });

  function convertOptionsToCliArguments(options) {
    var cliArguments = ['node', 'cucumberjs'];

    for (var option in options) {
      var cliArgumentValues = convertOptionValueToCliValues(option, options[option]);

      if (Array.isArray(cliArgumentValues)) {
        cliArgumentValues.forEach(function (value) {
          cliArguments.push('--' + option, value);
        });
      } else if (cliArgumentValues) {
        cliArguments.push('--' + option);
      }
    }

    return cliArguments;
  }

  function convertRequireOptionValuesToCliValues(values) {
    var configDir = runner.getConfig().configDir;

    return toArray(values).map(function(path) {
      // Handle glob matching
      return glob.sync(path, {cwd: configDir});
    }).reduce(function(opts, globPaths) {
      // Combine paths into flattened array
      return opts.concat(globPaths);
    }, []).map(function(requirePath) {
      // Resolve require absolute path
      return path.resolve(configDir, requirePath)
    }).filter(function(item, pos, orig) {
      // Make sure requires are unique
      return orig.indexOf(item) == pos;
    });
  }

  function convertGenericOptionValuesToCliValues(values) {
    if (values === true || !values) {
      return values;
    } else {
      return toArray(values);
    }
  }

  function convertOptionValueToCliValues(option, values) {
    if (option === 'require') {
      return convertRequireOptionValuesToCliValues(values);
    } else {
      return convertGenericOptionValuesToCliValues(values);
    }
  }

  function toArray(values) {
    return Array.isArray(values) ? values : [values];
  }
};
<|endoftext|>


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

const git = require('./git')
const inquirer = require('inquirer')
const meow = require('meow')

const cli = meow(
  `
    Usage
      $ change-branch

    Options
      -a, --all   Show all branches, even remotes
      -h, --help  Display this help and exit
  `,
  {
    alias: {
      a: 'all',
      h: 'help',
    },
  }
)

const remotesRegex = /^remotes\//

const getChoices = branches => {
  if (cli.flags.all) {
    return branches.map(branch => branch.replace(remotesRegex, ''))
  }

  return branches.filter(branch => !branch.match(remotesRegex))
}

const repo = git.openRepo()

repo.getBranches(repo)
  .then(result => {
    const choices = getChoices(result.all)
    const currentIndex = choices.indexOf(result.current)
    const defaultChoice = Math.max(currentIndex, 0)

    return inquirer.prompt([{
      choices,
      default: defaultChoice,
      message: 'Choose a branch',
      name: 'branch',
      type: 'list',
    }])
  })
  .then(({ branch }) => {
    return repo.checkout(branch)
  })
  .catch(error => {
    console.error('An error occured')
    console.error(error)
    process.exit(1)
  })
<commit_msg>Sets up update-notifier<commit_after>#!/usr/bin/env node

const git = require('./git')
const inquirer = require('inquirer')
const meow = require('meow')
const pkg = require('./package.json')
const updateNotifier = require('update-notifier')

updateNotifier({ pkg })
  .notify({ defer: false })

const cli = meow(
  `
    Usage
      $ change-branch

    Options
      -a, --all   Show all branches, even remotes
      -h, --help  Display this help and exit
  `,
  {
    alias: {
      a: 'all',
      h: 'help',
    },
  }
)

const remotesRegex = /^remotes\//

const getChoices = branches => {
  if (cli.flags.all) {
    return branches.map(branch => branch.replace(remotesRegex, ''))
  }

  return branches.filter(branch => !branch.match(remotesRegex))
}

const repo = git.openRepo()

repo.getBranches(repo)
  .then(result => {
    const choices = getChoices(result.all)
    const currentIndex = choices.indexOf(result.current)
    const defaultChoice = Math.max(currentIndex, 0)

    return inquirer.prompt([{
      choices,
      default: defaultChoice,
      message: 'Choose a branch',
      name: 'branch',
      type: 'list',
    }])
  })
  .then(({ branch }) => {
    return repo.checkout(branch)
  })
  .catch(error => {
    console.error('An error occured')
    console.error(error)
    process.exit(1)
  })
<|endoftext|>


<commit_before>'use strict';

var $ = require('jquery-deferred');
var _ = require('underscore');
var Imgur = require('./Imgur');

/**
 * Parses HipChat requests.
 * Promises resolve with HipChat response objects,
 * and fail with HipChat response objects.
 * @see {@link https://www.hipchat.com/docs/apiv2/webhooks}
 * @constructor
 */
function HipChatBot () {};

/**
 * start every error with an apology...
 * @type {string}
 */
HipChatBot.ERROR_ROOT = 'Sorry, {n}. ';

/**
 * error message for empty search result.
 * @type {string}
 */
HipChatBot.ERROR_NO_RESULTS = HipChatBot.ERROR_ROOT +
    'I couldn\'t find anything with the query: "{q}". I suck.';

/**
 * error message for 500.
 * @type {string}
 */
HipChatBot.ERROR_500 = HipChatBot.ERROR_ROOT +
    'Something\'s borked. Try again later...';

/**
 * error message for 500.
 * @type {string}
 */
HipChatBot.ERROR_BAD_HOOK = HipChatBot.ERROR_ROOT +
    'Something\'s borked. HipChat data looks funny.';

/**
 * Generic request Parser for HipChat WebHooks
 * Override this in subclasses.
 * Returns a promise that resolves with a generic HipChat message
 * Should reject with an error in subclassed implementations.
 * @param {Object} reqData - the HipChat request body
 * @returns {JqueryDeferred}
 */
HipChatBot.prototype.parseReq = function (reqData) {

    var _this = this;
    var def = $.Deferred();
    var sender = this.getSenderHandle(reqData);
    var message = this.getMessageText(reqData);

    if (!sender || !message) {
        def.reject(
            _this.buildResponse(_this.getBadHookMsg(sender), 'red')
        );
    } else {
        def.resolve(
            _this.buildResponse('oh hey, ' + sender + '. it\'s a generic bot.')
        );
    }

    return def.promise();
};

/**
 * Parses a chat message to return gif based on text after the slug
 * Returns a promise that resolves with a HipChat message consisting of a link to an Imgur GIF.
 * Promise rejects with a HipChat message object containing an error indicating no GIFs were found.
 * @param {Object} reqData - the HipChat request body
 * @param {String} slug - optional, the slug to remove from the message, default is "/gif"
 * @returns {JqueryDeferred}
 *
 */
HipChatBot.prototype.parseGifReq = function (reqData, slug) {

    var msg = this.getMessageText(reqData);
    var _this = this;
    var query;

    if (msg) {
        query = this.stripSlug(msg, slug) + Imgur.EXT_GIF;
        return this.findImg(query, reqData);
    }
    return $.Deferred().reject(
        _this.buildResponse(_this.getBadHookMsg(), 'red')
    ).promise();
};

/**
 * Searches Imgur.com for an image, returning a promise.
 * The promise resolves with a HipChat response message containing a link to an Imgur image.
 * The promise rejects with a HipChat error response if no images could be found.
 * @param {String} query - what you're searching for
 * @param {Object} reqData - the HipChat request body
 * @returns {JqueryDeferred}
 */
HipChatBot.prototype.findImg = function (query, reqData) {

    var _this = this;
    var imgur = new Imgur(process.env.IMGUR_ID);
    var def = $.Deferred();
    var handle = this.getSenderHandle(reqData);
    var errorMsg;

    imgur.getRandomFromSearch(encodeURIComponent(query))
        .done(function (resp) {
            def.resolve(_this.buildResponse(resp.link));
        })
        .fail(function (resp) {
            switch (resp.status) {
                case 500 :
                    errorMsg = _this.get500Msg(handle);
                    break;
                case 200 :
                    errorMsg = _this.getNoResultsMsg(handle, resp.data.query);
                    break;
                default :
                    errorMsg = _this.getCustomErrorMsg(handle, resp.data.error);
                    break;
            }
            def.reject(_this.buildResponse(errorMsg, 'red'));
        });

    return def.promise();
};

/**
 * Builds a response object to be sent to HipChat.
 * @param {String} message
 * @param {String} color - optional, defaults to green
 * @param {Boolean} notify - optional, defaults to false
 * @returns {Object} response
 * @returns {String} response.color
 * @returns {String} response.message
 * @returns {String} response.message_format
 * @returns {Boolean} response.notify
 */
HipChatBot.prototype.buildResponse = function (message, color, notify, format) {
    return {
        color: color || 'green',
        message: message,
        'message_format': format || 'text',
        notify: !!notify
    }
};

/**
 * Remove the slug from a HipChat message string, returning the message.
 * @param {String} msg - the string of a HipChat message
 * @param {String} slug - the hook/slug of the message, ex: '/gif'
 * @returns {String}
 */
HipChatBot.prototype.stripSlug = function (msg, slug) {
    return _.last(msg.split(slug)).trim();
};

/**
 * Retrieves a sender handle from a HipChat request object.
 * @param {object} reqData - HipChat request body
 * @returns {string} - an appropriate recipient name for personalized responses
 */
HipChatBot.prototype.getSenderHandle = function (reqData) {
    var msg = this.getMessage(reqData);
    if (msg.from && _.isString(msg.from.name)) {
        return msg.from.name.split(' ')[0];
    }
    return null;
};

/**
 * finds the message object in the req body
 * @param {Object} reqData - hip chat request body
 * @returns {Object} - message object from a HipChat request
 */
HipChatBot.prototype.getMessage = function (reqData) {
    if (reqData && reqData.item) {
        return reqData.item.message || {};
    }
    return {};
};

/**
 * Finds the message string in the req body.
 * @param {Object} reqData - hip chat request body
 * @returns {String} - message string from a HipChat request
 */
HipChatBot.prototype.getMessageText = function (reqData) {
    return this.getMessage(reqData).message;
};

/**
 * Finds the message string in the req body, removes the slug and separates it into lower case fragments
 * @param {Object} reqData - hip chat request body
 * @param {String} slug - optional, the slug to be removed from the message
 * @returns {Array}
 */
HipChatBot.prototype.getMessageExploded = function (reqData, slug) {
    var message = this.getMessageText(reqData);
    message = this.stripSlug(message, slug).toLowerCase();
    return message.split(' ');
};

/**
 * Error message for no results found.
 * @param {String} sender - the user's first name or handle
 * @param {String} query
 * @returns {String}
 */
HipChatBot.prototype.getNoResultsMsg = function (sender, query) {
    return HipChatBot.ERROR_NO_RESULTS
        .replace('{n}', sender)
        .replace('{q}', unescape(query).replace(Imgur.EXT_GIF, ''));
};

/**
 * Error message for missing or malformed WebHook data from HipChat
 * @param {string} sender - the user's first name or handle
 * @returns {String}
 */
HipChatBot.prototype.getBadHookMsg = function (sender) {
    return HipChatBot.ERROR_BAD_HOOK.replace('{n}', sender || 'guys');
};

/**
 * Error message for internal server error.
 * @param {string} sender - the user's first name or handle
 * @returns {String}
 */
HipChatBot.prototype.get500Msg = function (sender) {
    return HipChatBot.ERROR_500.replace('{n}', sender);
};

/**
 * Error message for other errors (403, invalid key, etc.).
 * @param {String} sender - the user's first name or handle
 * @param {String} error - custom error text you wish to append
 * @returns {String}
 */
HipChatBot.prototype.getCustomErrorMsg = function (sender, error) {
    return HipChatBot.ERROR_ROOT.replace('{n}', sender) + error;
}

module.exports = HipChatBot;
<commit_msg>cleaning up jsdoc in hipchat bot<commit_after>'use strict';

var $ = require('jquery-deferred');
var _ = require('underscore');
var Imgur = require('./Imgur');

/**
 * Parses HipChat requests.
 * Promises resolve and reject with HipChat response objects.
 * @see {@link https://www.hipchat.com/docs/apiv2/webhooks}
 * @constructor
 * @class
 */
function HipChatBot () {};

/**
 * Start every error with an apology...
 * @type {string}
 */
HipChatBot.ERROR_ROOT = 'Sorry, {n}. ';

/**
 * Error message for empty search result.
 * @type {string}
 */
HipChatBot.ERROR_NO_RESULTS = HipChatBot.ERROR_ROOT +
    'I couldn\'t find anything with the query: "{q}". I suck.';

/**
 * Error message for 500.
 * @type {string}
 */
HipChatBot.ERROR_500 = HipChatBot.ERROR_ROOT +
    'Something\'s borked. Try again later...';

/**
 * Error message for 500.
 * @type {string}
 */
HipChatBot.ERROR_BAD_HOOK = HipChatBot.ERROR_ROOT +
    'Something\'s borked. HipChat data looks funny.';

/**
 * Generic request parser for HipChat WebHooks.
 * Override this in subclasses.
 * Returns a promise that resolves with a generic HipChat message
 * Should reject with an error in subclassed implementations.
 * @param {Object} reqData - the HipChat request body
 * @returns {JqueryDeferred}
 */
HipChatBot.prototype.parseReq = function (reqData) {

    var _this = this;
    var def = $.Deferred();
    var sender = this.getSenderHandle(reqData);
    var message = this.getMessageText(reqData);

    if (!sender || !message) {
        def.reject(
            _this.buildResponse(_this.getBadHookMsg(sender), 'red')
        );
    } else {
        def.resolve(
            _this.buildResponse('oh hey, ' + sender + '. it\'s a generic bot.')
        );
    }

    return def.promise();
};

/**
 * Parses a chat message to return gif based on text after the slug.
 * Returns a promise that resolves with a HipChat message consisting of a link to an Imgur GIF.
 * Promise rejects with a HipChat message object containing an error indicating no GIFs were found.
 * @param {Object} reqData - the HipChat request body
 * @param {String} slug - optional, the slug to remove from the message, default is "/gif"
 * @returns {JqueryDeferred}
 *
 */
HipChatBot.prototype.parseGifReq = function (reqData, slug) {

    var msg = this.getMessageText(reqData);
    var _this = this;
    var query;

    if (msg) {
        query = this.stripSlug(msg, slug) + Imgur.EXT_GIF;
        return this.findImg(query, reqData);
    }
    return $.Deferred().reject(
        _this.buildResponse(_this.getBadHookMsg(), 'red')
    ).promise();
};

/**
 * Searches Imgur API for an image, returning a promise.
 * The promise resolves with a HipChat response message containing a link to an Imgur image.
 * The promise rejects with a HipChat error response if no images could be found.
 * @param {String} query - what you're searching for
 * @param {Object} reqData - the HipChat request body
 * @returns {JqueryDeferred}
 */
HipChatBot.prototype.findImg = function (query, reqData) {

    var _this = this;
    var imgur = new Imgur(process.env.IMGUR_ID);
    var def = $.Deferred();
    var handle = this.getSenderHandle(reqData);
    var errorMsg;

    imgur.getRandomFromSearch(encodeURIComponent(query))
        .done(function (resp) {
            def.resolve(_this.buildResponse(resp.link));
        })
        .fail(function (resp) {
            switch (resp.status) {
                case 500 :
                    errorMsg = _this.get500Msg(handle);
                    break;
                case 200 :
                    errorMsg = _this.getNoResultsMsg(handle, resp.data.query);
                    break;
                default :
                    errorMsg = _this.getCustomErrorMsg(handle, resp.data.error);
                    break;
            }
            def.reject(_this.buildResponse(errorMsg, 'red'));
        });

    return def.promise();
};

/**
 * Builds a response object to be sent to HipChat.
 * @param {String} message
 * @param {String} color - optional, defaults to green
 * @param {Boolean} notify - optional, defaults to false
 * @returns {Object} response
 * @returns {String} response.color
 * @returns {String} response.message
 * @returns {String} response.message_format
 * @returns {Boolean} response.notify
 */
HipChatBot.prototype.buildResponse = function (message, color, notify, format) {
    return {
        color: color || 'green',
        message: message,
        'message_format': format || 'text',
        notify: !!notify
    }
};

/**
 * Remove the slug from a HipChat message string, returning the message.
 * @param {String} msg - the string of a HipChat message
 * @param {String} slug - the hook/slug of the message, ex: '/gif'
 * @returns {String}
 */
HipChatBot.prototype.stripSlug = function (msg, slug) {
    return _.last(msg.split(slug)).trim();
};

/**
 * Retrieves a sender handle from a HipChat request object.
 * @param {object} reqData - HipChat request body
 * @returns {string} - an appropriate recipient name for personalized responses
 */
HipChatBot.prototype.getSenderHandle = function (reqData) {
    var msg = this.getMessage(reqData);
    if (msg.from && _.isString(msg.from.name)) {
        return msg.from.name.split(' ')[0];
    }
    return null;
};

/**
 * Finds the message object in the HipChat request object.
 * @param {Object} reqData - hip chat request body
 * @returns {Object} - message object from a HipChat request
 */
HipChatBot.prototype.getMessage = function (reqData) {
    if (reqData && reqData.item) {
        return reqData.item.message || {};
    }
    return {};
};

/**
 * Finds the message string in the HipChat request object.
 * @param {Object} reqData - hip chat request body
 * @returns {String} - message string from a HipChat request
 */
HipChatBot.prototype.getMessageText = function (reqData) {
    return this.getMessage(reqData).message;
};

/**
 * Finds the message string in the HipChat request object,
 * removes the slug and separates it into lowercase fragments
 * @param {Object} reqData - hip chat request body
 * @param {String} slug - optional, the slug to be removed from the message
 * @returns {Array}
 */
HipChatBot.prototype.getMessageExploded = function (reqData, slug) {
    var message = this.getMessageText(reqData);
    message = this.stripSlug(message, slug).toLowerCase();
    return message.split(' ');
};

/**
 * Error message for no results found.
 * @param {String} sender - the user's first name or handle
 * @param {String} query
 * @returns {String}
 */
HipChatBot.prototype.getNoResultsMsg = function (sender, query) {
    return HipChatBot.ERROR_NO_RESULTS
        .replace('{n}', sender)
        .replace('{q}', unescape(query).replace(Imgur.EXT_GIF, ''));
};

/**
 * Builds an error message for missing or malformed WebHook data from HipChat.
 * @param {string} sender - the user's first name or handle
 * @returns {String}
 */
HipChatBot.prototype.getBadHookMsg = function (sender) {
    return HipChatBot.ERROR_BAD_HOOK.replace('{n}', sender || 'guys');
};

/**
 * Builds an error message for an internal server error.
 * @param {string} sender - the user's first name or handle
 * @returns {String}
 */
HipChatBot.prototype.get500Msg = function (sender) {
    return HipChatBot.ERROR_500.replace('{n}', sender);
};

/**
 * Builds an error message for other errors (403, invalid key, etc.).
 * @param {String} sender - the user's first name or handle
 * @param {String} error - custom error text you wish to append
 * @returns {String}
 */
HipChatBot.prototype.getCustomErrorMsg = function (sender, error) {
    return HipChatBot.ERROR_ROOT.replace('{n}', sender) + error;
}

module.exports = HipChatBot;
<|endoftext|>


<commit_before>/**
 *
 * Copyright 2016-present reading
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
import React, { PropTypes } from 'react';
import {
  InteractionManager,
  StyleSheet,
  Text,
  View,
  DeviceEventEmitter,
  ScrollView,
  RefreshControl,
  Alert
} from 'react-native';

import AV from 'leancloud-storage';
import store from 'react-native-simple-store';
import GridView from '../components/GridView';
import Button from '../components/Button';
import ToastUtil from '../utils/ToastUtil';
import NavigationUtil from '../utils/NavigationUtil';

let tempTypeIds = [];
let maxCategory = 5; // 默认最多5个类别，远端可配置

const propTypes = {
  categoryActions: PropTypes.object,
  category: PropTypes.object.isRequired
};

class Category extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      typeIds: tempTypeIds
    };
  }

  componentWillMount() {
    const { params } = this.props.navigation.state;
    if (params === undefined || !params.isFirst) {
      InteractionManager.runAfterInteractions(() => {
        store.get('typeIds').then((typeIds) => {
          tempTypeIds = typeIds;
          this.setState({
            typeIds
          });
        });
      });
    }
  }

  componentDidMount() {
    const { categoryActions } = this.props;
    categoryActions.requestTypeList();
    const query = new AV.Query('Reading_Settings');
    query.get('57b86e0ba633bd002a96436b').then((settings) => {
      maxCategory = settings.get('max_category');
    });
    const { params } = this.props.navigation.state;
    if (params === undefined || !params.isFirst) {
      this.props.navigation.setParams({ handleCheck: this.onActionSelected });
    }
  }

  onRefresh = () => {
    const { categoryActions } = this.props;
    categoryActions.requestTypeList();
  };

  onPress = (type) => {
    const pos = tempTypeIds.indexOf(parseInt(type.id));
    if (pos === -1) {
      tempTypeIds.push(parseInt(type.id));
    } else {
      tempTypeIds.splice(pos, 1);
    }
    this.setState({
      typeIds: tempTypeIds
    });
  };

  onSelectCatagory = () => {
    if (this.state.typeIds.length === 0) {
      Alert.alert('提示', '您确定不选择任何分类吗？', [
        { text: '取消', style: 'cancel' },
        {
          text: '确定',
          onPress: () => {
            store.save('typeIds', this.state.typeIds);
            NavigationUtil.reset(this.props.navigation, 'Home');
          }
        }
      ]);
    } else if (this.state.typeIds.length > maxCategory) {
      ToastUtil.showShort(`不要超过${maxCategory}个类别哦`);
    } else {
      store.save('typeIds', this.state.typeIds);
      store.save('isInit', true);
      NavigationUtil.reset(this.props.navigation, 'Home');
    }
  };

  onActionSelected = () => {
    if (tempTypeIds.length > maxCategory) {
      ToastUtil.showShort(`不要超过${maxCategory}个类别哦`);
      return;
    }
    if (tempTypeIds.length < 1) {
      ToastUtil.showShort('不要少于1个类别哦');
    }
    const { navigate } = this.props.navigation;
    InteractionManager.runAfterInteractions(() => {
      store.get('typeIds').then((typeIds) => {
        if (
          typeIds.sort().toString() ===
          Array.from(tempTypeIds).sort().toString()
        ) {
          navigate('Main');
          return;
        }
        store.save('typeIds', this.state.typeIds).then(this.routeMain);
      });
    });
  };

  routeMain = () => {
    const { navigate } = this.props.navigation;
    DeviceEventEmitter.emit('changeCategory', this.state.typeIds);
    navigate('Main');
  };

  renderItem = (item) => {
    const isSelect =
      Array.from(this.state.typeIds).indexOf(parseInt(item.id)) !== -1;
    return (
      <Button
        key={item.id}
        containerStyle={[
          styles.categoryBtn,
          isSelect
            ? { backgroundColor: '#3e9ce9' }
            : { backgroundColor: '#fcfcfc' }
        ]}
        style={[
          styles.categoryText,
          isSelect ? { color: '#fcfcfc' } : { color: 'black' }
        ]}
        text={item.name}
        onPress={() => this.onPress(item)}
      />
    );
  };

  renderGridView = () => {
    const { category } = this.props;
    return (
      <ScrollView
        automaticallyAdjustContentInsets={false}
        horizontal={false}
        contentContainerStyle={styles.no_data}
        style={styles.base}
        refreshControl={
          <RefreshControl
            refreshing={category.loading}
            onRefresh={this.onRefresh}
            title="Loading..."
            colors={['#ffaa66cc', '#ff00ddff', '#ffffbb33', '#ffff4444']}
          />
        }
      >
        <View style={styles.gridLayout}>
          <GridView
            items={Array.from(category.typeList)}
            itemsPerRow={3}
            renderItem={this.renderItem}
          />
        </View>
      </ScrollView>
    );
  };

  render() {
    const { params } = this.props.navigation.state;
    if (params !== undefined && params.isFirst) {
      return (
        <View style={styles.container}>
          <View style={styles.header}>
            <Text
              style={[
                styles.btnText,
                { color: 'black', padding: 5, fontSize: 18 }
              ]}
            >
              初次见面，请选择您感兴趣的1-5个类别
            </Text>
          </View>
          {this.renderGridView()}
          <Button
            containerStyle={styles.sureBtn}
            style={styles.btnText}
            text={'确认'}
            onPress={() => this.onSelectCatagory()}
          />
        </View>
      );
    }
    return (
      <View style={styles.container}>
        <View style={styles.header}>
          <Text style={[styles.btnText, { color: 'black' }]}>
            请选择您感兴趣的1-5个类别
          </Text>
        </View>
        {this.renderGridView()}
      </View>
    );
  }
}

const styles = StyleSheet.create({
  base: {
    flex: 1
  },
  container: {
    flex: 1,
    flexDirection: 'column',
    backgroundColor: '#fff'
  },
  categoryBtn: {
    margin: 10,
    padding: 10,
    borderRadius: 10,
    borderWidth: 1,
    borderColor: '#dddddd'
  },
  categoryText: {
    fontSize: 16,
    textAlign: 'center'
  },
  gridLayout: {
    flex: 1,
    alignItems: 'center',
    backgroundColor: '#f2f2f2'
  },
  sureBtn: {
    margin: 10,
    padding: 10,
    borderRadius: 10,
    backgroundColor: '#3e9ce9'
  },
  btnText: {
    fontSize: 16,
    textAlign: 'center',
    color: '#fff'
  },
  header: {
    padding: 10,
    backgroundColor: '#fcfcfc'
  }
});

Category.propTypes = propTypes;

export default Category;
<commit_msg>fixed typo<commit_after>/**
 *
 * Copyright 2016-present reading
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
import React, { PropTypes } from 'react';
import {
  InteractionManager,
  StyleSheet,
  Text,
  View,
  DeviceEventEmitter,
  ScrollView,
  RefreshControl,
  Alert
} from 'react-native';

import AV from 'leancloud-storage';
import store from 'react-native-simple-store';
import GridView from '../components/GridView';
import Button from '../components/Button';
import ToastUtil from '../utils/ToastUtil';
import NavigationUtil from '../utils/NavigationUtil';

let tempTypeIds = [];
let maxCategory = 5; // 默认最多5个类别，远端可配置

const propTypes = {
  categoryActions: PropTypes.object,
  category: PropTypes.object.isRequired
};

class Category extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      typeIds: tempTypeIds
    };
  }

  componentWillMount() {
    const { params } = this.props.navigation.state;
    if (params === undefined || !params.isFirst) {
      InteractionManager.runAfterInteractions(() => {
        store.get('typeIds').then((typeIds) => {
          tempTypeIds = typeIds;
          this.setState({
            typeIds
          });
        });
      });
    }
  }

  componentDidMount() {
    const { categoryActions } = this.props;
    categoryActions.requestTypeList();
    const query = new AV.Query('Reading_Settings');
    query.get('57b86e0ba633bd002a96436b').then((settings) => {
      maxCategory = settings.get('max_category');
    });
    const { params } = this.props.navigation.state;
    if (params === undefined || !params.isFirst) {
      this.props.navigation.setParams({ handleCheck: this.onActionSelected });
    }
  }

  onRefresh = () => {
    const { categoryActions } = this.props;
    categoryActions.requestTypeList();
  };

  onPress = (type) => {
    const pos = tempTypeIds.indexOf(parseInt(type.id));
    if (pos === -1) {
      tempTypeIds.push(parseInt(type.id));
    } else {
      tempTypeIds.splice(pos, 1);
    }
    this.setState({
      typeIds: tempTypeIds
    });
  };

  onSelectCategory = () => {
    if (this.state.typeIds.length === 0) {
      Alert.alert('提示', '您确定不选择任何分类吗？', [
        { text: '取消', style: 'cancel' },
        {
          text: '确定',
          onPress: () => {
            store.save('typeIds', this.state.typeIds);
            NavigationUtil.reset(this.props.navigation, 'Home');
          }
        }
      ]);
    } else if (this.state.typeIds.length > maxCategory) {
      ToastUtil.showShort(`不要超过${maxCategory}个类别哦`);
    } else {
      store.save('typeIds', this.state.typeIds);
      store.save('isInit', true);
      NavigationUtil.reset(this.props.navigation, 'Home');
    }
  };

  onActionSelected = () => {
    if (tempTypeIds.length > maxCategory) {
      ToastUtil.showShort(`不要超过${maxCategory}个类别哦`);
      return;
    }
    if (tempTypeIds.length < 1) {
      ToastUtil.showShort('不要少于1个类别哦');
    }
    const { navigate } = this.props.navigation;
    InteractionManager.runAfterInteractions(() => {
      store.get('typeIds').then((typeIds) => {
        if (
          typeIds.sort().toString() ===
          Array.from(tempTypeIds).sort().toString()
        ) {
          navigate('Main');
          return;
        }
        store.save('typeIds', this.state.typeIds).then(this.routeMain);
      });
    });
  };

  routeMain = () => {
    const { navigate } = this.props.navigation;
    DeviceEventEmitter.emit('changeCategory', this.state.typeIds);
    navigate('Main');
  };

  renderItem = (item) => {
    const isSelect =
      Array.from(this.state.typeIds).indexOf(parseInt(item.id)) !== -1;
    return (
      <Button
        key={item.id}
        containerStyle={[
          styles.categoryBtn,
          isSelect
            ? { backgroundColor: '#3e9ce9' }
            : { backgroundColor: '#fcfcfc' }
        ]}
        style={[
          styles.categoryText,
          isSelect ? { color: '#fcfcfc' } : { color: 'black' }
        ]}
        text={item.name}
        onPress={() => this.onPress(item)}
      />
    );
  };

  renderGridView = () => {
    const { category } = this.props;
    return (
      <ScrollView
        automaticallyAdjustContentInsets={false}
        horizontal={false}
        contentContainerStyle={styles.no_data}
        style={styles.base}
        refreshControl={
          <RefreshControl
            refreshing={category.loading}
            onRefresh={this.onRefresh}
            title="Loading..."
            colors={['#ffaa66cc', '#ff00ddff', '#ffffbb33', '#ffff4444']}
          />
        }
      >
        <View style={styles.gridLayout}>
          <GridView
            items={Array.from(category.typeList)}
            itemsPerRow={3}
            renderItem={this.renderItem}
          />
        </View>
      </ScrollView>
    );
  };

  render() {
    const { params } = this.props.navigation.state;
    if (params !== undefined && params.isFirst) {
      return (
        <View style={styles.container}>
          <View style={styles.header}>
            <Text
              style={[
                styles.btnText,
                { color: 'black', padding: 5, fontSize: 18 }
              ]}
            >
              初次见面，请选择您感兴趣的1-5个类别
            </Text>
          </View>
          {this.renderGridView()}
          <Button
            containerStyle={styles.sureBtn}
            style={styles.btnText}
            text={'确认'}
            onPress={() => this.onSelectCategory()}
          />
        </View>
      );
    }
    return (
      <View style={styles.container}>
        <View style={styles.header}>
          <Text style={[styles.btnText, { color: 'black' }]}>
            请选择您感兴趣的1-5个类别
          </Text>
        </View>
        {this.renderGridView()}
      </View>
    );
  }
}

const styles = StyleSheet.create({
  base: {
    flex: 1
  },
  container: {
    flex: 1,
    flexDirection: 'column',
    backgroundColor: '#fff'
  },
  categoryBtn: {
    margin: 10,
    padding: 10,
    borderRadius: 10,
    borderWidth: 1,
    borderColor: '#dddddd'
  },
  categoryText: {
    fontSize: 16,
    textAlign: 'center'
  },
  gridLayout: {
    flex: 1,
    alignItems: 'center',
    backgroundColor: '#f2f2f2'
  },
  sureBtn: {
    margin: 10,
    padding: 10,
    borderRadius: 10,
    backgroundColor: '#3e9ce9'
  },
  btnText: {
    fontSize: 16,
    textAlign: 'center',
    color: '#fff'
  },
  header: {
    padding: 10,
    backgroundColor: '#fcfcfc'
  }
});

Category.propTypes = propTypes;

export default Category;
<|endoftext|>