<commit_before>'use strict';

var _ = require('lodash'),
    fp = require('lodash/fp'),
    os = require('os'),
    Alias = require('./alias.js'),
    util = require('./util.js');

/*----------------------------------------------------------------------------*/

/**
 * Gets the param type of `tag`.
 *
 * @private
 * @param {Object} tag The param tag to inspect.
 * @returns {string} Returns the param type.
 */
function getParamType(tag) {
  var expression = tag.expression,
      result = '',
      type = tag.type;

  switch (type) {
    case 'AllLiteral':
      result = '*';
      break;

    case 'NameExpression':
      result = _.toString(tag.name);
      break;

    case 'RestType':
      result = '...' + result;
      break;

    case 'TypeApplication':
      expression = undefined;
      result = _(tag)
        .chain()
        .get('applications')
        .map(_.flow(getParamType, fp.add(fp, '[]')))
        .sort(util.compareNatural)
        .join('|')
        .value();
      break;

    case 'UnionType':
      result = _(tag)
        .chain()
        .get('elements')
        .map(getParamType)
        .sort(util.compareNatural)
        .join('|')
        .value();
  }
  if (expression) {
    result += getParamType(expression);
  }
  return type == 'UnionType'
    ? ('(' + result + ')')
    : result;
}

/**
 * Gets an `entry` tag by `tagName`.
 *
 * @private
 * @param {Object} entry The entry to inspect.
 * @param {string} tagName The name of the tag.
 * @returns {null|Object} Returns the tag.
 */
function getTag(entry, tagName) {
  var parsed = entry.parsed;
  return _.find(parsed.tags, ['title', tagName]) || null;
}

/**
 * Gets an `entry` tag value by `tagName`.
 *
 * @private
 * @param {Object} entry The entry to inspect.
 * @param {string} tagName The name of the tag.
 * @returns {string} Returns the tag value.
 */
function getValue(entry, tagName) {
  var parsed = entry.parsed,
      result = parsed.description,
      tag = getTag(entry, tagName);

  if (tagName == 'alias') {
    result = _.get(tag, 'name') ;

    // Doctrine can't parse alias tags containing multiple values so extract
    // them from the error message.
    var error = _.first(_.get(tag, 'errors'));
    if (error) {
      result += error.replace(/^[^']*'|'[^']*$/g, '');
    }
  }
  else if (tagName == 'type') {
    result = _.get(tag, 'type.name');
  }
  else if (tagName != 'description') {
    result = _.get(tag, 'name') || _.get(tag, 'description');
  }
  return tagName == 'example'
    ? _.toString(result)
    : util.format(result);
}

/**
 * Checks if `entry` has a tag of `tagName`.
 *
 * @private
 * @param {Object} entry The entry to inspect.
 * @param {string} tagName The name of the tag.
 * @returns {boolean} Returns `true` if the tag is found, else `false`.
 */
function hasTag(entry, tagName) {
  return getTag(entry, tagName) !== null;
}

/*----------------------------------------------------------------------------*/

/**
 * The Entry constructor.
 *
 * @constructor
 * @param {string} entry The documentation entry to analyse.
 * @param {string} source The source code.
 * @param {string} [lang='js'] The language highlighter used for code examples.
 */
function Entry(entry, source, lang) {
  this.parsed = util.parse(entry.replace(/(\*)\/\s*.+$/, '*'));

  this.entry = entry;
  this.lang = lang == null ? 'js' : lang;
  this.source = source.replace(os.EOL, '\n');
  this.getCall = _.memoize(this.getCall);
  this.getCategory = _.memoize(this.getCategory);
  this.getDesc = _.memoize(this.getDesc);
  this.getExample = _.memoize(this.getExample);
  this.getHash = _.memoize(this.getHash);
  this.getLineNumber = _.memoize(this.getLineNumber);
  this.getName = _.memoize(this.getName);
  this.getReturns = _.memoize(this.getReturns);
  this.getSince = _.memoize(this.getSince);
  this.getType = _.memoize(this.getType);
  this.isAlias = _.memoize(this.isAlias);
  this.isCtor = _.memoize(this.isCtor);
  this.isFunction = _.memoize(this.isFunction);
  this.isLicense = _.memoize(this.isLicense);
  this.isPlugin = _.memoize(this.isPlugin);
  this.isPrivate = _.memoize(this.isPrivate);
  this.isStatic = _.memoize(this.isStatic);
  this._aliases = this._members = this._params = undefined;
}

/**
 * Extracts the documentation entries from source code.
 *
 * @static
 * @memberOf Entry
 * @param {string} source The source code.
 * @returns {Array} Returns the array of entries.
 */
function getEntries(source) {
  return _.toString(source).match(/\/\*\*(?![-!])[\s\S]*?\*\/\s*.+/g) || [];
}

/**
 * Extracts the entry's `alias` objects.
 *
 * @memberOf Entry
 * @param {number} index The index of the array value to return.
 * @returns {Array|string} Returns the entry's `alias` objects.
 */
function getAliases(index) {
  if (this._aliases === undefined) {
    var owner = this;
    this._aliases = _(getValue(this, 'alias'))
      .split(/,\s*/)
      .compact()
      .sort(util.compareNatural)
      .map(function(value) { return new Alias(value, owner); })
      .value();
  }
  var result = this._aliases;
  return index === undefined ? result : result[index];
}

/**
 * Extracts the function call from the entry.
 *
 * @memberOf Entry
 * @returns {string} Returns the function call.
 */
