<commit_before>// Entry point for the library. Implements the bot object,
// exports methods as necessary.
var util = require("util");
var https = require("https");
var stream = require("stream");
var EventEmitter = require("events");
var FormData = require("form-data");
var Readable = stream.Readable;

function Bot(authToken, options) {
  if (!(this instanceof Bot)) return new Bot(authToken, options);
  EventEmitter.call(this);

  if (options === undefined) options = {};
  if (!options.hasOwnProperty("timeout")) options.timeout = 10 * 60;
  if (!options.hasOwnProperty("strict")) options.strict = true; //TODO: use, set to false
  if (!options.hasOwnProperty("actionInterval")) options.actionInterval = 3000;
  if (!options.hasOwnProperty("autodetect")) options.autodetect = true;
  if (!options.hasOwnProperty("retryInterval")) options.retryInterval = 2000;

  if (!(authToken instanceof String))
    throw new Error("Invalid auth token specified");

  this.options = options;
  this.authToken = authToken;
  this.data = {};

  // TODO: panic on error emitted
  this.on("ready", startLoop);
  firstDetection.call(this);
}

util.inherit(Bot, EventEmitter);

Bot.prototype.get = function get(name) {
  return this.data[name];
};

Bot.prototype.set = function set(name, value) {
  this.data[name] = value;
};

// FIXME: wrap errors of form, req and res in NetworkError
Bot.prototype.callMethod = function (method, parameters, cb) {
  if (cb === undefined) cb = parameters, parameters = {};

  // Prepare the body
  var form = new FormData();
  form.on("error", cb);
  Object.keys(parameters).forEach(function (key) {
    var value = parameters[key];
    if (typeof value === "null" || typeof value === "undefined")
      return;
    if (typeof value === "number" || typeof value === "boolean")
      value = value.toString();
    if (!(value instanceof String || value instanceof Buffer || value instanceof Readable))
      value = JSON.stringify(value);
    form.append(value);
  });

  // Make the request
  var req = https.request({
    agent: this.options.agent,
    method: "POST",
    hostname: "api.telegram.org",
    path: "bot" + this.authToken + "/" + method,
    headers: { accept: "application/json" }
  }, handleResponse);
  req.on("error", cb);
  form.pipe(req);

  function handleResponse(res) {
    if (res.headers["content-type"] !== "application/json") {
      res.resume();
      return cb(new NetworkError("API response not in JSON"));
    }

    // FIXME: not the most efficient way to collect output
    var body = Buffer();
    res.on("error", cb);
    res.on("readable", function () { body = Buffer.concat(body, res.read()); });
    res.on("end", function () { handleBody(body, res.statusCode); });
  }

  function handleBody(body, status) {
    try {
      body = JSON.parse(body);
      if (typeof body.ok !== "boolean")
        return cb(new NetworkError("ok field not a boolean in API response"));
      if (body.ok !== (status === 200))
        return cb(new NetworkError("ok field not matching JSON response"));
      if (body.ok) return cb(null, body.result);
      cb(new TelegramError(body));
    } catch (err) { cb(err); }
  }
};

bot.prototype.autodetect = function autodetect(cb) {
  if (!cb) cb = function () {};
  this.callMethod("getMe", function (err, result) {
    if (err) return cb(err);

    this.set("id", result.id);
    this.set("username", result.username);
    this.set("firstname", result.first_name);
    this.set("lastname", result.last_name);
    cb();
  });
};

function firstDetection() {
  if (this.stopped) return;

  if (this.options.autodetect)
    this.autodetect(function (err) {
      if (err) {
        this.emit("error", err);
        return setTimeout(firstDetection.bind(this), this.options.retryInterval);
      }
      this.emit("ready");
    });
  else
    process.nextTick(function () {
      if (typeof this.get("id") !== "number") {
        console.error("Bot was not initialized!");
        process.exit(1);
      }
      this.emit("ready");
    });
}

function startLoop() {
  if (this.stopped) return;

  this.callMethod("getUpdates", {
    offset: this.offset,
    timeout: this.options.timeout
  }, function (err, result) {
    if (err) {
      this.emit("error", err);
      return setTimeout(startLoop.bind(this), this.options.retryInterval);
    }

    result.forEach(this.processUpdate.bind(this));
    this.accumulatedDone = true;
    if (result.length > 0)
      this.offset = result[result.length-1].update_id + 1;
    startLoop();
  });
}

bot.prototype.ready = function ready(cb) {
  if (typeof this.get("id") === "number")
    return cb();
  this.on("ready", cb);
};

bot.prototype.stop = function stop() {
  if (this.stopped) return;
  this.stopped = true;
  this.emit("stopped");
};

Bot.NetworkError = NetworkError;
Bot.TelegramError = TelegramError;
module.exports = Bot;
<commit_msg>Improvements in callMethod(), let form-data submit for us<commit_after>// Entry point for the library. Implements the bot object,
// get / set, lifecycle management, API calling and the
// `getUpdates` loop.
var util = require("util");
var stream = require("stream");
var EventEmitter = require("events");
var FormData = require("form-data");
var Readable = stream.Readable;

function Bot(authToken, options) {
  if (!(this instanceof Bot)) return new Bot(authToken, options);
  EventEmitter.call(this);

  if (options === undefined) options = {};
  if (!options.hasOwnProperty("timeout")) options.timeout = 10 * 60;
  if (!options.hasOwnProperty("strict")) options.strict = true; //TODO: use, set to false
  if (!options.hasOwnProperty("actionInterval")) options.actionInterval = 3000;
  if (!options.hasOwnProperty("autodetect")) options.autodetect = true;
  if (!options.hasOwnProperty("retryInterval")) options.retryInterval = 2000;

  if (!(authToken instanceof String))
    throw new Error("Invalid auth token specified");

  this.options = options;
  this.authToken = authToken;
  this.data = {};

  // TODO: panic on error emitted
  this.on("ready", startLoop);
  firstDetection.call(this);
}

util.inherit(Bot, EventEmitter);

Bot.prototype.get = function get(name) {
  return this.data[name];
};

Bot.prototype.set = function set(name, value) {
  this.data[name] = value;
};

// FIXME: wrap errors of form, req and res in NetworkError
Bot.prototype.callMethod = function (method, parameters, cb) {
  if (cb === undefined) cb = parameters, parameters = {};

  // Prepare the body
  var form = new FormData();
  form.on("error", handleResponse);
  Object.keys(parameters).forEach(function (key) {
    var value = parameters[key];
    if (value === null || value === undefined)
      return;
    if (typeof value === "number" || typeof value === "boolean")
      return form.append(key, value.toString());
    if (value instanceof String || value instanceof Buffer || value instanceof Readable)
      return form.append(key, value, value.options);
    form.append(key, JSON.stringify(value));
  });

  // Make the request
  form.submit({
    agent: this.options.agent,
    protocol: "https:",
    host: "api.telegram.org",
    path: "bot" + this.authToken + "/" + method,
    headers: { accept: "application/json" }
  }, handleResponse);

  // Parse the response
  function handleResponse(err, res) {
    if (err) return cb(new NetworkError(err));

    if (res.headers["content-type"] !== "application/json") {
      res.resume();
      return cb(new NetworkError("API response not in JSON"));
    }

    // FIXME: not the most efficient way to collect output
    var body = Buffer();
    res.on("error", handleResponse);
    res.on("readable", function () { body = Buffer.concat(body, res.read()); });
    res.on("end", function () { handleBody(body, res.statusCode); });
  }

  function handleBody(body, status) {
    try {
      body = JSON.parse(body);
      if (typeof body.ok !== "boolean")
        return cb(new NetworkError("ok field not a boolean in API response"));
      if (body.ok !== (status === 200))
        return cb(new NetworkError("ok field not matching JSON response"));
      if (body.ok) return cb(null, body.result);
      cb(new TelegramError(body));
    } catch (err) { cb(err); }
  }
};

bot.prototype.autodetect = function autodetect(cb) {
  if (!cb) cb = function () {};
  this.callMethod("getMe", function (err, result) {
    if (err) return cb(err);

    this.set("id", result.id);
    this.set("username", result.username);
    this.set("firstname", result.first_name);
    this.set("lastname", result.last_name);
    cb();
  });
};

function firstDetection() {
  if (this.stopped) return;

  if (this.options.autodetect)
    this.autodetect(function (err) {
      if (err) {
        this.emit("error", err);
        return setTimeout(firstDetection.bind(this), this.options.retryInterval);
      }
      this.emit("ready");
    });
  else
    process.nextTick(function () {
      if (typeof this.get("id") !== "number") {
        console.error("Bot was not initialized!");
        process.exit(1);
      }
      this.emit("ready");
    });
}

function startLoop() {
  if (this.stopped) return;

  this.callMethod("getUpdates", {
    offset: this.offset,
    timeout: this.options.timeout
  }, function (err, result) {
    if (err) {
      this.emit("error", err);
      return setTimeout(startLoop.bind(this), this.options.retryInterval);
    }

    result.forEach(this.processUpdate.bind(this));
    this.accumulatedDone = true;
    if (result.length > 0)
      this.offset = result[result.length-1].update_id + 1;
    startLoop();
  });
}

bot.prototype.ready = function ready(cb) {
  if (typeof this.get("id") === "number")
    return cb();
  this.on("ready", cb);
};

bot.prototype.stop = function stop() {
  if (this.stopped) return;
  this.stopped = true;
  this.emit("stopped");
};

Bot.NetworkError = NetworkError;
Bot.TelegramError = TelegramError;
module.exports = Bot;
<|endoftext|>


<commit_before>var EventEmitter = require('events').EventEmitter
var util = require('util')
var split = require('split')
var parser = require('./parser.js')
var createMap = require('./map.js')
var players = require('./players.js')

module.exports = createBot

function createBot (input, output, logger) {
  return new Bot(input, output, logger)
}

util.inherits(Bot, EventEmitter)
function Bot (input, output, logger) {
  EventEmitter.call(this)
  this.duration = 0
  this.round = 0
  this.input = input
  this.output = output
  this.logger = logger
  this.settings = {}
  this.gameMap = createMap()
  this.placements = []
  this.transfers = []
  this.transferTargets = []
  this.attacks = []

  input.pipe(split()).pipe(parser(this))

  this.on('settings', this.handleSettings.bind(this))
  this.on('setup_map', this.handleSetupMap.bind(this))
  this.on('pick_starting_region', this.handleStartingRegions.bind(this))
  this.on('go', this.handlePlaceAttack.bind(this))

  this.on('update_map', this.gameMap.handleUpdate.bind(this.gameMap))

  this.on('opponent_moves', this.handleOpponentMoves.bind(this))
  this.on('#', this.handleComment.bind(this))
  this.on('Round', this.handleComment.bind(this, 'Round'))
  this.on('Output', this.handleComment.bind(this, 'Output'))
}

Bot.prototype.handlePlaceAttack = function handlePlaceAttack (data) {
  var mode = data.shift()

  var toProcess = this.gameMap.onEnemyBorder()

  toProcess.sort(function sortByEnemyCount(a, b) {
    return b.getNeighboringEnemies().length - a.getNeighboringEnemies().length
  })

  var lastToProcess = this.gameMap.notOnEnemyBorder()

  lastToProcess.sort(function sortByNeutralValue(a, b) {
    return b.averageNeutralSRBonus() - a.averageNeutralSRBonus()
  })

  toProcess = toProcess.concat(lastToProcess)

  var important = toProcess[0]
  var currentRegion
  var neighboringAllies
  var neighboringEnemies
  var neighboringNeutrals
  var sourceRegion
  var targetRegion
  var maxAttack
  var minAttack
  var needed
  var i
  while (toProcess.length > 0) {
    currentRegion = toProcess.shift()
    neighboringAllies = currentRegion.getNeighboringAllies()
    for (i = 0; i < neighboringAllies.length; ++i) {
      sourceRegion = neighboringAllies[i]
      if (!sourceRegion.bordersEnemy() &&
          sourceRegion.troopCount > 1 &&
          this.transferTargets.indexOf(sourceRegion.id) === -1) {
        this.transfers.push([
          sourceRegion.id,
          currentRegion.id,
          sourceRegion.troopCount - 1
        ])
        this.transferTargets.push(currentRegion.id)
        sourceRegion.troopCount = 1
      }
    }
    neighboringEnemies = currentRegion.getNeighboringEnemies()
    neighboringEnemies.sort(sortByPower)
    for (i = 0; i < neighboringEnemies.length; ++i) {
      targetRegion = neighboringEnemies[i]
      maxAttack = currentRegion.troopCount - 1
      minAttack = (targetRegion.troopCount * 2) - 1
      if (maxAttack >= minAttack) {
        if (neighboringEnemies.length === 1) {
          minAttack = maxAttack
          currentRegion.troopCount = 1
        }
        this.attacks.push([
          currentRegion.id,
          targetRegion.id,
          minAttack
        ])
        currentRegion.troopCount -= minAttack
        continue
      }
      needed = minAttack - maxAttack
      if (this.settings.armies >= needed) {
        this.placements.push([currentRegion.id, needed])
        this.settings.armies -= needed
        this.attacks.push([
          currentRegion.id,
          targetRegion.id,
          minAttack
        ])
        currentRegion.troopCount -= minAttack
      }
    }
    // Don't go into neutral stuff if we border an enemy
    if (currentRegion.bordersEnemy()) {
      continue
    }

    neighboringNeutrals = currentRegion.getNeighboringNeutrals()
    var neutralValues = neighboringNeutrals.map(
      calcNeutralValue.bind(null, currentRegion.superRegion)
    )
    neutralValues.sort(sortByValueProp)
    for (i = 0; i < neutralValues.length; ++i) {
      targetRegion = neutralValues[i].region
      maxAttack = currentRegion.troopCount - 1
      minAttack = (targetRegion.troopCount * 2) - 1
      if (minAttack < 2) {
        minAttack = 2
      }
      if (maxAttack >= minAttack) {
        if (neighboringNeutrals.length === 1) {
          minAttack = maxAttack
          currentRegion.troopCount = 1
        }
        this.log(
          "attacking %s with %s from %s with score %s",
          targetRegion.id,
          minAttack,
          currentRegion.id,
          neutralValues[i].value
        )
        this.attacks.push([
          currentRegion.id,
          targetRegion.id,
          minAttack
        ])
        currentRegion.troopCount -= minAttack
        continue
      }
      needed = minAttack - maxAttack
      if (this.settings.armies >= needed) {
        this.placements.push([currentRegion.id, needed])
        this.settings.armies -= needed
        this.log(
          "attacking %s with %s from %s with score %s",
          targetRegion.id,
          minAttack,
          currentRegion.id,
          neutralValues[i].value
        )
        this.attacks.push([
          currentRegion.id,
          targetRegion.id,
          minAttack
        ])
        currentRegion.troopCount -= minAttack
        continue
      }
    }
  }

  if (this.settings.armies > 0) {
    this.placements.push([important.id, this.settings.armies])
    important.troopCount += this.settings.armies
    this.settings.armies = 0
  }

  var response = ''
  switch (mode) {
  case 'place_armies':
    for (i = 0; i < this.placements.length; ++i) {
      if (response.length !== 0) {
        response += ', '
      }
      response += players.us + ' place_armies ' + this.placements[i].join(' ')
    }
    if (response) {
      this.output.write(response + '\n')
    }
    this.placements = []
    break
  case 'attack/transfer':
    for (i = 0; i < this.transfers.length; ++i) {
      if (response.length !== 0) {
        response += ', '
      }
      response += players.us + ' attack/transfer ' + this.transfers[i].join(' ')
    }
    for (i = 0; i < this.attacks.length; ++i) {
      if (response.length !== 0) {
        response += ', '
      }
      response += players.us + ' attack/transfer ' + this.attacks[i].join(' ')
    }
    if (response) {
      this.output.write(response + '\n')
    }
    this.transfers = []
    this.transferTargets = []
    this.attacks = []
    break
  default:
    this.logger.write('unhandled mode in go: ' + mode + ': ' + data + '\n')
    break
  }
}