function getCall() {
  var result = /\*\/\s*(?:function\s+([^(]*)|(.*?)(?=[:=,]))/.exec(this.entry);
  if (result) {
    result = (result[1] || result[2]).split('.').pop();
    result = _.trim(_.trim(result), "'").split('var ').pop();
    result = _.trim(result);
  }
  var name = getValue(this, 'name') || _.toString(result);
  if (!this.isFunction()) {
    return name;
  }
  var params = this.getParams();
  result = _.castArray(result);

  // Compile the function call syntax.
  _.each(params, function(param) {
    var paramValue = param[1],
        parentParam = _.result(/\w+(?=\.[\w.]+)/.exec(paramValue), 0);

    var parentIndex = parentParam == null ? -1 : _.findIndex(params, function(param) {
      return _.trim(param[1], '[]') == parentParam;
    });

    // Skip params that are properties of other params (e.g. `options.leading`).
    if (_.result(params[parentIndex], 0) != 'Object') {
      result.push(paramValue);
    }
  });

  // Format the function call.
  return name + '(' + result.slice(1).join(', ') + ')';
}

/**
 * Extracts the entry's `category` data.
 *
 * @memberOf Entry
 * @returns {string} Returns the entry's `category` data.
 */
function getCategory() {
  var result = getValue(this, 'category');
  return result || (this.getType() == 'Function' ? 'Methods' : 'Properties');
}

/**
 * Extracts the entry's description.
 *
 * @memberOf Entry
 * @returns {string} Returns the entry's description.
 */
function getDesc() {
  var type = this.getType(),
      result = getValue(this, 'description');

  return (!result || type == 'Function' || type == 'unknown')
    ? result
    : ('(' + _.trim(type.replace(/\|/g, ', '), '()') + '): ' + result);
}

/**
 * Extracts the entry's `example` data.
 *
 * @memberOf Entry
 * @returns {string} Returns the entry's `example` data.
 */
function getExample() {
  var result = getValue(this, 'example');
  return result && ('```' + this.lang + '\n' + result + '\n```');
}

/**
 * Extracts the entry's hash value for permalinking.
 *
 * @memberOf Entry
 * @param {string} [style] The hash style.
 * @returns {string} Returns the entry's hash value (without a hash itself).
 */
function getHash(style) {
  var result = _.toString(this.getMembers(0));
  if (style == 'github') {
    if (result) {
      result += this.isPlugin() ? 'prototype' : '';
    }
    result += this.getCall();
    return result
      .replace(/[\\.=|'"(){}\[\]\t ]/g, '')
      .replace(/[#,]+/g, '-')
      .toLowerCase();
  }
  if (result) {
    result += '-' + (this.isPlugin() ? 'prototype-' : '');
  }
  result += this.isAlias() ? this.getOwner().getName() : this.getName();
  return result
    .replace(/\./g, '-')
    .replace(/^_-/, '');
}

/**
 * Resolves the entry's line number.
 *
 * @memberOf Entry
 * @returns {number} Returns the entry's line number.
 */
function getLineNumber() {
  var lines = this.source
    .slice(0, this.source.indexOf(this.entry) + this.entry.length)
    .match(/\n/g)
    .slice(1);

  // Offset by 2 because the first line number is before a line break and the
  // last line doesn't include a line break.
  return lines.length + 2;
}

/**
 * Extracts the entry's `member` data.
 *
 * @memberOf Entry
 * @param {number} [index] The index of the array value to return.
 * @returns {Array|string} Returns the entry's `member` data.
 */
function getMembers(index) {
  if (this._members === undefined) {
    this._members = _(getValue(this, 'member') || getValue(this, 'memberOf'))
      .split(/,\s*/)
      .compact()
      .sort(util.compareNatural)
      .value();
  }
  var result = this._members;
  return index === undefined ? result : result[index];
}

/**
 * Extracts the entry's `name` data.
 *
 * @memberOf Entry
 * @returns {string} Returns the entry's `name` data.
 */
function getName() {
  return hasTag(this, 'name')
    ? getValue(this, 'name')
    : _.toString(_.first(this.getCall().split('(')));
}

/**
 * Extracts the entry's `param` data.
 *
 * @memberOf Entry
 * @param {number} [index] The index of the array value to return.
 * @returns {Array} Returns the entry's `param` data.
 */
function getParams(index) {
  if (this._params === undefined) {
    this._params = _(this.parsed.tags)
      .filter(['title', 'param'])
      .filter('name')
      .map(function(tag) {
        var defaultValue = tag['default'],
            desc = util.format(tag.description),
            name = _.toString(tag.name),
            type = getParamType(tag.type);

        if (defaultValue != null) {
          name += '=' + defaultValue;
        }
        if (_.get(tag, 'type.type') == 'OptionalType') {
          name = '[' + name + ']';
        }
        return [type, name,  desc];
      })
      .value();
  }
  var result = this._params;
  return index === undefined ? result : result[index];
}

/**
 * Extracts the entry's `returns` data.
 *
 * @memberOf Entry
 * @returns {array} Returns the entry's `returns` data.
 */
function getReturns() {
  var tag = getTag(this, 'returns'),
      desc = _.toString(_.get(tag, 'description')),
      type = _.toString(_.get(tag, 'type.name')) || '*';

  return tag ? [type, desc] : [];
}

/**
 * Extracts the entry's `since` data.
 *
 * @memberOf Entry
 * @returns {string} Returns the entry's `since` data.
 */
function getSince() {
  return getValue(this, 'since');
}

/**
 * Extracts the entry's `type` data.
 *
 * @memberOf Entry
 * @returns {string} Returns the entry's `type` data.
 */
function getType() {
  var result = getValue(this, 'type');
  if (!result) {
    return this.isFunction() ? 'Function' : 'unknown';
  }
  return /^(?:array|function|object|regexp)$/.test(result)
    ? _.capitalize(result)
    : result;
}

/**
 * Checks if the entry is an alias.
 *
 * @memberOf Entry
 * @type {Function}
 * @returns {boolean} Returns `false`.
 */
var isAlias = _.constant(false);

/**
 * Checks if the entry is a constructor.
 *
 * @memberOf Entry
 * @returns {boolean} Returns `true` if a constructor, else `false`.
 */
function isCtor() {
  return hasTag(this, 'constructor');
}

/**
 * Checks if the entry is a function reference.
 *
 * @memberOf Entry
 * @returns {boolean} Returns `true` if the entry is a function reference, else `false`.
 */
function isFunction() {
  return !!(
    this.isCtor() ||
    _.size(this.getParams()) ||
    _.size(this.getReturns()) ||
    hasTag(this, 'function') ||
    /\*\/\s*function/.test(this.entry)
  );
}

/**
 * Checks if the entry is a license.
 *
 * @memberOf Entry
 * @returns {boolean} Returns `true` if a license, else `false`.
 */
function isLicense() {
  return hasTag(this, 'license');
}

/**
 * Checks if the entry *is* assigned to a prototype.
 *
 * @memberOf Entry
 * @returns {boolean} Returns `true` if assigned to a prototype, else `false`.
 */
function isPlugin() {
  return (
    !this.isCtor() &&
    !this.isPrivate() &&
    !this.isStatic()
  );
}

/**
 * Checks if the entry is private.
 *
 * @memberOf Entry
 * @returns {boolean} Returns `true` if private, else `false`.
 */
function isPrivate() {
  return (
    this.isLicense() ||
    hasTag(this, 'private') ||
    _.isEmpty(this.parsed.tags)
  );
}

/**
 * Checks if the entry is *not* assigned to a prototype.
 *
 * @memberOf Entry
 * @returns {boolean} Returns `true` if not assigned to a prototype, else `false`.
 */
function isStatic() {
  var isPublic = !this.isPrivate(),
      result = isPublic && hasTag(this, 'static');

  // Get the result in cases where it isn't explicitly stated.
  if (isPublic && !result) {
    var parent = _.last(_.toString(this.getMembers(0)).split(/[#.]/));
    if (!parent) {
      return true;
    }
    var source = this.source;
    _.each(getEntries(source), function(entry) {
      entry = new Entry(entry, source);
      if (entry.getName() == parent) {
        result = !entry.isCtor();
        return false;
      }
    });
  }
  return result;
}

/*----------------------------------------------------------------------------*/

Entry.getEntries = getEntries;

_.assign(Entry.prototype, {
  'getAliases': getAliases,
  'getCall': getCall,
  'getCategory': getCategory,
  'getDesc': getDesc,
  'getExample': getExample,
  'getHash': getHash,
  'getLineNumber': getLineNumber,
  'getMembers': getMembers,
  'getName': getName,
  'getParams': getParams,
  'getReturns': getReturns,
  'getSince': getSince,
  'getType': getType,
  'isAlias': isAlias,
  'isCtor': isCtor,
  'isFunction': isFunction,
  'isLicense': isLicense,
  'isPlugin': isPlugin,
  'isPrivate': isPrivate,
  'isStatic': isStatic
});

module.exports = Entry;
<commit_msg>Update function checks to work with ES6 style functions. [closes #39]<commit_after>'use strict';

var _ = require('lodash'),
    fp = require('lodash/fp'),
    os = require('os'),
    Alias = require('./alias.js'),
    util = require('./util.js');

/*----------------------------------------------------------------------------*/

/**
 * Gets the param type of `tag`.
 *
 * @private
 * @param {Object} tag The param tag to inspect.
 * @returns {string} Returns the param type.
 */
function getParamType(tag) {
  var expression = tag.expression,
      result = '',
      type = tag.type;

  switch (type) {
    case 'AllLiteral':
      result = '*';
      break;

    case 'NameExpression':
      result = _.toString(tag.name);
      break;

    case 'RestType':
      result = '...' + result;
      break;

    case 'TypeApplication':
      expression = undefined;
      result = _(tag)
        .chain()
        .get('applications')
        .map(_.flow(getParamType, fp.add(fp, '[]')))
        .sort(util.compareNatural)
        .join('|')
        .value();
      break;

    case 'UnionType':
      result = _(tag)
        .chain()
        .get('elements')
        .map(getParamType)
        .sort(util.compareNatural)
        .join('|')
        .value();
  }
  if (expression) {
    result += getParamType(expression);
  }
  return type == 'UnionType'
    ? ('(' + result + ')')
    : result;
}

/**
 * Gets an `entry` tag by `tagName`.
 *
 * @private
 * @param {Object} entry The entry to inspect.
 * @param {string} tagName The name of the tag.
 * @returns {null|Object} Returns the tag.
 */
function getTag(entry, tagName) {
  var parsed = entry.parsed;
  return _.find(parsed.tags, ['title', tagName]) || null;
}

/**
 * Gets an `entry` tag value by `tagName`.
 *
 * @private
 * @param {Object} entry The entry to inspect.
 * @param {string} tagName The name of the tag.
 * @returns {string} Returns the tag value.
 */
function getValue(entry, tagName) {
  var parsed = entry.parsed,
      result = parsed.description,
      tag = getTag(entry, tagName);

  if (tagName == 'alias') {
    result = _.get(tag, 'name') ;

    // Doctrine can't parse alias tags containing multiple values so extract
    // them from the error message.
    var error = _.first(_.get(tag, 'errors'));
    if (error) {
      result += error.replace(/^[^']*'|'[^']*$/g, '');
    }
  }
  else if (tagName == 'type') {
    result = _.get(tag, 'type.name');
  }
  else if (tagName != 'description') {
    result = _.get(tag, 'name') || _.get(tag, 'description');
  }
  return tagName == 'example'
    ? _.toString(result)
    : util.format(result);
}

/**
 * Checks if `entry` has a tag of `tagName`.
 *
 * @private
 * @param {Object} entry The entry to inspect.
 * @param {string} tagName The name of the tag.
 * @returns {boolean} Returns `true` if the tag is found, else `false`.
 */
function hasTag(entry, tagName) {
  return getTag(entry, tagName) !== null;
}

/*----------------------------------------------------------------------------*/

/**
 * The Entry constructor.
 *
 * @constructor
 * @param {string} entry The documentation entry to analyse.
 * @param {string} source The source code.
 * @param {string} [lang='js'] The language highlighter used for code examples.
 */
function Entry(entry, source, lang) {
  this.parsed = util.parse(entry.replace(/(\*)\/\s*.+$/, '*'));

  this.entry = entry;
  this.lang = lang == null ? 'js' : lang;
  this.source = source.replace(os.EOL, '\n');
  this.getCall = _.memoize(this.getCall);
  this.getCategory = _.memoize(this.getCategory);
  this.getDesc = _.memoize(this.getDesc);
  this.getExample = _.memoize(this.getExample);
  this.getHash = _.memoize(this.getHash);
  this.getLineNumber = _.memoize(this.getLineNumber);
  this.getName = _.memoize(this.getName);
  this.getReturns = _.memoize(this.getReturns);
  this.getSince = _.memoize(this.getSince);
  this.getType = _.memoize(this.getType);
  this.isAlias = _.memoize(this.isAlias);
  this.isCtor = _.memoize(this.isCtor);
  this.isFunction = _.memoize(this.isFunction);
  this.isLicense = _.memoize(this.isLicense);
  this.isPlugin = _.memoize(this.isPlugin);
  this.isPrivate = _.memoize(this.isPrivate);
  this.isStatic = _.memoize(this.isStatic);
  this._aliases = this._members = this._params = undefined;
}

/**
 * Extracts the documentation entries from source code.
 *
 * @static
 * @memberOf Entry
 * @param {string} source The source code.
 * @returns {Array} Returns the array of entries.
 */
function getEntries(source) {
  return _.toString(source).match(/\/\*\*(?![-!])[\s\S]*?\*\/\s*.+/g) || [];
}

/**
 * Extracts the entry's `alias` objects.
 *
 * @memberOf Entry
 * @param {number} index The index of the array value to return.
 * @returns {Array|string} Returns the entry's `alias` objects.
 */
function getAliases(index) {
  if (this._aliases === undefined) {
    var owner = this;
    this._aliases = _(getValue(this, 'alias'))
      .split(/,\s*/)
      .compact()
      .sort(util.compareNatural)
      .map(function(value) { return new Alias(value, owner); })
      .value();
  }
  var result = this._aliases;
  return index === undefined ? result : result[index];
}

/**
 * Extracts the function call from the entry.
 *
 * @memberOf Entry
 * @returns {string} Returns the function call.
 */
function getCall() {
  var result = _.trim(_.get(/\*\/\s*(?:function\s+)?([^\s(]*)(?=\s*\()/.exec(this.entry), 1));
  if (!result) {
    result = _.trim(_.get(/\*\/\s*(.*?)(?=[:=,])/.exec(this.entry), 1));
    result = /['"]$/.test(result)
      ? _.trim(result, '"\'')
      : result.split('.').pop().split(/^(?:const|let|var) /).pop();
  }
  var name = getValue(this, 'name') || _.toString(result);
  if (!this.isFunction()) {
    return name;
  }
  var params = this.getParams();
  result = _.castArray(result);

  // Compile the function call syntax.
  _.each(params, function(param) {
    var paramValue = param[1],
        parentParam = _.result(/\w+(?=\.[\w.]+)/.exec(paramValue), 0);

    var parentIndex = parentParam == null ? -1 : _.findIndex(params, function(param) {
      return _.trim(param[1], '[]') == parentParam;
    });

    // Skip params that are properties of other params (e.g. `options.leading`).
    if (_.result(params[parentIndex], 0) != 'Object') {
      result.push(paramValue);
    }
  });

  // Format the function call.
  return name + '(' + result.slice(1).join(', ') + ')';
}

/**
 * Extracts the entry's `category` data.
 *
 * @memberOf Entry
 * @returns {string} Returns the entry's `category` data.
 */
function getCategory() {
  var result = getValue(this, 'category');
  return result || (this.getType() == 'Function' ? 'Methods' : 'Properties');
}

/**
 * Extracts the entry's description.
 *
 * @memberOf Entry
 * @returns {string} Returns the entry's description.
 */
function getDesc() {
  var type = this.getType(),
      result = getValue(this, 'description');

  return (!result || type == 'Function' || type == 'unknown')
    ? result
    : ('(' + _.trim(type.replace(/\|/g, ', '), '()') + '): ' + result);
}

/**
 * Extracts the entry's `example` data.
 *
 * @memberOf Entry
 * @returns {string} Returns the entry's `example` data.
 */
function getExample() {
  var result = getValue(this, 'example');
  return result && ('```' + this.lang + '\n' + result + '\n```');
}

/**
 * Extracts the entry's hash value for permalinking.
 *
 * @memberOf Entry
 * @param {string} [style] The hash style.
 * @returns {string} Returns the entry's hash value (without a hash itself).
 */
function getHash(style) {
  var result = _.toString(this.getMembers(0));
  if (style == 'github') {
    if (result) {
      result += this.isPlugin() ? 'prototype' : '';
    }
    result += this.getCall();
    return result
      .replace(/[\\.=|'"(){}\[\]\t ]/g, '')
      .replace(/[#,]+/g, '-')
      .toLowerCase();
  }
  if (result) {
    result += '-' + (this.isPlugin() ? 'prototype-' : '');
  }
  result += this.isAlias() ? this.getOwner().getName() : this.getName();
  return result
    .replace(/\./g, '-')
    .replace(/^_-/, '');
}

/**
 * Resolves the entry's line number.
 *
 * @memberOf Entry
 * @returns {number} Returns the entry's line number.
 */
function getLineNumber() {
  var lines = this.source
    .slice(0, this.source.indexOf(this.entry) + this.entry.length)
    .match(/\n/g)
    .slice(1);

  // Offset by 2 because the first line number is before a line break and the
  // last line doesn't include a line break.
  return lines.length + 2;
}

/**
 * Extracts the entry's `member` data.
 *
 * @memberOf Entry
 * @param {number} [index] The index of the array value to return.
 * @returns {Array|string} Returns the entry's `member` data.
 */
function getMembers(index) {
  if (this._members === undefined) {
    this._members = _(getValue(this, 'member') || getValue(this, 'memberOf'))
      .split(/,\s*/)
      .compact()
      .sort(util.compareNatural)
      .value();
  }
  var result = this._members;
  return index === undefined ? result : result[index];
}

/**
 * Extracts the entry's `name` data.
 *
 * @memberOf Entry
 * @returns {string} Returns the entry's `name` data.
 */
function getName() {
  return hasTag(this, 'name')
    ? getValue(this, 'name')
    : _.toString(_.first(this.getCall().split('(')));
}

/**
 * Extracts the entry's `param` data.
 *
 * @memberOf Entry
 * @param {number} [index] The index of the array value to return.
 * @returns {Array} Returns the entry's `param` data.
 */
function getParams(index) {
  if (this._params === undefined) {
    this._params = _(this.parsed.tags)
      .filter(['title', 'param'])
      .filter('name')
      .map(function(tag) {
        var defaultValue = tag['default'],
            desc = util.format(tag.description),
            name = _.toString(tag.name),
            type = getParamType(tag.type);

        if (defaultValue != null) {
          name += '=' + defaultValue;
        }
        if (_.get(tag, 'type.type') == 'OptionalType') {
          name = '[' + name + ']';
        }
        return [type, name,  desc];
      })
      .value();
  }
  var result = this._params;
  return index === undefined ? result : result[index];
}

/**
 * Extracts the entry's `returns` data.
 *
 * @memberOf Entry
 * @returns {array} Returns the entry's `returns` data.
 */
function getReturns() {
  var tag = getTag(this, 'returns'),
      desc = _.toString(_.get(tag, 'description')),
      type = _.toString(_.get(tag, 'type.name')) || '*';

  return tag ? [type, desc] : [];
}

/**
 * Extracts the entry's `since` data.
 *
 * @memberOf Entry
 * @returns {string} Returns the entry's `since` data.
 */
function getSince() {
  return getValue(this, 'since');
}

/**
 * Extracts the entry's `type` data.
 *
 * @memberOf Entry
 * @returns {string} Returns the entry's `type` data.
 */
function getType() {
  var result = getValue(this, 'type');
  if (!result) {
    return this.isFunction() ? 'Function' : 'unknown';
  }
  return /^(?:array|function|object|regexp)$/.test(result)
    ? _.capitalize(result)
    : result;
}

/**
 * Checks if the entry is an alias.
 *
 * @memberOf Entry
 * @type {Function}
 * @returns {boolean} Returns `false`.
 */
var isAlias = _.constant(false);

/**
 * Checks if the entry is a constructor.
 *
 * @memberOf Entry
 * @returns {boolean} Returns `true` if a constructor, else `false`.
 */
function isCtor() {
  return hasTag(this, 'constructor');
}

/**
 * Checks if the entry is a function reference.
 *
 * @memberOf Entry
 * @returns {boolean} Returns `true` if the entry is a function reference, else `false`.
 */
function isFunction() {
  return !!(
    this.isCtor() ||
    _.size(this.getParams()) ||
    _.size(this.getReturns()) ||
    hasTag(this, 'function') ||
    /\*\/\s*(?:function\s+)?[^\s(]+\s*\(/.test(this.entry)
  );
}

/**
 * Checks if the entry is a license.
 *
 * @memberOf Entry
 * @returns {boolean} Returns `true` if a license, else `false`.
 */
function isLicense() {
  return hasTag(this, 'license');
}

/**
 * Checks if the entry *is* assigned to a prototype.
 *
 * @memberOf Entry
 * @returns {boolean} Returns `true` if assigned to a prototype, else `false`.
 */
function isPlugin() {
  return (
    !this.isCtor() &&
    !this.isPrivate() &&
    !this.isStatic()
  );
}

/**
 * Checks if the entry is private.
 *
 * @memberOf Entry
 * @returns {boolean} Returns `true` if private, else `false`.
 */
function isPrivate() {
  return (
    this.isLicense() ||
    hasTag(this, 'private') ||
    _.isEmpty(this.parsed.tags)
  );
}

/**
 * Checks if the entry is *not* assigned to a prototype.
 *
 * @memberOf Entry
 * @returns {boolean} Returns `true` if not assigned to a prototype, else `false`.
 */
function isStatic() {
  var isPublic = !this.isPrivate(),
      result = isPublic && hasTag(this, 'static');

  // Get the result in cases where it isn't explicitly stated.
  if (isPublic && !result) {
    var parent = _.last(_.toString(this.getMembers(0)).split(/[#.]/));
    if (!parent) {
      return true;
    }
    var source = this.source;
    _.each(getEntries(source), function(entry) {
      entry = new Entry(entry, source);
      if (entry.getName() == parent) {
        result = !entry.isCtor();
        return false;
      }
    });
  }
  return result;
}

/*----------------------------------------------------------------------------*/

Entry.getEntries = getEntries;

_.assign(Entry.prototype, {
  'getAliases': getAliases,
  'getCall': getCall,
  'getCategory': getCategory,
  'getDesc': getDesc,
  'getExample': getExample,
  'getHash': getHash,
  'getLineNumber': getLineNumber,
  'getMembers': getMembers,
  'getName': getName,
  'getParams': getParams,
  'getReturns': getReturns,
  'getSince': getSince,
  'getType': getType,
  'isAlias': isAlias,
  'isCtor': isCtor,
  'isFunction': isFunction,
  'isLicense': isLicense,
  'isPlugin': isPlugin,
  'isPrivate': isPrivate,
  'isStatic': isStatic
});

module.exports = Entry;
<|endoftext|>


<commit_before>import _ from 'lodash';
import irc from 'irc';
import logger from 'winston';
import { MemoryDataStore, RtmClient, WebClient, RTM_EVENTS,
  RTM_MESSAGE_SUBTYPES } from '@slack/client';
import { ConfigurationError } from './errors';
import emojis from '../assets/emoji.json';
import { validateChannelMapping } from './validators';
import { highlightUsername } from './helpers';

const ALLOWED_SUBTYPES = ['me_message'];
const REQUIRED_FIELDS = ['server', 'nickname', 'channelMapping', 'token'];
const NICK_SUFFIX = '-slack';
const NICK_REGEX = new RegExp(`^(\\w*)${NICK_SUFFIX}$`);

/**
 * An IRC bot, works as a middleman for all communication
 * @param {object} options
 */
class Bot {
  constructor(options) {
    REQUIRED_FIELDS.forEach(field => {
      if (!options[field]) {
        throw new ConfigurationError(`Missing configuration field ${field}`);
      }
    });

    validateChannelMapping(options.channelMapping);

    const web = new WebClient(options.token);
    const rtm = new RtmClient(options.token, { dataStore: new MemoryDataStore() });
    this.slack = { web, rtm };

    this.server = options.server;
    this.nickname = options.nickname;
    this.ircStatusNotices = options.ircStatusNotices || {};
    this.commandCharacters = options.commandCharacters || [];
    this.slackChannels = _.keys(options.channelMapping);
    this.ircChannels = _.values(options.channelMapping);
    this.muteSlackbot = options.muteSlackbot || false;

    const ircOptions = {
      userName: this.nickname,
      realName: this.nickname,
      channels: this.ircChannels,
      autoConnect: false,
      floodProtection: true,
      floodProtectionDelay: 500,
      retryCount: 10,
      ...options.ircOptions
    };
    this.ircOptions = ircOptions;
    this.ircClients = {};

    const defaultUrl = 'http://api.adorable.io/avatars/48/$username.png';
    // Disable if it's set to false, override default with custom if available:
    this.avatarUrl = options.avatarUrl !== false && (options.avatarUrl || defaultUrl);
    this.channelMapping = {};

    // Remove channel passwords from the mapping and lowercase IRC channel names
    _.forOwn(options.channelMapping, (ircChan, slackChan) => {
      this.channelMapping[slackChan] = ircChan.split(' ')[0].toLowerCase();
    }, this);

    this.invertedMapping = _.invert(this.channelMapping);
    this.autoSendCommands = options.autoSendCommands || [];
  }

  connect() {
    logger.debug('Connecting to IRC and Slack');
    this.slack.rtm.start();

    this.ircClient = new irc.Client(this.server, this.nickname, this.ircOptions);
    this.ircClient.connect(5, msg => {
      if (msg.rawCommand === '001') {
        logger.debug('IRC bot connected.');
      } else {
        throw new Error('IRC bot could not connect.');
      }
    });
    this.attachListeners();
  }

  attachListeners() {
    this.slack.rtm.on('open', () => {
      logger.debug('Connected to Slack');
      for (const key of this.slackChannels) {
        this.checkActiveUsers(key);
      }
    });

    this.ircClient.on('registered', message => {
      logger.debug('Registered event: ', message);
      this.autoSendCommands.forEach(element => {
        this.ircClient.send(...element);
      });
    });

    this.ircClient.on('error', error => {
      logger.error('Received error event from IRC', error);
    });

    this.ircClient.on('abort', () => {
      logger.error('Maximum IRC retry count reached, exiting.');
      process.exit(1);
    });

    this.slack.rtm.on('error', error => {
      logger.error('Received error event from Slack', error);
    });

    this.slack.rtm.on(RTM_EVENTS.MESSAGE, message => {
      // Ignore bot messages and people leaving/joining
      if (message.type === 'message' &&
        (!message.subtype || ALLOWED_SUBTYPES.indexOf(message.subtype) > -1)) {
        const { dataStore } = this.slack.rtm;
        const user = dataStore.getUserById(message.user);
        this.sendToIRC(user, message);
      }
    });

    this.slack.rtm.on(RTM_EVENTS.PRESENCE_CHANGE, event => {
      // Connect/disconnect based on 'active'/'away' Slack status
      const { dataStore } = this.slack.rtm;
      const user = dataStore.getUserById(event.user);
      logger.debug(`Slack user ${user.name} status changed to ${event.presence}.`);
      if (this.isBot(user.id)) return;
      if (event.presence === 'active') {
        this.connectNewClient(user);
      } else {
        this.deleteClient(user);
      }
    });

    this.slack.rtm.on(RTM_EVENTS.USER_CHANGE, event => {
      const client = this.ircClients[event.user.id];
      const ircNick = this.ircNick(event.user.name);
      if (ircNick !== client.nick) {
        logger.debug(`Slack user name change ${client.nick} -> ${ircNick}.`);
        client.send('NICK', ircNick);
      }
    });

    this.ircClient.on('message', this.sendToSlack.bind(this));

    this.ircClient.on('notice', (author, to, text) => {
      const formattedText = `*${text}*`;
      this.sendToSlack(author, to, formattedText);
    });

    this.ircClient.on('action', (author, to, text) => {
      const formattedText = `_${text}_`;
      this.sendToSlack(author, to, formattedText);
    });

    this.ircClient.on('invite', (channel, from) => {
      logger.debug('Received invite:', channel, from);
      if (!this.invertedMapping[channel]) {
        logger.debug('Channel not found in config, not joining:', channel);
      } else {
        this.ircClient.join(channel);
        logger.debug('Joining channel:', channel);
      }
    });

    if (this.ircStatusNotices.join) {
      this.ircClient.on('join', (channel, nick) => {
        if (nick !== this.nickname) {
          this.sendToSlack(this.nickname, channel, `*${nick}* has joined the IRC channel`);
        }
      });
    }

    if (this.ircStatusNotices.leave) {
      this.ircClient.on('part', (channel, nick) => {
        this.sendToSlack(this.nickname, channel, `*${nick}* has left the IRC channel`);
      });

      this.ircClient.on('quit', (nick, reason, channels) => {
        channels.forEach(channel => {
          this.sendToSlack(this.nickname, channel, `*${nick}* has quit the IRC channel`);
        });
      });
    }
  }

  parseText(text) {
    const { dataStore } = this.slack.rtm;
    return text
      .replace(/\n|\r\n|\r/g, ' ')
      .replace(/&/g, '&')
      .replace(/</g, '<')
      .replace(/>/g, '>')
      .replace(/<!channel>/g, '@channel')
      .replace(/<!group>/g, '@group')
      .replace(/<!everyone>/g, '@everyone')
      .replace(/<#(C\w+)\|?(\w+)?>/g, (match, channelId, readable) => {
        const { name } = dataStore.getChannelById(channelId);
        return readable || `#${name}`;
      })
      .replace(/<@(U\w+)\|?(\w+)?>/g, (match, userId, readable) => {
        const { name } = dataStore.getUserById(userId);
        return readable || `@${name}`;
      })
      .replace(/<(?!!)([^\|]+)>/g, (match, link) => link)
      .replace(/<!(\w+)\|?(\w+)?>/g, (match, command, label) =>
        `<${label || command}>`
      )
      .replace(/:(\w+):/g, (match, emoji) => {
        if (emoji in emojis) {
          return emojis[emoji];
        }

        return match;
      })
      .replace(/<.+\|(.+)>/g, (match, readable) => readable);
  }

  ircNick(slackName) {
    return `${slackName}${NICK_SUFFIX}`;
  }

  isBot(userId) {
    return this.slack.rtm.dataStore.getBotByUserId(userId) != null;
  }

  isCommandMessage(message) {
    return this.commandCharacters.indexOf(message[0]) !== -1;
  }

  connectNewClient(user) {
    logger.debug(`Connecting a new IRC client for Slack user ${user.name}.`);
    if (!(user.id in this.ircClients)) {
      const userOptions = Object.assign({}, this.ircOptions);
      userOptions.userName = user.name;
      if (this.ircClients[user.id] == null) {
        this.newClient(user, userOptions);
      }
    }
  }

  newClient(user, options) {
    const client = new irc.Client(this.server, this.ircNick(user.name), options);
    client.connect(5, msg => {
      if (msg.rawCommand === '001') {
        this.ircClients[user.id] = client;
        this.attachUserListeners(user);
        logger.debug(`User ${msg.args[0]} connected to IRC.`);
      } else {
        logger.debug(`Bot for ${user.name} could not connect to IRC.`);
      }
    });
  }

  deleteClient(user) {
    const client = this.ircClients[user.id];
    if (client != null) {
      client.disconnect(`User ${user.name} went away.`, () => {
        logger.debug(`Bot for Slack user ${user.name} disconnected.`);
        delete this.ircClients[user.id];
      });
    }
  }

  attachUserListeners(user) {
    const client = this.ircClients[user.id];
    client.on('error', error => {
      logger.error(`${user.name}: Received error event from IRC`, error);
    });

    client.on('abort', () => {
      logger.error(`${user.name}: Maximum IRC retry count reached, exiting.`);
      delete this.ircClients[user.id];
    });
  }

  sendToIRC(user, message) {
    const { dataStore } = this.slack.rtm;
    const channel = dataStore.getChannelGroupOrDMById(message.channel);
    const client = this.ircClients[user.id];
    if (client == null) {
      this.connectNewClient(user);
    }
    if (!channel) {
      logger.info(`Received message from a channel the user ${user.name} isn't in:`,
        message.channel);
      return;
    }

    const channelName = channel.is_channel ? `#${channel.name}` : channel.name;
    const ircChannel = this.channelMapping[channelName];

    logger.debug('Channel Mapping', channelName, this.channelMapping[channelName]);
    if (ircChannel) {
      const text = this.parseText(message.text);

      if (this.isCommandMessage(text)) {
        const prelude = `Command sent from Slack by ${user.name}:`;
        this.ircClient.say(ircChannel, prelude);
      } else if (!message.subtype) {
        logger.debug(`${user.name}: Sending message to IRC`, channelName, text);
        client.say(ircChannel, text);
      } else if (message.subtype === RTM_MESSAGE_SUBTYPES.ME_MESSAGE) {
        logger.debug(`${user.name}: Sending action to IRC`, channelName, text);
        client.action(ircChannel, text);
      }
    }
  }

  sendToSlack(author, channel, text) {
    const slackChannelName = this.invertedMapping[channel.toLowerCase()];
    if (slackChannelName) {
      const { dataStore } = this.slack.rtm;
      const name = slackChannelName.replace(/^#/, '');
      const slackChannel = dataStore.getChannelOrGroupByName(name);

      // If it's a private group and the bot isn't in it, we won't find anything here.
      // If it's a channel however, we need to check is_member.
      if (!slackChannel || (!slackChannel.is_member && !slackChannel.is_group)) {
        logger.info('Tried to send a message to a channel the bot isn\'t in: ',
          slackChannelName);
        return;
      }

      const currentChannelUsernames = slackChannel.members.map(member =>
        dataStore.getUserById(member).name
      );

      // Don't relay a shadow user's IRC bot message back to Slack
      const nickMatch = author.match(NICK_REGEX);
      if (nickMatch && currentChannelUsernames.indexOf(nickMatch[1]) > -1) {
        logger.debug(`Ignoring message from shadow user IRC bot '${author}'.`);
        return;
      }

      const mappedText = currentChannelUsernames.reduce((current, username) =>
        highlightUsername(username, current)
      , text);

      let iconUrl;
      if (author !== this.nickname && this.avatarUrl) {
        iconUrl = this.avatarUrl.replace(/\$username/g, author);
      }

      const options = {
        username: author,
        parse: 'full',
        icon_url: iconUrl
      };

      logger.debug('Sending message to Slack', mappedText, channel, '->', slackChannelName);
      this.slack.web.chat.postMessage(slackChannel.id, mappedText, options);
    }
  }

  checkActiveUsers(slackChannelName) {
    logger.debug(`Creating clients for active users on connect for channel ${slackChannelName}.`);
    const { dataStore } = this.slack.rtm;
    const name = slackChannelName.replace(/^#/, '');
    const slackChannel = dataStore.getChannelOrGroupByName(name);
    for (const member of slackChannel.members) {
      const user = dataStore.getUserById(member);
      if (user.presence === 'active') {
        this.connectNewClient(user);
      }
    }
  }
}

export default Bot;
<commit_msg>Limit length of IRC usernames<commit_after>import _ from 'lodash';
import irc from 'irc';
import logger from 'winston';
import { MemoryDataStore, RtmClient, WebClient, RTM_EVENTS,
  RTM_MESSAGE_SUBTYPES } from '@slack/client';
import { ConfigurationError } from './errors';
import emojis from '../assets/emoji.json';
import { validateChannelMapping } from './validators';
import { highlightUsername } from './helpers';

const ALLOWED_SUBTYPES = ['me_message'];
const REQUIRED_FIELDS = ['server', 'nickname', 'channelMapping', 'token'];
const NICK_SUFFIX = '-sl';
const SERVER_NICKLEN = 16;

/**
 * An IRC bot, works as a middleman for all communication
 * @param {object} options
 */
class Bot {
  constructor(options) {
    REQUIRED_FIELDS.forEach(field => {
      if (!options[field]) {
        throw new ConfigurationError(`Missing configuration field ${field}`);
      }
    });

    validateChannelMapping(options.channelMapping);

    const web = new WebClient(options.token);
    const rtm = new RtmClient(options.token, { dataStore: new MemoryDataStore() });
    this.slack = { web, rtm };

    this.server = options.server;
    this.nickname = options.nickname;
    this.ircStatusNotices = options.ircStatusNotices || {};
    this.commandCharacters = options.commandCharacters || [];
    this.slackChannels = _.keys(options.channelMapping);
    this.ircChannels = _.values(options.channelMapping);
    this.muteSlackbot = options.muteSlackbot || false;

    const ircOptions = {
      userName: this.nickname,
      realName: this.nickname,
      channels: this.ircChannels,
      autoConnect: false,
      floodProtection: true,
      floodProtectionDelay: 500,
      retryCount: 10,
      ...options.ircOptions
    };
    this.ircOptions = ircOptions;
    this.ircClients = {};

    const defaultUrl = 'http://api.adorable.io/avatars/48/$username.png';
    // Disable if it's set to false, override default with custom if available:
    this.avatarUrl = options.avatarUrl !== false && (options.avatarUrl || defaultUrl);
    this.channelMapping = {};

    // Remove channel passwords from the mapping and lowercase IRC channel names
    _.forOwn(options.channelMapping, (ircChan, slackChan) => {
      this.channelMapping[slackChan] = ircChan.split(' ')[0].toLowerCase();
    }, this);

    this.invertedMapping = _.invert(this.channelMapping);
    this.autoSendCommands = options.autoSendCommands || [];
  }

  connect() {
    logger.debug('Connecting to IRC and Slack');
    this.slack.rtm.start();

    this.ircClient = new irc.Client(this.server, this.nickname, this.ircOptions);
    this.ircClient.connect(5, msg => {
      if (msg.rawCommand === '001') {
        logger.debug('IRC bot connected.');
      } else {
        throw new Error('IRC bot could not connect.');
      }
    });
    this.attachListeners();
  }

  attachListeners() {
    this.slack.rtm.on('open', () => {
      logger.debug('Connected to Slack');
      for (const key of this.slackChannels) {
        this.checkActiveUsers(key);
      }
    });

    this.ircClient.on('registered', message => {
      logger.debug('Registered event: ', message);
      this.autoSendCommands.forEach(element => {
        this.ircClient.send(...element);
      });
    });

    this.ircClient.on('error', error => {
      logger.error('Received error event from IRC', error);
    });

    this.ircClient.on('abort', () => {
      logger.error('Maximum IRC retry count reached, exiting.');
      process.exit(1);
    });

    this.slack.rtm.on('error', error => {
      logger.error('Received error event from Slack', error);
    });

    this.slack.rtm.on(RTM_EVENTS.MESSAGE, message => {
      // Ignore bot messages and people leaving/joining
      if (message.type === 'message' &&
        (!message.subtype || ALLOWED_SUBTYPES.indexOf(message.subtype) > -1)) {
        const { dataStore } = this.slack.rtm;
        const user = dataStore.getUserById(message.user);
        this.sendToIRC(user, message);
      }
    });

    this.slack.rtm.on(RTM_EVENTS.PRESENCE_CHANGE, event => {
      // Connect/disconnect based on 'active'/'away' Slack status
      const { dataStore } = this.slack.rtm;
      const user = dataStore.getUserById(event.user);
      logger.debug(`Slack user ${user.name} status changed to ${event.presence}.`);
      if (this.isBot(user.id)) return;
      if (event.presence === 'active') {
        this.connectNewClient(user);
      } else {
        this.deleteClient(user);
      }
    });

    this.slack.rtm.on(RTM_EVENTS.USER_CHANGE, event => {
      const client = this.ircClients[event.user.id];
      const ircNick = this.ircNick(event.user.name);
      if (ircNick !== client.nick) {
        logger.debug(`Slack user name change ${client.nick} -> ${ircNick}.`);
        client.send('NICK', ircNick);
      }
    });

    this.ircClient.on('message', this.sendToSlack.bind(this));

    this.ircClient.on('notice', (author, to, text) => {
      const formattedText = `*${text}*`;
      this.sendToSlack(author, to, formattedText);
    });

    this.ircClient.on('action', (author, to, text) => {
      const formattedText = `_${text}_`;
      this.sendToSlack(author, to, formattedText);
    });

    this.ircClient.on('invite', (channel, from) => {
      logger.debug('Received invite:', channel, from);
      if (!this.invertedMapping[channel]) {
        logger.debug('Channel not found in config, not joining:', channel);
      } else {
        this.ircClient.join(channel);
        logger.debug('Joining channel:', channel);
      }
    });

    if (this.ircStatusNotices.join) {
      this.ircClient.on('join', (channel, nick) => {
        if (nick !== this.nickname) {
          this.sendToSlack(this.nickname, channel, `*${nick}* has joined the IRC channel`);
        }
      });
    }

    if (this.ircStatusNotices.leave) {
      this.ircClient.on('part', (channel, nick) => {
        this.sendToSlack(this.nickname, channel, `*${nick}* has left the IRC channel`);
      });

      this.ircClient.on('quit', (nick, reason, channels) => {
        channels.forEach(channel => {
          this.sendToSlack(this.nickname, channel, `*${nick}* has quit the IRC channel`);
        });
      });
    }
  }

  parseText(text) {
    const { dataStore } = this.slack.rtm;
    return text
      .replace(/\n|\r\n|\r/g, ' ')
      .replace(/&/g, '&')
      .replace(/</g, '<')
      .replace(/>/g, '>')
      .replace(/<!channel>/g, '@channel')
      .replace(/<!group>/g, '@group')
      .replace(/<!everyone>/g, '@everyone')
      .replace(/<#(C\w+)\|?(\w+)?>/g, (match, channelId, readable) => {
        const { name } = dataStore.getChannelById(channelId);
        return readable || `#${name}`;
      })
      .replace(/<@(U\w+)\|?(\w+)?>/g, (match, userId, readable) => {
        const { name } = dataStore.getUserById(userId);
        return readable || `@${name}`;
      })
      .replace(/<(?!!)([^\|]+)>/g, (match, link) => link)
      .replace(/<!(\w+)\|?(\w+)?>/g, (match, command, label) =>
        `<${label || command}>`
      )
      .replace(/:(\w+):/g, (match, emoji) => {
        if (emoji in emojis) {
          return emojis[emoji];
        }

        return match;
      })
      .replace(/<.+\|(.+)>/g, (match, readable) => readable);
  }

  ircNick(slackName) {
    return slackName.substr(0, SERVER_NICKLEN - NICK_SUFFIX.length) + NICK_SUFFIX;
  }

  isBot(userId) {
    return this.slack.rtm.dataStore.getBotByUserId(userId) != null;
  }

  isCommandMessage(message) {
    return this.commandCharacters.indexOf(message[0]) !== -1;
  }

  connectNewClient(user) {
    logger.debug(`Connecting a new IRC client for Slack user ${user.name}.`);
    if (!(user.id in this.ircClients)) {
      const userOptions = Object.assign({}, this.ircOptions);
      userOptions.userName = user.name;
      if (this.ircClients[user.id] == null) {
        this.newClient(user, userOptions);
      }
    }
  }

  newClient(user, options) {
    const client = new irc.Client(this.server, this.ircNick(user.name), options);
    client.connect(5, msg => {
      if (msg.rawCommand === '001') {
        client.slackName = user.name;
        this.ircClients[user.id] = client;
        this.attachUserListeners(user);
        logger.debug(`User ${client.nick} connected to IRC.`);
      } else {
        logger.debug(`Bot for ${user.name} could not connect to IRC.`);
      }
    });
  }

  deleteClient(user) {
    const client = this.ircClients[user.id];
    if (client != null) {
      client.disconnect(`User ${user.name} went away.`, () => {
        logger.debug(`Bot for Slack user ${user.name} disconnected.`);
        delete this.ircClients[user.id];
      });
    }
  }

  attachUserListeners(user) {
    const client = this.ircClients[user.id];
    client.on('error', error => {
      logger.error(`${user.name}: Received error event from IRC`, error);
    });

    client.on('abort', () => {
      logger.error(`${user.name}: Maximum IRC retry count reached, exiting.`);
      delete this.ircClients[user.id];
    });
  }

  sendToIRC(user, message) {
    const { dataStore } = this.slack.rtm;
    const channel = dataStore.getChannelGroupOrDMById(message.channel);
    const client = this.ircClients[user.id];
    if (client == null) {
      this.connectNewClient(user);
    }
    if (!channel) {
      logger.info(`Received message from a channel the user ${user.name} isn't in:`,
        message.channel);
      return;
    }

    const channelName = channel.is_channel ? `#${channel.name}` : channel.name;
    const ircChannel = this.channelMapping[channelName];

    logger.debug('Channel Mapping', channelName, this.channelMapping[channelName]);
    if (ircChannel) {
      const text = this.parseText(message.text);

      if (this.isCommandMessage(text)) {
        const prelude = `Command sent from Slack by ${user.name}:`;
        this.ircClient.say(ircChannel, prelude);
      } else if (!message.subtype) {
        logger.debug(`${user.name}: Sending message to IRC`, channelName, text);
        client.say(ircChannel, text);
      } else if (message.subtype === RTM_MESSAGE_SUBTYPES.ME_MESSAGE) {
        logger.debug(`${user.name}: Sending action to IRC`, channelName, text);
        client.action(ircChannel, text);
      }
    }
  }

  sendToSlack(author, channel, text) {
    const slackChannelName = this.invertedMapping[channel.toLowerCase()];
    if (slackChannelName) {
      const { dataStore } = this.slack.rtm;
      const name = slackChannelName.replace(/^#/, '');
      const slackChannel = dataStore.getChannelOrGroupByName(name);

      // If it's a private group and the bot isn't in it, we won't find anything here.
      // If it's a channel however, we need to check is_member.
      if (!slackChannel || (!slackChannel.is_member && !slackChannel.is_group)) {
        logger.info('Tried to send a message to a channel the bot isn\'t in: ',
          slackChannelName);
        return;
      }

      const currentChannelUsernames = slackChannel.members.map(member =>
        dataStore.getUserById(member).name
      );

      // Don't relay a shadow user's IRC bot message back to Slack
      const currentShadowUsernames = _.keys(this.ircClients).map(userId =>
        this.ircClients[userId].nick
      );
      if (currentShadowUsernames.indexOf(author) > -1) {
        logger.debug(`Ignoring message from shadow user IRC bot '${author}'.`);
        return;
      }

      const mappedText = currentChannelUsernames.reduce((current, username) =>
        highlightUsername(username, current)
      , text);

      let iconUrl;
      if (author !== this.nickname && this.avatarUrl) {
        iconUrl = this.avatarUrl.replace(/\$username/g, author);
      }

      const options = {
        username: author,
        parse: 'full',
        icon_url: iconUrl
      };

      logger.debug('Sending message to Slack', mappedText, channel, '->', slackChannelName);
      this.slack.web.chat.postMessage(slackChannel.id, mappedText, options);
    }
  }

  checkActiveUsers(slackChannelName) {
    logger.debug(`Creating clients for active users on connect for channel ${slackChannelName}.`);
    const { dataStore } = this.slack.rtm;
    const name = slackChannelName.replace(/^#/, '');
    const slackChannel = dataStore.getChannelOrGroupByName(name);
    for (const member of slackChannel.members) {
      const user = dataStore.getUserById(member);
      if (user.presence === 'active') {
        this.connectNewClient(user);
      }
    }
  }
}

export default Bot;
<|endoftext|>


<commit_before>/* global describe, it */

var app = require('../app/index');
var request = require('supertest').agent(app.listen());

describe('GET /', function () {
  it('status code should be 200', function (done) {
    request
      .get('/')
      .expect(200, done);
  });
});

describe('GET user id from params', function () {
  it('should display "1024"', function (done) {
    request
      .get('/users/1024')
      .expect('1024', done);
  });
});
<commit_msg>add test on static file serving<commit_after>/* global describe, it */

var app = require('../app/index');
var request = require('supertest').agent(app.listen());

describe('GET /', function () {
  it('status code should be 200', function (done) {
    request
      .get('/')
      .expect(200, done);
  });
});

describe('GET user id from params', function () {
  it('should display "1024"', function (done) {
    request
      .get('/users/1024')
      .expect('1024', done);
  });
});

describe('GET static file', function () {
  it('status code should be 200', function (done) {
    request
      .get('/notice.html')
      .expect(200, done);
  });
});<|endoftext|>


<commit_before>"use strict";

import os from 'os';
import EnumItem from './enumItem';
import { isString } from './isType';
import { indexOf } from './indexOf';

const endianness = os.endianness();

var reservedKeys = ['_options', 'get', 'getKey', 'getValue', 'enums', 'isFlaggable'];

/**
 * Represents an Enum with enum items.
 * @param {Array || Object}  map     This are the enum items.
 * @param {String || Object} options This are options. [optional]
 */
export default class Enum {
  constructor(map, options) {
    /* implement the "ref type interface", so that Enum types can
     * be used in `node-ffi` function declarations and invokations.
     * In C, these Enums act as `uint32_t` types.
     *
     * https://github.com/TooTallNate/ref#the-type-interface
     */
    this.size = 4;
    this.indirection = 1;

    if (options && isString(options)) {
      options = { name: options };
    }

    this._options = options || {};
    this._options.separator = this._options.separator || " | ";
    this._options.endianness = this._options.endianness || endianness;
    this._options.ignoreCase = this._options.ignoreCase || false;

    this.enums = [];

    if (map.length) {
      var array = map;
      map = {};

      for (var i = 0; i < array.length; i++) {
        map[array[i]] = Math.pow(2, i);
      }
    }

    for (var member in map) {
      if ((this._options.name && member === 'name') || indexOf.call(reservedKeys, member) >= 0) {
        throw new Error("Enum key \"" + member + "\" is a reserved word!");
      }
      this[member] = new EnumItem(member, map[member], { ignoreCase: this._options.ignoreCase });
      this.enums.push(this[member]);
    }

    if (this._options.ignoreCase) {
      this.getLowerCaseEnums = function () {
        var res = {};
        for (var i = 0, len = this.enums.length; i < len; i++) {
          res[this.enums[i].key.toLowerCase()] = this.enums[i];
        }
        return res;
      };
    }

    if (this._options.name) {
      this.name = this._options.name;
    }

    const isFlaggable = () => {
      for (var i = 0, len = this.enums.length; i < len; i++) {
        var e = this.enums[i];

        if (!(e.value !== 0 && !(e.value & e.value - 1))) {
          return false;
        }
      }
      return true;
    }

    this.isFlaggable = isFlaggable();
    this.freezeEnums(); //this will make instances of Enum non-extensible
  }

  /**
   * Returns the appropriate EnumItem key.
   * @param  {EnumItem || String || Number} key The object to get with.
   * @return {String}                           The get result.
   */
  getKey(value) {
    var item = this.get(value);
    if (item) {
      return item.key;
    }
  }

  /**
   * Returns the appropriate EnumItem value.
   * @param  {EnumItem || String || Number} key The object to get with.
   * @return {Number}                           The get result.
   */
  getValue(key) {
    var item = this.get(key);
    if (item) {
      return item.value;
    }
  }

  /**
   * Returns the appropriate EnumItem.
   * @param  {EnumItem || String || Number} key The object to get with.
   * @return {EnumItem}                         The get result.
   */
  get(key, offset) {
    if (key === null || key === undefined) {
      return;
    } // Buffer instance support, part of the ref Type interface
    if (Buffer.isBuffer(key)) {
      key = key["readUInt32" + this._options.endianness](offset || 0);
    }

    if (EnumItem.isEnumItem(key)) {
      var foundIndex = indexOf.call(this.enums, key);
      if (foundIndex >= 0) {
        return key;
      }
      if (!this.isFlaggable || this.isFlaggable && key.key.indexOf(this._options.separator) < 0) {
        return;
      }
      return this.get(key.key);
    } else if (isString(key)) {

      var enums = this;
      if (this._options.ignoreCase) {
        enums = this.getLowerCaseEnums();
        key = key.toLowerCase();
      }

      if (key.indexOf(this._options.separator) > 0) {
        var parts = key.split(this._options.separator);

        var value = 0;
        for (var i = 0; i < parts.length; i++) {
          var part = parts[i];

          value |= enums[part].value;
        }

        return new EnumItem(key, value);
      } else {
        return enums[key];
      }
    } else {
      for (var m in this) {
        if (this.hasOwnProperty(m)) {
          if (this[m].value === key) {
            return this[m];
          }
        }
      }

      var result = null;

      if (this.isFlaggable) {
        for (var n in this) {
          if (this.hasOwnProperty(n)) {
            if ((key & this[n].value) !== 0) {
              if (result) {
                result += this._options.separator;
              } else {
                result = "";
              }
              result += n;
            }
          }
        }
      }

      return this.get(result || null);
    }
  }

  /**
   * Sets the Enum "value" onto the give `buffer` at the specified `offset`.
   * Part of the ref "Type interface".
   *
   * @param  {Buffer} buffer The Buffer instance to write to.
   * @param  {Number} offset The offset in the buffer to write to. Default 0.
   * @param  {EnumItem || String || Number} value The EnumItem to write.
   */
  set(buffer, offset, value) {
    var item = this.get(value);
    if (item) {
      return buffer["writeUInt32" + this._options.endianness](item.value, offset || 0);
    }
  }

  /**
   * Define freezeEnums() as a property of the prototype.
   * make enumerable items nonconfigurable and deep freeze the properties. Throw Error on property setter.
   */
  freezeEnums() {
    function envSupportsFreezing() {
      return (
        Object.isFrozen && Object.isSealed &&
        Object.getOwnPropertyNames && Object.getOwnPropertyDescriptor &&
        Object.defineProperties && Object.__defineGetter__ && Object.__defineSetter__
      );
    }

    function freezer(o) {
      var props = Object.getOwnPropertyNames(o);
      props.forEach(function (p) {
        if (!Object.getOwnPropertyDescriptor(o, p).configurable) {
          return;
        }

        Object.defineProperties(o, p, { writable: false, configurable: false });
      });
      return o;
    }

    function getPropertyValue(value) {
      return value;
    }

    function deepFreezeEnums(o) {
      if (typeof o !== "object" || o === null || Object.isFrozen(o) || Object.isSealed(o)) {
        return;
      }
      for (var key in o) {
        if (o.hasOwnProperty(key)) {
          o.__defineGetter__(key, getPropertyValue.bind(null, o[key]));
          o.__defineSetter__(key, function throwPropertySetError(value) {
            throw TypeError("Cannot redefine property; Enum Type is not extensible.");
          });
          deepFreezeEnums(o[key]);
        }
      }
      if (Object.freeze) {
        Object.freeze(o);
      } else {
        freezer(o);
      }
    }

    if (envSupportsFreezing() ) {
      deepFreezeEnums(this);
    }

    return this;
  }

  /**
   * Registers the Enum Type globally in node.js.
   * @param  {String} key Global variable. [optional]
   */
  static register(key='Enum') {
    if (!global[key]) {
      global[key] = Enum;
    }
  }
};

<commit_msg>factor out private guard clause<commit_after>"use strict";

import os from 'os';
import EnumItem from './enumItem';
import { isString } from './isType';
import { indexOf } from './indexOf';

const endianness = os.endianness();

/**
 * Represents an Enum with enum items.
 * @param {Array || Object}  map     This are the enum items.
 * @param {String || Object} options This are options. [optional]
 */
export default class Enum {
  constructor(map, options) {
    /* implement the "ref type interface", so that Enum types can
     * be used in `node-ffi` function declarations and invokations.
     * In C, these Enums act as `uint32_t` types.
     *
     * https://github.com/TooTallNate/ref#the-type-interface
     */
    this.size = 4;
    this.indirection = 1;

    if (options && isString(options)) {
      options = { name: options };
    }

    this._options = options || {};
    this._options.separator = this._options.separator || " | ";
    this._options.endianness = this._options.endianness || endianness;
    this._options.ignoreCase = this._options.ignoreCase || false;

    this.enums = [];

    if (map.length) {
      var array = map;
      map = {};

      for (var i = 0; i < array.length; i++) {
        map[array[i]] = Math.pow(2, i);
      }
    }

    for (var member in map) {
      guardReservedKeys(this._options.name, member);
      this[member] = new EnumItem(member, map[member], { ignoreCase: this._options.ignoreCase });
      this.enums.push(this[member]);
    }

    if (this._options.ignoreCase) {
      this.getLowerCaseEnums = function () {
        var res = {};
        for (var i = 0, len = this.enums.length; i < len; i++) {
          res[this.enums[i].key.toLowerCase()] = this.enums[i];
        }
        return res;
      };
    }

    if (this._options.name) {
      this.name = this._options.name;
    }

    const isFlaggable = () => {
      for (var i = 0, len = this.enums.length; i < len; i++) {
        var e = this.enums[i];

        if (!(e.value !== 0 && !(e.value & e.value - 1))) {
          return false;
        }
      }
      return true;
    }

    this.isFlaggable = isFlaggable();
    this.freezeEnums(); //this will make instances of Enum non-extensible
  }

  /**
   * Returns the appropriate EnumItem key.
   * @param  {EnumItem || String || Number} key The object to get with.
   * @return {String}                           The get result.
   */
  getKey(value) {
    var item = this.get(value);
    if (item) {
      return item.key;
    }
  }

  /**
   * Returns the appropriate EnumItem value.
   * @param  {EnumItem || String || Number} key The object to get with.
   * @return {Number}                           The get result.
   */
  getValue(key) {
    var item = this.get(key);
    if (item) {
      return item.value;
    }
  }

  /**
   * Returns the appropriate EnumItem.
   * @param  {EnumItem || String || Number} key The object to get with.
   * @return {EnumItem}                         The get result.
   */
  get(key, offset) {
    if (key === null || key === undefined) {
      return;
    } // Buffer instance support, part of the ref Type interface
    if (Buffer.isBuffer(key)) {
      key = key["readUInt32" + this._options.endianness](offset || 0);
    }

    if (EnumItem.isEnumItem(key)) {
      var foundIndex = indexOf.call(this.enums, key);
      if (foundIndex >= 0) {
        return key;
      }
      if (!this.isFlaggable || this.isFlaggable && key.key.indexOf(this._options.separator) < 0) {
        return;
      }
      return this.get(key.key);
    } else if (isString(key)) {

      var enums = this;
      if (this._options.ignoreCase) {
        enums = this.getLowerCaseEnums();
        key = key.toLowerCase();
      }

      if (key.indexOf(this._options.separator) > 0) {
        var parts = key.split(this._options.separator);

        var value = 0;
        for (var i = 0; i < parts.length; i++) {
          var part = parts[i];

          value |= enums[part].value;
        }

        return new EnumItem(key, value);
      } else {
        return enums[key];
      }
    } else {
      for (var m in this) {
        if (this.hasOwnProperty(m)) {
          if (this[m].value === key) {
            return this[m];
          }
        }
      }

      var result = null;

      if (this.isFlaggable) {
        for (var n in this) {
          if (this.hasOwnProperty(n)) {
            if ((key & this[n].value) !== 0) {
              if (result) {
                result += this._options.separator;
              } else {
                result = "";
              }
              result += n;
            }
          }
        }
      }

      return this.get(result || null);
    }
  }

  /**
   * Sets the Enum "value" onto the give `buffer` at the specified `offset`.
   * Part of the ref "Type interface".
   *
   * @param  {Buffer} buffer The Buffer instance to write to.
   * @param  {Number} offset The offset in the buffer to write to. Default 0.
   * @param  {EnumItem || String || Number} value The EnumItem to write.
   */
  set(buffer, offset, value) {
    var item = this.get(value);
    if (item) {
      return buffer["writeUInt32" + this._options.endianness](item.value, offset || 0);
    }
  }

  /**
   * Define freezeEnums() as a property of the prototype.
   * make enumerable items nonconfigurable and deep freeze the properties. Throw Error on property setter.
   */
  freezeEnums() {
    function envSupportsFreezing() {
      return (
        Object.isFrozen && Object.isSealed &&
        Object.getOwnPropertyNames && Object.getOwnPropertyDescriptor &&
        Object.defineProperties && Object.__defineGetter__ && Object.__defineSetter__
      );
    }

    function freezer(o) {
      var props = Object.getOwnPropertyNames(o);
      props.forEach(function (p) {
        if (!Object.getOwnPropertyDescriptor(o, p).configurable) {
          return;
        }

        Object.defineProperties(o, p, { writable: false, configurable: false });
      });
      return o;
    }

    function getPropertyValue(value) {
      return value;
    }

    function deepFreezeEnums(o) {
      if (typeof o !== "object" || o === null || Object.isFrozen(o) || Object.isSealed(o)) {
        return;
      }
      for (var key in o) {
        if (o.hasOwnProperty(key)) {
          o.__defineGetter__(key, getPropertyValue.bind(null, o[key]));
          o.__defineSetter__(key, function throwPropertySetError(value) {
            throw TypeError("Cannot redefine property; Enum Type is not extensible.");
          });
          deepFreezeEnums(o[key]);
        }
      }
      if (Object.freeze) {
        Object.freeze(o);
      } else {
        freezer(o);
      }
    }

    if (envSupportsFreezing() ) {
      deepFreezeEnums(this);
    }

    return this;
  }

  /**
   * Registers the Enum Type globally in node.js.
   * @param  {String} key Global variable. [optional]
   */
  static register(key='Enum') {
    if (!global[key]) {
      global[key] = Enum;
    }
  }
};

// private

var reservedKeys = ['_options', 'get', 'getKey', 'getValue', 'enums', 'isFlaggable'];

function guardReservedKeys(customName, key) {
  if ((customName && key === 'name') || indexOf.call(reservedKeys, key) >= 0) {
    throw new Error("Enum key \"" + key + "\" is a reserved word!");
  }
}
<|endoftext|>


<commit_before>import classNames from 'classnames';
import {DCOSStore, ResourceTableUtil} from 'foundation-ui';
import {Confirm, Table} from 'reactjs-components';
import {Link} from 'react-router';
import mixin from 'reactjs-mixin';
/* eslint-disable no-unused-vars */
import React from 'react';
/* eslint-enable no-unused-vars */
import {StoreMixin} from 'mesosphere-shared-reactjs';

import AlertPanel from '../../../../../../src/js/components/AlertPanel';
import CollapsingString from '../../../../../../src/js/components/CollapsingString';
import defaultServiceImage from '../../../img/icon-service-default-small@2x.png';
import Loader from '../../../../../../src/js/components/Loader';
import MarathonActions from '../../events/MarathonActions';
import ModalHeading from '../../../../../../src/js/components/modals/ModalHeading';
import NestedServiceLinks from '../../../../../../src/js/components/NestedServiceLinks';
import Page from '../../../../../../src/js/components/Page';
import StatusBar from '../../../../../../src/js/components/StatusBar';
import StringUtil from '../../../../../../src/js/utils/StringUtil';
import TimeAgo from '../../../../../../src/js/components/TimeAgo';

const columnHeading = ResourceTableUtil.renderHeading({
  id: 'AFFECTED SERVICES',
  startTime: 'STARTED',
  location: 'LOCATION',
  status: 'STATUS',
  action: ''
});
const COLLAPSING_COLUMNS = ['location', 'startTime', 'action'];
const METHODS_TO_BIND = [
  'renderAffectedServices',
  'renderAffectedServicesList',
  'renderLocation',
  'renderStartTime',
  'renderStatus',
  'renderAction',
  'handleRollbackClick',
  'handleRollbackCancel',
  'handleRollbackConfirm',
  'onMarathonStoreDeploymentRollbackSuccess',
  'onMarathonStoreDeploymentRollbackError'
];

// collapsing columns are tightly coupled to the left-align caret property;
// this wrapper allows ordinary columns to collapse.
function columnClassNameGetter(prop, sortBy, row) {
  let classSet = ResourceTableUtil.getClassName(prop, sortBy, row);
  if (COLLAPSING_COLUMNS.includes(prop)) {
    return classNames(classSet, 'hidden-small-down');
  }
  return classSet;
}

const DeploymentsBreadcrumbs = () => {
  const crumbs = [
    <Link to="services" key={-1}>Services</Link>,
    <Link to="services/deployments" key={0}>Deployments</Link>
  ];

  return <Page.Header.Breadcrumbs iconID="services" breadcrumbs={crumbs} />;
};

class DeploymentsTab extends mixin(StoreMixin) {

  constructor() {
    super(...arguments);

    this.state = {};
    this.store_listeners = [
      {name: 'dcos', events: ['change'], suppressUpdate: false},
      {
        name: 'marathon',
        events: ['deploymentRollbackSuccess', 'deploymentRollbackError'],
        suppressUpdate: true
      }
    ];

    METHODS_TO_BIND.forEach((method) => {
      this[method] = this[method].bind(this);
    }, this);
  }

  onMarathonStoreDeploymentRollbackSuccess(data) {
    let {deploymentToRollback} = this.state;
    if (deploymentToRollback != null &&
        deploymentToRollback.getId() === data.originalDeploymentID) {
      this.setState({
        awaitingRevertDeploymentResponse: false,
        deploymentToRollback: null,
        deploymentRollbackError: null
      });
    }
  }

  onMarathonStoreDeploymentRollbackError(data) {
    let {deploymentToRollback} = this.state;
    if (deploymentToRollback != null &&
        deploymentToRollback.getId() === data.originalDeploymentID) {
      this.setState({
        awaitingRevertDeploymentResponse: false,
        deploymentRollbackError: data.error
      });
    }
  }

  renderAffectedServices(prop, deployment) {
    return (

          <CollapsingString string={deployment.getId()} />

        {this.renderAffectedServicesList(deployment.getAffectedServices())}
        {this.renderStaleServicesList(deployment.getStaleServiceIds())}

    );
  }

  renderStaleServicesList(serviceIds) {
    if (serviceIds == null) {
      return;
    }
    return serviceIds.map(function (serviceId) {
      return (
        <dd key={`stale_${serviceId}`}>

          {serviceId}

      );
    });
  }

  renderAffectedServicesList(services) {
    return services.map(function (service) {
      const id = encodeURIComponent(service.getId());
      const image = service.getImages()['icon-small'];

      return (
        <dd key={`service_${id}`}>
          <Link to={`/services/overview/${id}`} className="deployment-service-name table-cell-link-primary">

            {service.getName()}
          </Link>

      );
    });
  }

  renderLocation(prop, deployment) {
    const services = deployment.getAffectedServices();
    const items = services.map(function (service, index) {
      return (

          <NestedServiceLinks
            serviceID={service.getId()}
            className="deployment-breadcrumb"
            majorLinkClassName="deployment-breadcrumb-service-id table-cell-link-secondary"
            minorLinkWrapperClassName="deployment-breadcrumb-crumb table-cell-link-secondary" />

      );
    });

    return (

        {items}

    );
  }

  renderStartTime(prop, deployment) {
    return (
      <TimeAgo
        className="deployment-start-time"
        time={deployment.getStartTime()} />
    );
  }

  renderStatus(prop, deployment) {
    const currentStep = deployment.getCurrentStep();
    const totalSteps = deployment.getTotalSteps();
    const title = `Step ${currentStep} of ${totalSteps}`;
    const statusBar = this.renderStatusBar(currentStep, totalSteps);
    const services = deployment.getAffectedServices();

    let currentActions = {};
    if (deployment.currentActions && deployment.currentActions.length > 0) {
      currentActions =
        deployment.currentActions.reduce(function (memo, action) {
          memo[action.app] = action.action;

          return memo;
        }, {});
    }

    const items = services.map(function (service, index) {
      let statusText = service.getStatus();
      if (currentActions[service.id] != null) {
        statusText += ` (${currentActions[service.id]})`;
      }

      return (

          {statusText}

      );
    });

    return (

        {statusBar}
        {title}

{items}

    );
  }

  renderStatusBar(currentStep, totalSteps) {
    let data = [
      {className: 'color-4', value: currentStep - 1},
      {className: 'staged', value: 1},
      {className: '', value: totalSteps - currentStep}
    ];

    return (
      <StatusBar className="status-bar-large" data={data} />
    );
  }

  renderAction(prop, deployment) {
    if (deployment != null) {
      let linkText = 'Rollback';
      if (deployment.isStarting()) {
        linkText = linkText + ' & Remove';
      }
      return (

          {linkText}

      );
    }
  }

  handleRollbackClick(deployment) {
    this.setState({deploymentToRollback: deployment});
  }

  handleRollbackCancel() {
    this.setState({
      awaitingRevertDeploymentResponse: false,
      deploymentToRollback: null,
      deploymentRollbackError: null
    });
  }

  handleRollbackConfirm() {
    let {deploymentToRollback} = this.state;
    if (deploymentToRollback != null) {
      this.setState({awaitingRevertDeploymentResponse: true});
      MarathonActions.revertDeployment(deploymentToRollback.getId());
    }
  }

  getColumns() {
    return [
      {
        className: columnClassNameGetter,
        heading: columnHeading,
        prop: 'id',
        render: this.renderAffectedServices
      },
      {
        className: columnClassNameGetter,
        heading: columnHeading,
        prop: 'location',
        render: this.renderLocation
      },
      {
        className: columnClassNameGetter,
        heading: columnHeading,
        prop: 'startTime',
        render: this.renderStartTime
      },
      {
        className: columnClassNameGetter,
        heading: columnHeading,
        prop: 'status',
        render: this.renderStatus
      },
      {
        className: columnClassNameGetter,
        heading: columnHeading,
        prop: 'action',
        render: this.renderAction
      }
    ];
  }

  getColGroup() {
    return (
      <colgroup>
        <col style={{width: '300px'}} />
        <col className="hidden-small-down" />
        <col className="hidden-small-down" style={{width: '120px'}} />
        <col style={{width: '240px'}} />
        <col className="hidden-small-down" style={{width: '160px'}} />
      </colgroup>
    );
  }

  renderEmpty() {
    return (
      <Page>
        <Page.Header breadcrumbs={<DeploymentsBreadcrumbs/>} />
        <AlertPanel
          title="No active deployments">

Active deployments will be shown here.

        </AlertPanel>
      </Page>
    );
  }

  renderLoading() {
    return <Loader />;
  }

  renderPopulated(deploymentsItems) {
    let deploymentsCount = deploymentsItems.length;
    let deploymentsLabel = StringUtil.pluralize('Deployment', deploymentsCount);

    return (
      <Page>
        <Page.Header breadcrumbs={<DeploymentsBreadcrumbs />}>
          <Link to="services">Services</Link> > <Link to="services/deployments">Deployments</Link>

            {deploymentsCount} Active {deploymentsLabel}

        </Page.Header>

        {this.renderRollbackModal()}
      </Page>
    );
  }

  renderRollbackModal() {
    let {
      awaitingRevertDeploymentResponse,
      deploymentToRollback,
      deploymentRollbackError
    } = this.state;

    let heading = (
      <ModalHeading>
        You're About To Rollback The Deployment
      </ModalHeading>
    );

    if (deploymentToRollback != null) {
      return (
        <Confirm
          closeByBackdropClick={true}
          disabled={!!awaitingRevertDeploymentResponse}
          header={heading}
          onClose={this.handleRollbackCancel}
          leftButtonCallback={this.handleRollbackCancel}
          leftButtonText="Cancel"
          rightButtonClassName="button button-danger"
          rightButtonCallback={this.handleRollbackConfirm}
          rightButtonText="Continue Rollback"
          showHeader={true}>

{this.getRollbackModalText(deploymentToRollback)}

            {this.renderRollbackError(deploymentRollbackError)}

        </Confirm>
      );
    }
  }

  getRollbackModalText(deploymentToRollback) {
    let serviceNames = deploymentToRollback.getAffectedServices()
      .map(function (service) {
        return StringUtil.capitalize(service.getName());
      });
    let listOfServiceNames = StringUtil.humanizeArray(serviceNames);
    let serviceCount = serviceNames.length;

    let service = StringUtil.pluralize('service', serviceCount);
    let its = (serviceCount === 1) ? 'its' : 'their';
    let version = StringUtil.pluralize('version', serviceCount);

    if (deploymentToRollback.isStarting()) {
      return `This will stop the current deployment of ${listOfServiceNames}
              and start a new deployment to remove the affected ${service}.`;
    }

    return `This will stop the current deployment of ${listOfServiceNames} and
            start a new deployment to revert the affected ${service} to ${its}
            previous ${version}.`;
  }

  renderRollbackError(deploymentRollbackError) {
    if (deploymentRollbackError != null) {
      return (

          {deploymentRollbackError}

      );
    }
  }

  render() {
    let deployments = DCOSStore.deploymentsList.getItems();
    let loading = !DCOSStore.dataProcessed;

    if (loading) {
      return this.renderLoading();
    }
    if (deployments.length === 0) {
      return this.renderEmpty();
    } else {
      return this.renderPopulated(deployments);
    }
  }

}

DeploymentsTab.routeConfig = {
  label: 'Deployments',
  matches: /^\/services\/deployments/
};

module.exports = DeploymentsTab;
<commit_msg>Show active deployment tab title<commit_after>import classNames from 'classnames';
import {DCOSStore, ResourceTableUtil} from 'foundation-ui';
import {Confirm, Table} from 'reactjs-components';
import {Link} from 'react-router';
import mixin from 'reactjs-mixin';
/* eslint-disable no-unused-vars */
import React from 'react';
/* eslint-enable no-unused-vars */
import {StoreMixin} from 'mesosphere-shared-reactjs';

import AlertPanel from '../../../../../../src/js/components/AlertPanel';
import CollapsingString from '../../../../../../src/js/components/CollapsingString';
import defaultServiceImage from '../../../img/icon-service-default-small@2x.png';
import Loader from '../../../../../../src/js/components/Loader';
import MarathonActions from '../../events/MarathonActions';
import ModalHeading from '../../../../../../src/js/components/modals/ModalHeading';
import NestedServiceLinks from '../../../../../../src/js/components/NestedServiceLinks';
import Page from '../../../../../../src/js/components/Page';
import StatusBar from '../../../../../../src/js/components/StatusBar';
import StringUtil from '../../../../../../src/js/utils/StringUtil';
import TimeAgo from '../../../../../../src/js/components/TimeAgo';

const columnHeading = ResourceTableUtil.renderHeading({
  id: 'AFFECTED SERVICES',
  startTime: 'STARTED',
  location: 'LOCATION',
  status: 'STATUS',
  action: ''
});
const COLLAPSING_COLUMNS = ['location', 'startTime', 'action'];
const METHODS_TO_BIND = [
  'renderAffectedServices',
  'renderAffectedServicesList',
  'renderLocation',
  'renderStartTime',
  'renderStatus',
  'renderAction',
  'handleRollbackClick',
  'handleRollbackCancel',
  'handleRollbackConfirm',
  'onMarathonStoreDeploymentRollbackSuccess',
  'onMarathonStoreDeploymentRollbackError'
];

// collapsing columns are tightly coupled to the left-align caret property;
// this wrapper allows ordinary columns to collapse.
function columnClassNameGetter(prop, sortBy, row) {
  let classSet = ResourceTableUtil.getClassName(prop, sortBy, row);
  if (COLLAPSING_COLUMNS.includes(prop)) {
    return classNames(classSet, 'hidden-small-down');
  }
  return classSet;
}

const DeploymentsBreadcrumbs = () => {
  const crumbs = [
    <Link to="services" key={-1}>Services</Link>,
    <Link to="services/deployments" key={0}>Deployments</Link>
  ];

  return <Page.Header.Breadcrumbs iconID="services" breadcrumbs={crumbs} />;
};

class DeploymentsTab extends mixin(StoreMixin) {

  constructor() {
    super(...arguments);

    this.state = {};
    this.store_listeners = [
      {name: 'dcos', events: ['change'], suppressUpdate: false},
      {
        name: 'marathon',
        events: ['deploymentRollbackSuccess', 'deploymentRollbackError'],
        suppressUpdate: true
      }
    ];

    METHODS_TO_BIND.forEach((method) => {
      this[method] = this[method].bind(this);
    }, this);
  }

  onMarathonStoreDeploymentRollbackSuccess(data) {
    let {deploymentToRollback} = this.state;
    if (deploymentToRollback != null &&
        deploymentToRollback.getId() === data.originalDeploymentID) {
      this.setState({
        awaitingRevertDeploymentResponse: false,
        deploymentToRollback: null,
        deploymentRollbackError: null
      });
    }
  }

  onMarathonStoreDeploymentRollbackError(data) {
    let {deploymentToRollback} = this.state;
    if (deploymentToRollback != null &&
        deploymentToRollback.getId() === data.originalDeploymentID) {
      this.setState({
        awaitingRevertDeploymentResponse: false,
        deploymentRollbackError: data.error
      });
    }
  }

  renderAffectedServices(prop, deployment) {
    return (

          <CollapsingString string={deployment.getId()} />

        {this.renderAffectedServicesList(deployment.getAffectedServices())}
        {this.renderStaleServicesList(deployment.getStaleServiceIds())}

    );
  }

  renderStaleServicesList(serviceIds) {
    if (serviceIds == null) {
      return;
    }
    return serviceIds.map(function (serviceId) {
      return (
        <dd key={`stale_${serviceId}`}>

          {serviceId}

      );
    });
  }

  renderAffectedServicesList(services) {
    return services.map(function (service) {
      const id = encodeURIComponent(service.getId());
      const image = service.getImages()['icon-small'];

      return (
        <dd key={`service_${id}`}>
          <Link to={`/services/overview/${id}`} className="deployment-service-name table-cell-link-primary">

            {service.getName()}
          </Link>

      );
    });
  }

  renderLocation(prop, deployment) {
    const services = deployment.getAffectedServices();
    const items = services.map(function (service, index) {
      return (

          <NestedServiceLinks
            serviceID={service.getId()}
            className="deployment-breadcrumb"
            majorLinkClassName="deployment-breadcrumb-service-id table-cell-link-secondary"
            minorLinkWrapperClassName="deployment-breadcrumb-crumb table-cell-link-secondary" />

      );
    });

    return (

        {items}

    );
  }

  renderStartTime(prop, deployment) {
    return (
      <TimeAgo
        className="deployment-start-time"
        time={deployment.getStartTime()} />
    );
  }

  renderStatus(prop, deployment) {
    const currentStep = deployment.getCurrentStep();
    const totalSteps = deployment.getTotalSteps();
    const title = `Step ${currentStep} of ${totalSteps}`;
    const statusBar = this.renderStatusBar(currentStep, totalSteps);
    const services = deployment.getAffectedServices();

    let currentActions = {};
    if (deployment.currentActions && deployment.currentActions.length > 0) {
      currentActions =
        deployment.currentActions.reduce(function (memo, action) {
          memo[action.app] = action.action;

          return memo;
        }, {});
    }

    const items = services.map(function (service, index) {
      let statusText = service.getStatus();
      if (currentActions[service.id] != null) {
        statusText += ` (${currentActions[service.id]})`;
      }

      return (

          {statusText}

      );
    });

    return (

        {statusBar}
        {title}

{items}

    );
  }

  renderStatusBar(currentStep, totalSteps) {
    let data = [
      {className: 'color-4', value: currentStep - 1},
      {className: 'staged', value: 1},
      {className: '', value: totalSteps - currentStep}
    ];

    return (
      <StatusBar className="status-bar-large" data={data} />
    );
  }

  renderAction(prop, deployment) {
    if (deployment != null) {
      let linkText = 'Rollback';
      if (deployment.isStarting()) {
        linkText = linkText + ' & Remove';
      }
      return (

          {linkText}

      );
    }
  }

  handleRollbackClick(deployment) {
    this.setState({deploymentToRollback: deployment});
  }

  handleRollbackCancel() {
    this.setState({
      awaitingRevertDeploymentResponse: false,
      deploymentToRollback: null,
      deploymentRollbackError: null
    });
  }

  handleRollbackConfirm() {
    let {deploymentToRollback} = this.state;
    if (deploymentToRollback != null) {
      this.setState({awaitingRevertDeploymentResponse: true});
      MarathonActions.revertDeployment(deploymentToRollback.getId());
    }
  }

  getColumns() {
    return [
      {
        className: columnClassNameGetter,
        heading: columnHeading,
        prop: 'id',
        render: this.renderAffectedServices
      },
      {
        className: columnClassNameGetter,
        heading: columnHeading,
        prop: 'location',
        render: this.renderLocation
      },
      {
        className: columnClassNameGetter,
        heading: columnHeading,
        prop: 'startTime',
        render: this.renderStartTime
      },
      {
        className: columnClassNameGetter,
        heading: columnHeading,
        prop: 'status',
        render: this.renderStatus
      },
      {
        className: columnClassNameGetter,
        heading: columnHeading,
        prop: 'action',
        render: this.renderAction
      }
    ];
  }

  getColGroup() {
    return (
      <colgroup>
        <col style={{width: '300px'}} />
        <col className="hidden-small-down" />
        <col className="hidden-small-down" style={{width: '120px'}} />
        <col style={{width: '240px'}} />
        <col className="hidden-small-down" style={{width: '160px'}} />
      </colgroup>
    );
  }

  renderEmpty() {
    return (
      <Page>
        <Page.Header breadcrumbs={<DeploymentsBreadcrumbs/>} />
        <AlertPanel
          title="No active deployments">

Active deployments will be shown here.

        </AlertPanel>
      </Page>
    );
  }

  renderLoading() {
    return <Loader />;
  }

  renderPopulated(deploymentsItems) {
    let deploymentsCount = deploymentsItems.length;
    let deploymentsLabel = StringUtil.pluralize('Deployment', deploymentsCount);

    return (
      <Page>
        <Page.Header breadcrumbs={<DeploymentsBreadcrumbs />} />

          {deploymentsCount} Active {deploymentsLabel}

        {this.renderRollbackModal()}
      </Page>
    );
  }

  renderRollbackModal() {
    let {
      awaitingRevertDeploymentResponse,
      deploymentToRollback,
      deploymentRollbackError
    } = this.state;

    let heading = (
      <ModalHeading>
        You're About To Rollback The Deployment
      </ModalHeading>
    );

    if (deploymentToRollback != null) {
      return (
        <Confirm
          closeByBackdropClick={true}
          disabled={!!awaitingRevertDeploymentResponse}
          header={heading}
          onClose={this.handleRollbackCancel}
          leftButtonCallback={this.handleRollbackCancel}
          leftButtonText="Cancel"
          rightButtonClassName="button button-danger"
          rightButtonCallback={this.handleRollbackConfirm}
          rightButtonText="Continue Rollback"
          showHeader={true}>

{this.getRollbackModalText(deploymentToRollback)}

            {this.renderRollbackError(deploymentRollbackError)}

        </Confirm>
      );
    }
  }

  getRollbackModalText(deploymentToRollback) {
    let serviceNames = deploymentToRollback.getAffectedServices()
      .map(function (service) {
        return StringUtil.capitalize(service.getName());
      });
    let listOfServiceNames = StringUtil.humanizeArray(serviceNames);
    let serviceCount = serviceNames.length;

    let service = StringUtil.pluralize('service', serviceCount);
    let its = (serviceCount === 1) ? 'its' : 'their';
    let version = StringUtil.pluralize('version', serviceCount);

    if (deploymentToRollback.isStarting()) {
      return `This will stop the current deployment of ${listOfServiceNames}
              and start a new deployment to remove the affected ${service}.`;
    }

    return `This will stop the current deployment of ${listOfServiceNames} and
            start a new deployment to revert the affected ${service} to ${its}
            previous ${version}.`;
  }

  renderRollbackError(deploymentRollbackError) {
    if (deploymentRollbackError != null) {
      return (

          {deploymentRollbackError}

      );
    }
  }

  render() {
    let deployments = DCOSStore.deploymentsList.getItems();
    let loading = !DCOSStore.dataProcessed;

    if (loading) {
      return this.renderLoading();
    }
    if (deployments.length === 0) {
      return this.renderEmpty();
    } else {
      return this.renderPopulated(deployments);
    }
  }

}

DeploymentsTab.routeConfig = {
  label: 'Deployments',
  matches: /^\/services\/deployments/
};

module.exports = DeploymentsTab;
<|endoftext|>


<commit_before>goog.provide('og.control.ZoomControl');

goog.require('og.inheritance');
goog.require('og.control.Control');
goog.require('og.control.MouseNavigation');

og.control.ZoomControl = function (options) {
    og.inheritance.base(this, options);

    this.distDiff = 0.33;
    this.stepsCount = 5;
    this.stepsForward = null;
    this.stepIndex = 0;

    this.planet = null;
};

og.inheritance.extend(og.control.ZoomControl, og.control.Control);

og.control.ZoomControl.prototype.init = function () {
    var zoomDiv = document.createElement('div'),
        btnZoomIn = document.createElement('button'),
        btnZoomOut = document.createElement('button');

    zoomDiv.className = 'ogZoomControl';
    btnZoomIn.className = 'ogZoomButton ogZoomIn';
    btnZoomOut.className = 'ogZoomButton ogZoomOut';

    zoomDiv.appendChild(btnZoomIn);
    zoomDiv.appendChild(btnZoomOut);

    this.renderer.div.appendChild(zoomDiv);

    var that = this;
    btnZoomIn.onclick = function (e) {
        that.zoomIn();
    };

    btnZoomOut.onclick = function (e) {
        that.zoomOut();
    };

    this.planet = this.renderer.renderNodes.Earth;
    this.renderer.events.on("draw", this, this.onDraw);
};

og.control.ZoomControl.prototype.zoomIn = function () {
    this.stepIndex = this.stepsCount;
    this.stepsForward = og.control.MouseNavigation.getMovePointsFromPixelTerrain(this.renderer.activeCamera,
        this.planet, this.stepsCount, this.distDiff * 1.7, this.renderer.getCenter(), true, this.renderer.activeCamera._n.getNegate());
};

og.control.ZoomControl.prototype.zoomOut = function () {
    this.stepIndex = this.stepsCount;
    this.stepsForward = og.control.MouseNavigation.getMovePointsFromPixelTerrain(this.renderer.activeCamera,
        this.planet, this.stepsCount, this.distDiff * 2, this.renderer.getCenter(), false, this.renderer.activeCamera._n.getNegate());
};

og.control.ZoomControl.prototype.onDraw = function (e) {

    if (this.stepIndex) {
        var sf = this.stepsForward[this.stepsCount - this.stepIndex--];
        var cam = this.renderer.activeCamera;
        cam.eye = sf.eye;
        cam._v = sf.v;
        cam._u = sf.u;
        cam._n = sf.n;
        cam.update();
    }
};<commit_msg>Little zoom animation control optimization.<commit_after>goog.provide('og.control.ZoomControl');

goog.require('og.inheritance');
goog.require('og.control.Control');
goog.require('og.control.MouseNavigation');

og.control.ZoomControl = function (options) {
    og.inheritance.base(this, options);

    this.distDiff = 0.33;
    this.stepsCount = 5;
    this.stepsForward = null;
    this.stepIndex = 0;

    this.planet = null;
};

og.inheritance.extend(og.control.ZoomControl, og.control.Control);

og.control.ZoomControl.prototype.init = function () {
    var zoomDiv = document.createElement('div'),
        btnZoomIn = document.createElement('button'),
        btnZoomOut = document.createElement('button');

    zoomDiv.className = 'ogZoomControl';
    btnZoomIn.className = 'ogZoomButton ogZoomIn';
    btnZoomOut.className = 'ogZoomButton ogZoomOut';

    zoomDiv.appendChild(btnZoomIn);
    zoomDiv.appendChild(btnZoomOut);

    this.renderer.div.appendChild(zoomDiv);

    var that = this;
    btnZoomIn.onclick = function (e) {
        that.zoomIn();
    };

    btnZoomOut.onclick = function (e) {
        that.zoomOut();
    };

    this.planet = this.renderer.renderNodes.Earth;
    this.renderer.events.on("draw", this, this.onDraw);
};

og.control.ZoomControl.prototype.zoomIn = function () {
    this.planet.normalMapCreator.active = false;
    this.planet.terrainProvider.active = false;
    this.stepIndex = this.stepsCount;
    this.stepsForward = og.control.MouseNavigation.getMovePointsFromPixelTerrain(this.renderer.activeCamera,
        this.planet, this.stepsCount, this.distDiff * 1.7, this.renderer.getCenter(), true, this.renderer.activeCamera._n.getNegate());
};

og.control.ZoomControl.prototype.zoomOut = function () {
    this.planet.normalMapCreator.active = false;
    this.planet.terrainProvider.active = false;
    this.stepIndex = this.stepsCount;
    this.stepsForward = og.control.MouseNavigation.getMovePointsFromPixelTerrain(this.renderer.activeCamera,
        this.planet, this.stepsCount, this.distDiff * 2, this.renderer.getCenter(), false, this.renderer.activeCamera._n.getNegate());
};

og.control.ZoomControl.prototype.onDraw = function (e) {

    if (this.stepIndex) {
        var sf = this.stepsForward[this.stepsCount - this.stepIndex--];
        var cam = this.renderer.activeCamera;
        cam.eye = sf.eye;
        cam._v = sf.v;
        cam._u = sf.u;
        cam._n = sf.n;
        cam.update();
    } else {
        this.planet.normalMapCreator.active = true;
        this.planet.terrainProvider.active = true;
    }
};<|endoftext|>


<commit_before>var util = require('util'),
    exec = require('child_process').exec,
    child;

child = exec('./test/test.sh',
  function (error, stdout, stderr) {
    console.log('stdout: ' + stdout);
    console.log('stderr: ' + stderr);
    if (error !== null) {
      console.log('exec error: ' + error);
    }
});<commit_msg>ls && ls test && ./test/test.sh<commit_after>var util = require('util'),
    exec = require('child_process').exec,
    child;

child = exec('ls && ls test && ./test/test.sh',
  function (error, stdout, stderr) {
    console.log('stdout: ' + stdout);
    console.log('stderr: ' + stderr);
    if (error !== null) {
      console.log('exec error: ' + error);
    }
});<|endoftext|>