function sortByPower (a, b) {
  var aValue = ((a.troopCount * 100) +
                (a.superRegion.bonus * 10) +
                a.neighbors.length)
  var bValue = ((b.troopCount * 100) +
                (b.superRegion.bonus * 10) +
                b.neighbors.length)
  return bValue - aValue
}

function calcNeutralValue (superRegion, region) {
  var value = (
    100 -
    (region.troopCount * 10) +
    (region.superRegion.bonus * 10) +
    region.neighbors.length +
    (superRegion === region.superRegion ? 30 : 0)
  )
  return {region: region, value: value}
}

function sortByValueProp (a, b) {
  return b.value - a.value
}
Bot.prototype.handleSettings = function handleSettings (data) {
  var type = data.shift()

  switch (type) {
  case 'starting_armies':
    this.settings.armies = parseInt(data[0], 10)
    break
  case 'your_bot':
    players.us = data[0]
    break
  case 'opponent_bot':
    players.them = data[0]
    break
  case 'max_rounds':
  case 'time_per_move':
  case 'timebank':
  case 'starting_regions':
  case 'starting_pick_amount':
    // Do not handle these yet, maybe in the future
    break
  default:
    this.logger.write('Unhandled settings: ' + type + ': ' + data + '\n')
    break
  }
}

Bot.prototype.handleSetupMap = function handleSetupMap(data) {
  var type = data.shift()
  var i = 0

  switch (type) {
  case 'super_regions':
    for (i = 0; i < data.length; i += 2) {
      this.gameMap.addSuperRegion(parseInt(data[i], 10), parseInt(data[i + 1], 10))
    }
    break
  case 'regions':
    for (i = 0; i < data.length; i += 2) {
      this.gameMap.addRegion(parseInt(data[i], 10), parseInt(data[i + 1], 10))
    }
    break
  case 'neighbors':
    for (i = 0; i < data.length; i += 2) {
      var neighbors = data[i + 1].split(',')
      for (var j = 0; j < neighbors.length; ++j) {
        neighbors[j] = parseInt(neighbors[j], 10)
      }
      this.gameMap.addNeighbors(parseInt(data[i], 10), neighbors)
    }
    break
  case 'wastelands':
    for (i = 0; i < data.length; ++i) {
      data[i] = parseInt(data[i], 10)
    }
    this.gameMap.addWastelands(data)
    break
  case 'opponent_starting_regions':
    for (i = 0; i < data.length; ++i) {
      this.gameMap.regionsById[parseInt(data[i], 10)].owner = players.them
    }
    break
  default:
    this.logger.write('Unhandled setup_map: ' + type + ': ' + data + '\n')
    break
  }
}

Bot.prototype.handleStartingRegions = function handleStartingRegions (ids) {
  // drop remaining timebank
  ids.shift()
  var best
  for (var i = 0; i < ids.length; i++) {
    var region = this.gameMap.regionsById[parseInt(ids[i], 10)]
    var value = 100
    value -= region.superRegion.wastelandCount * 10
    value -= region.superRegion.regions.length * 5
    value += region.superRegion.bonus * 3
    this.log(
      'SuperRegion: %s, Region: %s, Value: %s',
      region.superRegion.id,
      region.id, value
    )
    if (!best || value > best.value) {
      best = {
        region: region,
        value: value
      }
    }
  }
  this.output.write(best.region.id.toString() + '\n')
}

Bot.prototype.handleOpponentMoves = function handleOpponentMoves () {
  if (this.round !== 0) {
    this.log('Round %s end, took %s µs', this.round, this.duration)
  } else {
    this.log('Init took %s µs', this.duration)
  }
  this.round += 1

  this.duration = 0
}

Bot.prototype.handleComment = function handleComment (type, data) {
  if (data) {
    type += ' '
  } else {
    data = type
    type = ''
  }
  this.logger.write('# ' + type + data.join(' ') + '\n')
}

Bot.prototype.log = function log () {
  this.logger.write(util.format.apply(null, arguments) + '\n')
}
<commit_msg>never attack with 1<commit_after>var EventEmitter = require('events').EventEmitter
var util = require('util')
var split = require('split')
var parser = require('./parser.js')
var createMap = require('./map.js')
var players = require('./players.js')

module.exports = createBot

function createBot (input, output, logger) {
  return new Bot(input, output, logger)
}

util.inherits(Bot, EventEmitter)
function Bot (input, output, logger) {
  EventEmitter.call(this)
  this.duration = 0
  this.round = 0
  this.input = input
  this.output = output
  this.logger = logger
  this.settings = {}
  this.gameMap = createMap()
  this.placements = []
  this.transfers = []
  this.transferTargets = []
  this.attacks = []

  input.pipe(split()).pipe(parser(this))

  this.on('settings', this.handleSettings.bind(this))
  this.on('setup_map', this.handleSetupMap.bind(this))
  this.on('pick_starting_region', this.handleStartingRegions.bind(this))
  this.on('go', this.handlePlaceAttack.bind(this))

  this.on('update_map', this.gameMap.handleUpdate.bind(this.gameMap))

  this.on('opponent_moves', this.handleOpponentMoves.bind(this))
  this.on('#', this.handleComment.bind(this))
  this.on('Round', this.handleComment.bind(this, 'Round'))
  this.on('Output', this.handleComment.bind(this, 'Output'))
}

Bot.prototype.handlePlaceAttack = function handlePlaceAttack (data) {
  var mode = data.shift()

  var toProcess = this.gameMap.onEnemyBorder()

  toProcess.sort(function sortByEnemyCount(a, b) {
    return b.getNeighboringEnemies().length - a.getNeighboringEnemies().length
  })

  var lastToProcess = this.gameMap.notOnEnemyBorder()

  lastToProcess.sort(function sortByNeutralValue(a, b) {
    return b.averageNeutralSRBonus() - a.averageNeutralSRBonus()
  })

  toProcess = toProcess.concat(lastToProcess)

  var important = toProcess[0]
  var currentRegion
  var neighboringAllies
  var neighboringEnemies
  var neighboringNeutrals
  var sourceRegion
  var targetRegion
  var maxAttack
  var minAttack
  var needed
  var i
  while (toProcess.length > 0) {
    currentRegion = toProcess.shift()
    neighboringAllies = currentRegion.getNeighboringAllies()
    for (i = 0; i < neighboringAllies.length; ++i) {
      sourceRegion = neighboringAllies[i]
      if (!sourceRegion.bordersEnemy() &&
          sourceRegion.troopCount > 1 &&
          this.transferTargets.indexOf(sourceRegion.id) === -1) {
        this.transfers.push([
          sourceRegion.id,
          currentRegion.id,
          sourceRegion.troopCount - 1
        ])
        this.transferTargets.push(currentRegion.id)
        sourceRegion.troopCount = 1
      }
    }
    neighboringEnemies = currentRegion.getNeighboringEnemies()
    neighboringEnemies.sort(sortByPower)
    for (i = 0; i < neighboringEnemies.length; ++i) {
      targetRegion = neighboringEnemies[i]
      maxAttack = currentRegion.troopCount - 1
      minAttack = (targetRegion.troopCount * 2) - 1
      if (minAttack < 2) {
        minAttack = 2
      }
      if (maxAttack >= minAttack) {
        if (neighboringEnemies.length === 1) {
          minAttack = maxAttack
          currentRegion.troopCount = 1
        }
        this.attacks.push([
          currentRegion.id,
          targetRegion.id,
          minAttack
        ])
        currentRegion.troopCount -= minAttack
        continue
      }
      needed = minAttack - maxAttack
      if (this.settings.armies >= needed) {
        this.placements.push([currentRegion.id, needed])
        this.settings.armies -= needed
        this.attacks.push([
          currentRegion.id,
          targetRegion.id,
          minAttack
        ])
        currentRegion.troopCount -= minAttack
      }
    }
    // Don't go into neutral stuff if we border an enemy
    if (currentRegion.bordersEnemy()) {
      continue
    }

    neighboringNeutrals = currentRegion.getNeighboringNeutrals()
    var neutralValues = neighboringNeutrals.map(
      calcNeutralValue.bind(null, currentRegion.superRegion)
    )
    neutralValues.sort(sortByValueProp)
    for (i = 0; i < neutralValues.length; ++i) {
      targetRegion = neutralValues[i].region
      maxAttack = currentRegion.troopCount - 1
      minAttack = (targetRegion.troopCount * 2) - 1
      if (minAttack < 2) {
        minAttack = 2
      }
      if (maxAttack >= minAttack) {
        if (neighboringNeutrals.length === 1) {
          minAttack = maxAttack
          currentRegion.troopCount = 1
        }
        this.log(
          "attacking %s with %s from %s with score %s",
          targetRegion.id,
          minAttack,
          currentRegion.id,
          neutralValues[i].value
        )
        this.attacks.push([
          currentRegion.id,
          targetRegion.id,
          minAttack
        ])
        currentRegion.troopCount -= minAttack
        continue
      }
      needed = minAttack - maxAttack
      if (this.settings.armies >= needed) {
        this.placements.push([currentRegion.id, needed])
        this.settings.armies -= needed
        this.log(
          "attacking %s with %s from %s with score %s",
          targetRegion.id,
          minAttack,
          currentRegion.id,
          neutralValues[i].value
        )
        this.attacks.push([
          currentRegion.id,
          targetRegion.id,
          minAttack
        ])
        currentRegion.troopCount -= minAttack
        continue
      }
    }
  }

  if (this.settings.armies > 0) {
    this.placements.push([important.id, this.settings.armies])
    important.troopCount += this.settings.armies
    this.settings.armies = 0
  }

  var response = ''
  switch (mode) {
  case 'place_armies':
    for (i = 0; i < this.placements.length; ++i) {
      if (response.length !== 0) {
        response += ', '
      }
      response += players.us + ' place_armies ' + this.placements[i].join(' ')
    }
    if (response) {
      this.output.write(response + '\n')
    }
    this.placements = []
    break
  case 'attack/transfer':
    for (i = 0; i < this.transfers.length; ++i) {
      if (response.length !== 0) {
        response += ', '
      }
      response += players.us + ' attack/transfer ' + this.transfers[i].join(' ')
    }
    for (i = 0; i < this.attacks.length; ++i) {
      if (response.length !== 0) {
        response += ', '
      }
      response += players.us + ' attack/transfer ' + this.attacks[i].join(' ')
    }
    if (response) {
      this.output.write(response + '\n')
    }
    this.transfers = []
    this.transferTargets = []
    this.attacks = []
    break
  default:
    this.logger.write('unhandled mode in go: ' + mode + ': ' + data + '\n')
    break
  }
}

function sortByPower (a, b) {
  var aValue = ((a.troopCount * 100) +
                (a.superRegion.bonus * 10) +
                a.neighbors.length)
  var bValue = ((b.troopCount * 100) +
                (b.superRegion.bonus * 10) +
                b.neighbors.length)
  return bValue - aValue
}

function calcNeutralValue (superRegion, region) {
  var value = (
    100 -
    (region.troopCount * 10) +
    (region.superRegion.bonus * 10) +
    region.neighbors.length +
    (superRegion === region.superRegion ? 30 : 0)
  )
  return {region: region, value: value}
}

function sortByValueProp (a, b) {
  return b.value - a.value
}
Bot.prototype.handleSettings = function handleSettings (data) {
  var type = data.shift()

  switch (type) {
  case 'starting_armies':
    this.settings.armies = parseInt(data[0], 10)
    break
  case 'your_bot':
    players.us = data[0]
    break
  case 'opponent_bot':
    players.them = data[0]
    break
  case 'max_rounds':
  case 'time_per_move':
  case 'timebank':
  case 'starting_regions':
  case 'starting_pick_amount':
    // Do not handle these yet, maybe in the future
    break
  default:
    this.logger.write('Unhandled settings: ' + type + ': ' + data + '\n')
    break
  }
}

Bot.prototype.handleSetupMap = function handleSetupMap(data) {
  var type = data.shift()
  var i = 0

  switch (type) {
  case 'super_regions':
    for (i = 0; i < data.length; i += 2) {
      this.gameMap.addSuperRegion(parseInt(data[i], 10), parseInt(data[i + 1], 10))
    }
    break
  case 'regions':
    for (i = 0; i < data.length; i += 2) {
      this.gameMap.addRegion(parseInt(data[i], 10), parseInt(data[i + 1], 10))
    }
    break
  case 'neighbors':
    for (i = 0; i < data.length; i += 2) {
      var neighbors = data[i + 1].split(',')
      for (var j = 0; j < neighbors.length; ++j) {
        neighbors[j] = parseInt(neighbors[j], 10)
      }
      this.gameMap.addNeighbors(parseInt(data[i], 10), neighbors)
    }
    break
  case 'wastelands':
    for (i = 0; i < data.length; ++i) {
      data[i] = parseInt(data[i], 10)
    }
    this.gameMap.addWastelands(data)
    break
  case 'opponent_starting_regions':
    for (i = 0; i < data.length; ++i) {
      this.gameMap.regionsById[parseInt(data[i], 10)].owner = players.them
    }
    break
  default:
    this.logger.write('Unhandled setup_map: ' + type + ': ' + data + '\n')
    break
  }
}

Bot.prototype.handleStartingRegions = function handleStartingRegions (ids) {
  // drop remaining timebank
  ids.shift()
  var best
  for (var i = 0; i < ids.length; i++) {
    var region = this.gameMap.regionsById[parseInt(ids[i], 10)]
    var value = 100
    value -= region.superRegion.wastelandCount * 10
    value -= region.superRegion.regions.length * 5
    value += region.superRegion.bonus * 3
    this.log(
      'SuperRegion: %s, Region: %s, Value: %s',
      region.superRegion.id,
      region.id, value
    )
    if (!best || value > best.value) {
      best = {
        region: region,
        value: value
      }
    }
  }
  this.output.write(best.region.id.toString() + '\n')
}

Bot.prototype.handleOpponentMoves = function handleOpponentMoves () {
  if (this.round !== 0) {
    this.log('Round %s end, took %s µs', this.round, this.duration)
  } else {
    this.log('Init took %s µs', this.duration)
  }
  this.round += 1

  this.duration = 0
}

Bot.prototype.handleComment = function handleComment (type, data) {
  if (data) {
    type += ' '
  } else {
    data = type
    type = ''
  }
  this.logger.write('# ' + type + data.join(' ') + '\n')
}

Bot.prototype.log = function log () {
  this.logger.write(util.format.apply(null, arguments) + '\n')
}
<|endoftext|>


<commit_before>import _ from 'lodash';
import irc from 'irc';
import logger from 'winston';
import Slack 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', 'slackUser', 'channelMapping', 'token'];

/**
 * 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);

    this.slack = new Slack(options.token);

    this.server = options.server;
    this.nickname = options.nickname;
    this.slackUser = options.slackUser;
    this.ircOptions = options.ircOptions;
    this.ircStatusNotices = options.ircStatusNotices || {};
    this.channels = _.values(options.channelMapping);

    this.rememberRecipientsFor = +(options.rememberRecipientsFor || 1000 * 60 * 10);
    this.lastPM = {
      user: '',
      timestamp: 0
    };

    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.login();

    const ircOptions = {
      userName: this.nickname,
      realName: this.nickname,
      channels: this.channels,
      floodProtection: true,
      floodProtectionDelay: 500,
      ...this.ircOptions
    };

    this.ircClient = new irc.Client(this.server, this.nickname, ircOptions);
    this.attachListeners();
  }

  attachListeners() {
    this.slack.on('open', () => {
      logger.debug('Connected to Slack');
    });

    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.sendToSlack(false, this.nickname, `_${error.command}_ : _${error.args.join(' ')}_`);
    });

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

    this.slack.on('presenceChange', (user, presence) => {
      if (user.name === this.slackUser) {
        if (presence === 'active') {
          this.ircClient.send('AWAY');
        } else {
          this.ircClient.send('AWAY', ' ');
        }
      }
    });

    this.slack.on('message', message => {
      // Ignore everything except the desired Slack user
      const user = this.slack.getUserByID(message.user);
      if (user && user.name === this.slackUser && message.type === 'message' &&
        (!message.subtype || ALLOWED_SUBTYPES.indexOf(message.subtype) > -1)) {
        this.sendToIRC(message);
      }
    });

    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('topic', (channel, topic, nick) => {
      this.sendToSlack(false, channel, `*${nick}* has changed the topic to: *${topic}*`);
    });

    this.ircClient.on('kick', (channel, nick, by, reason) => {
      this.sendToSlack(false, channel, `*${by}* has kicked *${nick}* (_${reason}_)`);
    });

    this.ircClient.on('kill', (nick, reason, channels) => {
      channels.forEach(channel => {
        this.sendToSlack(false, channel, `*${nick}* has been killed (_${reason}_)`);
      });
    });

    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);
      }
    });

    this.ircClient.on('whois', (info) => {
      const date = new Date(Date.now() - (info.idle || 0) * 1000);
      this.sendToSlack(false, this.nickname, `
        WHOIS for *${info.nick}*
        (_${info.user}@${info.host}_): _${info.realname}_
        _${info.server}_ :_${info.serverinfo}_
        ${info.accountinfo} _${info.account}_
        idle since _${date.toDateString()}, ${date.toLocaleTimeString()}_
      `);
    });

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

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

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

    if (this.ircStatusNotices.changeNick) {
      this.ircClient.on('nick', (oldNick, newNick, channels) => {
        channels.forEach(channel => {
          this.sendToSlack(false, channel, `*${oldNick}* is now known as *${newNick}*`);
        });
      });
    }

    if (this.ircStatusNotices.modes) {
      this.ircClient.on('+mode', (channel, by, mode, argument) => {
        this.sendToSlack(false, channel, `*${by}* sets mode *+${mode}* on _${argument || channel}_`);
      });
      this.ircClient.on('-mode', (channel, by, mode, argument) => {
        this.sendToSlack(false, channel, `*${by}* sets mode *-${mode}* on _${argument || channel}_`);
      });
    }
  }

  parseText(text) {
    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 } = this.slack.getChannelByID(channelId);
        return readable || `#${name}`;
      })
      .replace(/<@(U\w+)\|?(\w+)?>/g, (match, userId, readable) => {
        const { name } = this.slack.getUserByID(userId);
        return readable || `@${name}`;
      })
      .replace(/<(?!!)(\S+)>/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;
      });
  }

  sendDMToIRC(message, channel) {
    const text = this.parseText(message.getBody());
    const cmdMatch = (/^[A-Z]+\s\S/).test(text);
    const pmMatch = (/^(\S+):\s+(.+)/i).exec(text);

    if (cmdMatch) {
      logger.debug('Sending raw command to IRC', text);
      this.ircClient.send(...text.split(' '));
      channel.send(`_sent raw command_`);
      return;
    }

    if (pmMatch) {
      const [, user, msg] = pmMatch;
      logger.debug('Sending /msg to IRC user', user, msg);
      this.ircClient.send('PRIVMSG', user, msg);
      this.lastPM.user = user;
      this.lastPM.timestamp = Date.now();
      return;
    }

    const lastMessage = Date.now() - this.lastPM.timestamp;
    if (lastMessage < this.rememberRecipientsFor) {
      const user = this.lastPM.user;
      logger.debug('Sending /msg to last messaged IRC user', user, text);
      this.ircClient.send('PRIVMSG', user, text);
      this.lastPM.user = user;
      this.lastPM.timestamp = Date.now();
      return;
    }

    if (this.lastPM.user) {
      logger.debug('Not sending message', text, 'since user', this.lastPM.user, 'was messaged',
          lastMessage, 'ago, which is more than', this.rememberRecipientsFor);
      channel.send(`_it's been too long since your last message, please specify the user_`);
      return;
    }

    logger.debug('Not sending message', text, 'since no users have been messaged');
    channel.send(`_you haven't messaged anyone yet, please specify the user_`);
  }

  sendToIRC(message) {
    const channel = this.slack.getChannelGroupOrDMByID(message.channel);
    if (!channel) {
      logger.info('Received message from a channel the bot isn\'t in:',
        message.channel);
      return;
    }

    if (channel.is_im) {
      this.sendDMToIRC(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) {
      let text = this.parseText(message.getBody());

      if (message.subtype === 'me_message') {
        text = `\x01ACTION ${text}\x01`;
      }
      logger.debug('Sending message to IRC', channelName, text);
      this.ircClient.say(ircChannel, text);
    }
  }

  sendToSlack(author, channel, text) {
    const slackChannelName = channel === this.nickname ?
        this.slackUser :
        this.invertedMapping[channel.toLowerCase()];
    if (slackChannelName) {
      const slackChannel = this.slack.getChannelGroupOrDMByName(slackChannelName);

      // 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 && !slackChannel.is_im)) {
        logger.info('Tried to send a message to a channel the bot isn\'t in:', slackChannelName);
        return;
      }

      const currentChannelUsernames = (slackChannel.members || []).map(member =>
        this.slack.getUserByID(member).name
      );

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

      logger.debug('Sending message to Slack', mappedText, channel, '->', slackChannelName);
      if (author) {
        slackChannel.postMessage({
          text: mappedText,
          username: author,
          parse: 'full',
          icon_url: `http://api.adorable.io/avatars/48/${author}.png`
        });
      } else {
        slackChannel.send(mappedText);
      }
    }
  }
}

export default Bot;
<commit_msg>reset the PM timestamp when sending a raw command<commit_after>import _ from 'lodash';
import irc from 'irc';
import logger from 'winston';
import Slack 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', 'slackUser', 'channelMapping', 'token'];

/**
 * 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);

    this.slack = new Slack(options.token);

    this.server = options.server;
    this.nickname = options.nickname;
    this.slackUser = options.slackUser;
    this.ircOptions = options.ircOptions;
    this.ircStatusNotices = options.ircStatusNotices || {};
    this.channels = _.values(options.channelMapping);

    this.rememberRecipientsFor = +(options.rememberRecipientsFor || 1000 * 60 * 10);
    this.lastPM = {
      user: '',
      timestamp: 0
    };

    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.login();

    const ircOptions = {
      userName: this.nickname,
      realName: this.nickname,
      channels: this.channels,
      floodProtection: true,
      floodProtectionDelay: 500,
      ...this.ircOptions
    };

    this.ircClient = new irc.Client(this.server, this.nickname, ircOptions);
    this.attachListeners();
  }

  attachListeners() {
    this.slack.on('open', () => {
      logger.debug('Connected to Slack');
    });

    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.sendToSlack(false, this.nickname, `_${error.command}_ : _${error.args.join(' ')}_`);
    });

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

    this.slack.on('presenceChange', (user, presence) => {
      if (user.name === this.slackUser) {
        if (presence === 'active') {
          this.ircClient.send('AWAY');
        } else {
          this.ircClient.send('AWAY', ' ');
        }
      }
    });

    this.slack.on('message', message => {
      // Ignore everything except the desired Slack user
      const user = this.slack.getUserByID(message.user);
      if (user && user.name === this.slackUser && message.type === 'message' &&
        (!message.subtype || ALLOWED_SUBTYPES.indexOf(message.subtype) > -1)) {
        this.sendToIRC(message);
      }
    });

    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('topic', (channel, topic, nick) => {
      this.sendToSlack(false, channel, `*${nick}* has changed the topic to: *${topic}*`);
    });

    this.ircClient.on('kick', (channel, nick, by, reason) => {
      this.sendToSlack(false, channel, `*${by}* has kicked *${nick}* (_${reason}_)`);
    });

    this.ircClient.on('kill', (nick, reason, channels) => {
      channels.forEach(channel => {
        this.sendToSlack(false, channel, `*${nick}* has been killed (_${reason}_)`);
      });
    });

    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);
      }
    });

    this.ircClient.on('whois', (info) => {
      const date = new Date(Date.now() - (info.idle || 0) * 1000);
      this.sendToSlack(false, this.nickname, `
        WHOIS for *${info.nick}*
        (_${info.user}@${info.host}_): _${info.realname}_
        _${info.server}_ :_${info.serverinfo}_
        ${info.accountinfo} _${info.account}_
        idle since _${date.toDateString()}, ${date.toLocaleTimeString()}_
      `);
    });

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

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

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

    if (this.ircStatusNotices.changeNick) {
      this.ircClient.on('nick', (oldNick, newNick, channels) => {
        channels.forEach(channel => {
          this.sendToSlack(false, channel, `*${oldNick}* is now known as *${newNick}*`);
        });
      });
    }

    if (this.ircStatusNotices.modes) {
      this.ircClient.on('+mode', (channel, by, mode, argument) => {
        this.sendToSlack(false, channel, `*${by}* sets mode *+${mode}* on _${argument || channel}_`);
      });
      this.ircClient.on('-mode', (channel, by, mode, argument) => {
        this.sendToSlack(false, channel, `*${by}* sets mode *-${mode}* on _${argument || channel}_`);
      });
    }
  }

  parseText(text) {
    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 } = this.slack.getChannelByID(channelId);
        return readable || `#${name}`;
      })
      .replace(/<@(U\w+)\|?(\w+)?>/g, (match, userId, readable) => {
        const { name } = this.slack.getUserByID(userId);
        return readable || `@${name}`;
      })
      .replace(/<(?!!)(\S+)>/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;
      });
  }

  sendDMToIRC(message, channel) {
    const text = this.parseText(message.getBody());
    const cmdMatch = (/^[A-Z]+\s\S/).test(text);
    const pmMatch = (/^(\S+):\s+(.+)/i).exec(text);

    if (cmdMatch) {
      logger.debug('Sending raw command to IRC', text);
      this.ircClient.send(...text.split(' '));
      channel.send(`_sent raw command_`);
      // reset the PM timestamp, so you don't accidentally message them after a command
      this.lastPM.timestamp = 0;
      return;
    }

    if (pmMatch) {
      const [, user, msg] = pmMatch;
      logger.debug('Sending /msg to IRC user', user, msg);
      this.ircClient.send('PRIVMSG', user, msg);
      this.lastPM.user = user;
      this.lastPM.timestamp = Date.now();
      return;
    }

    const lastMessage = Date.now() - this.lastPM.timestamp;
    if (lastMessage < this.rememberRecipientsFor) {
      const user = this.lastPM.user;
      logger.debug('Sending /msg to last messaged IRC user', user, text);
      this.ircClient.send('PRIVMSG', user, text);
      this.lastPM.user = user;
      this.lastPM.timestamp = Date.now();
      return;
    }

    if (this.lastPM.user) {
      logger.debug('Not sending message', text, 'since user', this.lastPM.user, 'was messaged',
          lastMessage, 'ago, which is more than', this.rememberRecipientsFor);
      channel.send(`_it's been too long since your last message, please specify the user_`);
      return;
    }

    logger.debug('Not sending message', text, 'since no users have been messaged');
    channel.send(`_you haven't messaged anyone yet, please specify the user_`);
  }

  sendToIRC(message) {
    const channel = this.slack.getChannelGroupOrDMByID(message.channel);
    if (!channel) {
      logger.info('Received message from a channel the bot isn\'t in:',
        message.channel);
      return;
    }

    if (channel.is_im) {
      this.sendDMToIRC(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) {
      let text = this.parseText(message.getBody());

      if (message.subtype === 'me_message') {
        text = `\x01ACTION ${text}\x01`;
      }
      logger.debug('Sending message to IRC', channelName, text);
      this.ircClient.say(ircChannel, text);
    }
  }

  sendToSlack(author, channel, text) {
    const slackChannelName = channel === this.nickname ?
        this.slackUser :
        this.invertedMapping[channel.toLowerCase()];
    if (slackChannelName) {
      const slackChannel = this.slack.getChannelGroupOrDMByName(slackChannelName);

      // 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 && !slackChannel.is_im)) {
        logger.info('Tried to send a message to a channel the bot isn\'t in:', slackChannelName);
        return;
      }

      const currentChannelUsernames = (slackChannel.members || []).map(member =>
        this.slack.getUserByID(member).name
      );

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

      logger.debug('Sending message to Slack', mappedText, channel, '->', slackChannelName);
      if (author) {
        slackChannel.postMessage({
          text: mappedText,
          username: author,
          parse: 'full',
          icon_url: `http://api.adorable.io/avatars/48/${author}.png`
        });
      } else {
        slackChannel.send(mappedText);
      }
    }
  }
}

export default Bot;
<|endoftext|>


<commit_before>import _ from 'lodash';
import irc from 'irc';
import logger from 'winston';
import Slack 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', 'slackUser', 'channelMapping', 'token'];

/**
 * 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);

    this.slack = new Slack(options.token);

    this.server = options.server;
    this.nickname = options.nickname;
    this.slackUser = options.slackUser;
    this.ircOptions = options.ircOptions;
    this.ircStatusNotices = options.ircStatusNotices || {};
    this.channels = _.values(options.channelMapping);
    this.muteSlackbot = options.muteSlackbot || false;

    this.rememberRecipientsFor = +(options.rememberRecipientsFor || 1000 * 60 * 10);
    this.lastPM = {
      user: '',
      timestamp: 0
    };

    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.login();

    const ircOptions = {
      userName: this.nickname,
      realName: this.nickname,
      channels: this.channels,
      floodProtection: true,
      floodProtectionDelay: 500,
      ...this.ircOptions
    };

    this.ircClient = new irc.Client(this.server, this.nickname, ircOptions);
    this.attachListeners();
  }

  attachListeners() {
    this.slack.on('open', () => {
      logger.debug('Connected to Slack');
    });

    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.slack.on('error', error => {
      logger.error('Received error event from Slack', error);
    });

    this.slack.on('presenceChange', (user, presence) => {
      if (user.name === this.slackUser) {
        if (presence === 'active') {
          this.ircClient.send('AWAY');
        } else {
          this.ircClient.send('AWAY', ' ');
        }
      }
    });

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

    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(false, channel, `*${nick}* has joined the IRC channel`);
        }
      });
    }

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

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

  parseText(text) {
    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 } = this.slack.getChannelByID(channelId);
        return readable || `#${name}`;
      })
      .replace(/<@(U\w+)\|?(\w+)?>/g, (match, userId, readable) => {
        const { name } = this.slack.getUserByID(userId);
        return readable || `@${name}`;
      })
      .replace(/<(?!!)(\S+)>/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;
      });
  }

  sendDMToIRC(message, channel) {
    const text = this.parseText(message.getBody());
    const cmdMatch = (/^[A-Z]+\s\S/).test(text);
    const pmMatch = (/^(\S+):\s+(.+)/i).exec(text);

    if (cmdMatch) {
      logger.debug('Sending raw command to IRC', text);
      this.ircClient.send(...text.split(' '));
      channel.send(`_sent raw command_`);
      return;
    }

    if (pmMatch) {
      const [, user, msg] = pmMatch;
      logger.debug('Sending /msg to IRC user', user, msg);
      this.ircClient.send('PRIVMSG', user, msg);
      this.lastPM.user = user;
      this.lastPM.timestamp = Date.now();
      return;
    }

    const lastMessage = Date.now() - this.lastPM.timestamp;
    if (lastMessage < this.rememberRecipientsFor) {
      const user = this.lastPM.user;
      logger.debug('Sending /msg to last messaged IRC user', user, text);
      this.ircClient.send('PRIVMSG', user, text);
      this.lastPM.user = user;
      this.lastPM.timestamp = Date.now();
      return;
    }

    if (this.lastPM.user) {
      logger.debug('Not sending message', text, 'since user', this.lastPM.user, 'was messaged',
          lastMessage, 'ago, which is more than', this.rememberRecipientsFor);
      channel.send(`_it's been too long since your last message, please specify the user_`);
      return;
    }

    logger.debug('Not sending message', text, 'since no users have been messaged');
    channel.send(`_you haven't messaged anyone yet, please specify the user_`);
  }

  sendToIRC(message) {
    const channel = this.slack.getChannelGroupOrDMByID(message.channel);
    if (!channel) {
      logger.info('Received message from a channel the bot isn\'t in:',
        message.channel);
      return;
    }

    if (this.muteSlackbot && message.user === 'USLACKBOT') {
      logger.debug(`Muted message from Slackbot: "${message.text}"`);
      return;
    }

    if (channel.is_im) {
      this.sendDMToIRC(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) {
      let text = this.parseText(message.getBody());

      if (message.subtype === 'me_message') {
        text = `\x01ACTION ${text}\x01`;
      }
      logger.debug('Sending message to IRC', channelName, text);
      this.ircClient.say(ircChannel, text);
    }
  }

  sendToSlack(author, channel, text) {
    const slackChannelName = channel === this.nickname ?
        this.slackUser :
        this.invertedMapping[channel.toLowerCase()];
    if (slackChannelName) {
      const slackChannel = this.slack.getChannelGroupOrDMByName(slackChannelName);

      // 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 && !slackChannel.is_im)) {
        logger.info('Tried to send a message to a channel the bot isn\'t in:', slackChannelName);
        return;
      }

      const currentChannelUsernames = (slackChannel.members || []).map(member =>
        this.slack.getUserByID(member).name
      );

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

      logger.debug('Sending message to Slack', mappedText, channel, '->', slackChannelName);
      if (author) {
        slackChannel.postMessage({
          text: mappedText,
          username: author,
          parse: 'full',
          icon_url: `http://api.adorable.io/avatars/48/${author}.png`
        });
      } else {
        slackChannel.send(mappedText);
      }
    }
  }
}

export default Bot;
<commit_msg>send topic changes to Slack<commit_after>import _ from 'lodash';
import irc from 'irc';
import logger from 'winston';
import Slack 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', 'slackUser', 'channelMapping', 'token'];

/**
 * 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);

    this.slack = new Slack(options.token);

    this.server = options.server;
    this.nickname = options.nickname;
    this.slackUser = options.slackUser;
    this.ircOptions = options.ircOptions;
    this.ircStatusNotices = options.ircStatusNotices || {};
    this.channels = _.values(options.channelMapping);
    this.muteSlackbot = options.muteSlackbot || false;

    this.rememberRecipientsFor = +(options.rememberRecipientsFor || 1000 * 60 * 10);
    this.lastPM = {
      user: '',
      timestamp: 0
    };

    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.login();

    const ircOptions = {
      userName: this.nickname,
      realName: this.nickname,
      channels: this.channels,
      floodProtection: true,
      floodProtectionDelay: 500,
      ...this.ircOptions
    };

    this.ircClient = new irc.Client(this.server, this.nickname, ircOptions);
    this.attachListeners();
  }

  attachListeners() {
    this.slack.on('open', () => {
      logger.debug('Connected to Slack');
    });

    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.slack.on('error', error => {
      logger.error('Received error event from Slack', error);
    });

    this.slack.on('presenceChange', (user, presence) => {
      if (user.name === this.slackUser) {
        if (presence === 'active') {
          this.ircClient.send('AWAY');
        } else {
          this.ircClient.send('AWAY', ' ');
        }
      }
    });

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

    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('topic', (channel, topic, nick) => {
      this.sendToSlack(false, channel, `*${nick}* has changed the topic to: *${topic}*`);
    });

    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(false, channel, `*${nick}* has joined the IRC channel`);
        }
      });
    }

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

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

  parseText(text) {
    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 } = this.slack.getChannelByID(channelId);
        return readable || `#${name}`;
      })
      .replace(/<@(U\w+)\|?(\w+)?>/g, (match, userId, readable) => {
        const { name } = this.slack.getUserByID(userId);
        return readable || `@${name}`;
      })
      .replace(/<(?!!)(\S+)>/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;
      });
  }

  sendDMToIRC(message, channel) {
    const text = this.parseText(message.getBody());
    const cmdMatch = (/^[A-Z]+\s\S/).test(text);
    const pmMatch = (/^(\S+):\s+(.+)/i).exec(text);

    if (cmdMatch) {
      logger.debug('Sending raw command to IRC', text);
      this.ircClient.send(...text.split(' '));
      channel.send(`_sent raw command_`);
      return;
    }

    if (pmMatch) {
      const [, user, msg] = pmMatch;
      logger.debug('Sending /msg to IRC user', user, msg);
      this.ircClient.send('PRIVMSG', user, msg);
      this.lastPM.user = user;
      this.lastPM.timestamp = Date.now();
      return;
    }

    const lastMessage = Date.now() - this.lastPM.timestamp;
    if (lastMessage < this.rememberRecipientsFor) {
      const user = this.lastPM.user;
      logger.debug('Sending /msg to last messaged IRC user', user, text);
      this.ircClient.send('PRIVMSG', user, text);
      this.lastPM.user = user;
      this.lastPM.timestamp = Date.now();
      return;
    }

    if (this.lastPM.user) {
      logger.debug('Not sending message', text, 'since user', this.lastPM.user, 'was messaged',
          lastMessage, 'ago, which is more than', this.rememberRecipientsFor);
      channel.send(`_it's been too long since your last message, please specify the user_`);
      return;
    }

    logger.debug('Not sending message', text, 'since no users have been messaged');
    channel.send(`_you haven't messaged anyone yet, please specify the user_`);
  }

  sendToIRC(message) {
    const channel = this.slack.getChannelGroupOrDMByID(message.channel);
    if (!channel) {
      logger.info('Received message from a channel the bot isn\'t in:',
        message.channel);
      return;
    }

    if (this.muteSlackbot && message.user === 'USLACKBOT') {
      logger.debug(`Muted message from Slackbot: "${message.text}"`);
      return;
    }

    if (channel.is_im) {
      this.sendDMToIRC(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) {
      let text = this.parseText(message.getBody());

      if (message.subtype === 'me_message') {
        text = `\x01ACTION ${text}\x01`;
      }
      logger.debug('Sending message to IRC', channelName, text);
      this.ircClient.say(ircChannel, text);
    }
  }

  sendToSlack(author, channel, text) {
    const slackChannelName = channel === this.nickname ?
        this.slackUser :
        this.invertedMapping[channel.toLowerCase()];
    if (slackChannelName) {
      const slackChannel = this.slack.getChannelGroupOrDMByName(slackChannelName);

      // 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 && !slackChannel.is_im)) {
        logger.info('Tried to send a message to a channel the bot isn\'t in:', slackChannelName);
        return;
      }

      const currentChannelUsernames = (slackChannel.members || []).map(member =>
        this.slack.getUserByID(member).name
      );

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

      logger.debug('Sending message to Slack', mappedText, channel, '->', slackChannelName);
      if (author) {
        slackChannel.postMessage({
          text: mappedText,
          username: author,
          parse: 'full',
          icon_url: `http://api.adorable.io/avatars/48/${author}.png`
        });
      } else {
        slackChannel.send(mappedText);
      }
    }
  }
}

export default Bot;
<|endoftext|>


<commit_before>import _ from 'lodash';
import irc from 'irc';
import logger from 'winston';
import { MemoryDataStore, RtmClient, WebClient } 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'];

/**
 * 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.channels = _.values(options.channelMapping);
    this.muteSlackbot = options.muteSlackbot || false;

    const ircOptions = {
      userName: this.nickname,
      realName: this.nickname,
      channels: this.channels,
      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');
    });

    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('message', message => {
      // Ignore bot messages and people leaving/joining
      if (message.type === 'message' &&
        (!message.subtype || ALLOWED_SUBTYPES.indexOf(message.subtype) > -1)) {
        this.sendToIRC(message);
      }
    });

    this.slack.rtm.on('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.newClient(user);
      } else {
        this.deleteClient(user);
      }
    });

    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);
  }

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

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

  newClient(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) {
        const client = new irc.Client(this.server, `${user.name}-slack`, userOptions);
        client.connect(5, msg => {
          if (msg.rawCommand === '001') {
            this.ircClients[user.id] = client;
            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];
      });
    }
  }

  sendToIRC(message) {
    const { dataStore } = this.slack.rtm;
    const channel = dataStore.getChannelGroupOrDMById(message.channel);
    if (!channel) {
      logger.info('Received message from a channel the bot isn\'t in:',
        message.channel);
      return;
    }

    if (this.muteSlackbot && message.user === 'USLACKBOT') {
      logger.debug(`Muted message from Slackbot: "${message.text}"`);
      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 user = dataStore.getUserById(message.user);
      let 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) {
        text = `<${user.name}> ${text}`;
      } else if (message.subtype === 'me_message') {
        text = `Action: ${user.name} ${text}`;
      }
      logger.debug('Sending message to IRC', channelName, text);
      this.ircClient.say(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
      );

      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);
    }
  }
}

export default Bot;
<commit_msg>Add some error listeners to user IRC clients<commit_after>import _ from 'lodash';
import irc from 'irc';
import logger from 'winston';
import { MemoryDataStore, RtmClient, WebClient } 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'];

/**
 * 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.channels = _.values(options.channelMapping);
    this.muteSlackbot = options.muteSlackbot || false;

    const ircOptions = {
      userName: this.nickname,
      realName: this.nickname,
      channels: this.channels,
      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');
    });

    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('message', message => {
      // Ignore bot messages and people leaving/joining
      if (message.type === 'message' &&
        (!message.subtype || ALLOWED_SUBTYPES.indexOf(message.subtype) > -1)) {
        this.sendToIRC(message);
      }
    });

    this.slack.rtm.on('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.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);
  }

  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, `${user.name}-slack`, 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(message) {
    const { dataStore } = this.slack.rtm;
    const channel = dataStore.getChannelGroupOrDMById(message.channel);
    if (!channel) {
      logger.info('Received message from a channel the bot isn\'t in:',
        message.channel);
      return;
    }

    if (this.muteSlackbot && message.user === 'USLACKBOT') {
      logger.debug(`Muted message from Slackbot: "${message.text}"`);
      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 user = dataStore.getUserById(message.user);
      let 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) {
        text = `<${user.name}> ${text}`;
      } else if (message.subtype === 'me_message') {
        text = `Action: ${user.name} ${text}`;
      }
      logger.debug('Sending message to IRC', channelName, text);
      this.ircClient.say(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
      );

      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);
    }
  }
}

export default Bot;
<|endoftext|>