/*!
 * Coolony's Kiwi
 * Copyright ©2012 Pierre Matri <pierre.matri@coolony.com>
 * MIT Licensed
 */

/**
 * Module dependencies
 */

// if browser
var frame;
// end
// if node
var _ = require('underscore');
var fs = require('fs');
var path = require('path');
var exists = fs.exists || path.exists;
var frame = require('frame');
// end

/**
 * Constants
 */

var DEFAULT_FILE_EXTENSION = '.kiwi';
var FILTER_SPLIT_RE = /\|(?=(?:[^'"]|'[^']*'|"[^"]*")*$)/g;
var FILTER_SPLIT_ARGS_RE = /\,(?=(?:[^'"]|'[^']*'|"[^"]*")*$)/g;
var FILTER_MATCH_RE = /^(\w+)\s*(?:\((.*)\))?$/i;

// if node
/**
 * Load `filePath`, and invoke `callback(err, data)`.
 *
 * @param {String} filePath
 * @param {Function} callback
 * @api private
 */

exports.loadTemplate = function(filePath, callback) {
  fs.readFile(filePath, 'utf-8', function onLoad(err, data) {
    if(err) return callback(err);
    callback(null, data);
  });
};

/**
 * Lookup `template` relative to `parentTemplate`, and invoke
 * `callback(err, filePath)`.
 *
 * @param {String} template
 * @param {Template} parentTemplate
 * @param (Function} callback
 * @api private
 */

exports.lookupTemplate = function(name, parentTemplate, callback) {
  var ext = path.extname(name);
  var parentPath = parentTemplate.options.path;
  var additionalPaths = [process.cwd()];
  if(parentPath) additionalPaths.push(path.dirname(parentPath));
  var lookupPaths = parentTemplate.options.lookupPaths || [];

  function doTry(tryPath, next) {
    exists(tryPath, function(yes) {
      if(yes) return callback(null, tryPath);
      next();
    });
  }

  var tryPaths = [];
  if(name[0] === '/') tryPaths.push(name);
  lookupPaths.concat(additionalPaths).forEach(function(lookupPath) {
    tryPaths.push(path.join(lookupPath, name));
    tryPaths.push(path.join(lookupPath, name + DEFAULT_FILE_EXTENSION));
  });

  frame.asyncForEach(tryPaths, doTry, function onDone() {
    callback(new Error('RenderError: Can\'t locate template ' +
                       '`' + name + '`.'
                       ));
  });
};
// end

/**
 * Asynchronous ForEach implementation.
 * Iterates over `array`, invoking `fn(item, args…, next)` for each item, and
 * invoke `callback(err)` when done.
 *
 * @see http://zef.me/3420/async-foreach-in-javascript
 * @param {Array} array
 * @param {Function} fn
 * @param {Mixed[]} [args]
 * @param {Function} callback
 * @api puclic
 */

function tmpAsyncForEach(array, fn, args, callback) {

  if(typeof args === 'function' && !callback) {
    callback = args;
    args = null;
  }

  if(!args) args = [];
  array = array.slice(0);

  function handleProcessedCallback(err) {
    if(err) return callback(err);
    if(array.length > 0) {
      setTimeout(processOne, 0);
    } else {
      callback();
    }
  }

  function processOne() {
    var item = array.shift();
    fn.apply(this, [item].concat(args).concat([handleProcessedCallback]));
  }
  if(array.length > 0) {
    setTimeout(processOne, 0);
  } else {
    callback();
  }
}
var asyncForEach = frame ? frame.asyncForEach : tmpAsyncForEach;

/**
 * Asynchronously apply `processor` to `input`, and invoke
 * `callback(err, result)`.
 *
 * @param {Mixed} input
 * @param {Function} processor
 * @param {Function} callback
 * @api private
 */

var apply = exports.apply = function(input, processor, args, callback) {

  if(typeof args === 'function' && !callback) {
    callback = args;
    args = null;
  }

  function done(err, result) {
    if(err) return callback(err);
    callback(null, result);
  }

  processor.apply(this, [input].concat(args || []).concat([done]));
};

/**
 * Asynchronously apply `processors` to `input` with `args`, and invoke
 * `callback(err, result)`.
 *
 * @param {Mixed} input
 * @param {Function[]} processors
 * @param {Mixed[]} [args]
 * @param {Function} callback
 * @api private
 */

exports.applyAll = function(input, processors, args, callback) {

  function applyOne(processor, next) {
    apply(input, processor, args || [], function onApplied(err, result) {
      if(err) return next(err);
      input = result;
      next();
    });
  }

  function done(err) {
    if(err) return callback(err);
    callback(null, input);
  }

  if(typeof args === 'function' && !callback) {
    callback = args;
    args = null;
  }
  asyncForEach(processors, applyOne, done);
};

/**
 * Asynchronously compiles `tokens`, and invoke
 * `callback(err, compiled)` with `compiled` as an array.
 *
 * @param {BaseToken[]} tokens
 * @param {Function} callback
 * @api private
 */

function compileTokenArray(tokens, compiler, callback) {
  var acc = [];
  var index = 0;

  function compileOne(token, next) {
    token.compile(compiler, function onCompiled(err, compiled) {
      if(err) return next(err);
      acc.push(compiled);
      next(null, compiled);
    });
    index++;
  }

  function done(err) {
    if(err) return callback(err);
    callback(null, acc);
  }

  asyncForEach(tokens, compileOne, done);
}
exports.compileTokenArray = compileTokenArray;

/**
 * Asynchronously compiles `tokens`, glue them, and invoke
 * `callback(err, compiled)`.
 *
 * @param {BaseToken[]} tokens
 * @param {Compiler} compiler
 * @param {Function} callback
 * @api private
 */

exports.compileTokens = function(tokens, compiler, callback) {
  compileTokenArray(tokens, compiler, function(err, compiled) {
    if(err) return callback(err);
    callback(null, compiled.join(''));
  });
};

/**
 * Escape `str` for use in template compilation.
 *
 * @param {String} str
 * @return {String} Escaped `str`.
 * @api private
 */

exports.escapeCompiledString = function(str) {
  return str.replace(/([\\"])/g, '\\$1')
            .replace(/\n/g, '\\n')
            .replace(/\r/g, '\\r')
            .replace(/\t/g, '\\t');
};

/**
 * Return `true` if `input` is iterable and not a string.
 *
 * @param {Mixed} input
 * @return {Boolean}
 * @api private
 */

exports.isIterable = function(input) {
  return typeof input === 'object' && !(input instanceof String);
};

/**
 * Parse `filters` with `defaults`, and return the parsed string to include
 * in compiled template.
 *
 * @param {String} filters
 * @param {Mixed[]} defaults
 * @return {String}
 * @api private
 */

exports.parseFilters = function(filters, defaults) {
  var raw = false;
  defaults = defaults || [];
  var splittedFilters = filters.split(FILTER_SPLIT_RE);

  if(!splittedFilters) {
    throw new Error('Compilation error: Unable to parse filters `' +
                    filters +
                    '`.'
                    );
  }

  var parsedFilters = splittedFilters
    .filter(function filterOne(filter) {
      if(filter === 'raw') {
        raw = true;
        return false;
      }
      return true;
    })
    .map(function mapOne(filter) {
      var parsedFilter = filter.match(FILTER_MATCH_RE);

      if(!parsedFilter) {
        throw new Error('Compilation error: Unable to parse filter `' +
                        filter +
                        '`.'
                        );
      }

      parsedFilter[1] = parsedFilter[1].replace('"', '\"');
      if(!parsedFilter[2]) {
        return '"' + parsedFilter[1] + '"';
      }
      var splittedArgs = parsedFilter[2].split(FILTER_SPLIT_ARGS_RE);
      return '["' + parsedFilter[1] + '", ' +
             splittedArgs.join(',') + ']';
    });

    if(!raw) parsedFilters = parsedFilters.concat(defaults);
    return _.uniq(parsedFilters);
};

/**
 * Simple class inheritance.
 * Make `subclass` inherit from `superclass`.
 *
 * based on http://peter.michaux.ca/articles/class-based-inheritance-in-javascript
 * @param {Object} subclass
 * @param {Object} superclass
 * @api public
 */

exports.inherits = function(subclass, superclass) {
  function Dummy(){}
  Dummy.prototype = superclass.prototype;
  subclass.prototype = new Dummy();
  subclass.prototype.constructor = subclass;
  subclass._superclass = superclass;
  subclass._superproto = superclass.prototype;
};

/**
 * Module exports
 */

module.exports = exports;


/*!
 * Coolony's Kiwi
 * Copyright ©2012 Pierre Matri <pierre.matri@coolony.com>
 * MIT Licensed
 */

/**
 * Module dependencies
 */

// if browser
//var Cache = require('./cache');
//var CappedCache = Cache;
// end
// if node
var path = require('path');
var frame = require('frame');
var Cache = frame.Cache;
var CappedCache = frame.CappedCache;
var construct = frame.classes.construct;
var _ = require('underscore');
// end

var utils = require('./utils');
var token = require('./token');
var tools = require('./tools');
var Compiler = require('./compiler');
var filter = require('./filter');

/**
 * Constants
 */

var DEFAULTS = {
  lookup: utils.lookupTemplate,
  load: utils.loadTemplate,
  path: null,
  cache: true,
  cacheHandler: Cache,
  cacheOptions: [],
  lookupPaths: [],
  cacheTmplHandler: CappedCache,
  cacheTmplOptions: [1000],
  useIsolatedTmplCache: true,
  cacheContext: null,
  strict: true,
  eachCounters: true,
  _parent: null,
  _cacheAttachment: '_cache',
  _cacheTmplAttachment: '_nestCache'
};
var TEMPLATE_EXPORTS = {filter: filter, utils: utils, tools: tools};

/**
 * Initializes `Template` with optionnally the given `str` and
 * `options`.
 *
 * @param {String} [str]
 * @param {Object} [options]
 * @api public
 */

function Template(str, options) {

  // Handle the case where the only argument passed is the `options` object
  if(_.isObject(str) && !options){
    options = str;
    str = null;
  }

  // Create options if not provided
  options = options ? _.clone(options) : {};

  // Set default cache behavior
  // if node
  if(!_.isBoolean(options.cache)) {
    options.cache = process.env.NODE_ENV === 'production';
  }
  // end

  // Merges given `options` with `DEFAULTS`
  options = _.defaults(options, DEFAULTS);
  options.cacheContext = options.cacheContext || Template;

  // Sets instance variables
  this.template = str;
  this.options = options;
  this._compiled = null;

  // Creates the cache if not already done
  if(options.cache && !(this._getCache() instanceof options.cacheHandler)) {
    var cacheOptions = [options.cacheHandler].concat(options.cacheOptions);
    options.cacheContext[options._cacheProp] = typeof window !== 'undefined' ?
                                                 new options.cacheHandler() :
                                                 construct.apply(this,
                                                                 cacheOptions);
  }
}

/**
 * Get cache handler
 *
 * @return {Object}
 * @api private
 */

Template.prototype._getCache = function() {
  return this.options.cacheContext[this.options._cacheProp];
};

/**
 * Load the template from disk at `filePath`, and invoke `callback(err)`.
 *
 * @param {String} filePath
 * @param {Function} callback
 * @api public
 */

Template.prototype.loadFile = function(filePath, callback) {
//if node
  var _this = this;
  this.options.load(filePath, function onLoad(err, data) {
    if(err) return callback(err);
    _this.options.path = path.normalize(filePath);
    _this.template = data;
    callback();
  });
// end
// if browser
// callback(new Error('Client mode does not support reading from file.'));
// end
};

/**
 * Render the template with given `data`, and invoke `callback(err, compiled)`.
 *
 * @param {Object} [data]
 * @param {Function} callback
 * @api public
 */

Template.prototype.render = function(data, callback) {

  // Support callback as 1st arg
  if(_.isFunction(data) && !callback){
    callback = data;
    data = null;
  }

  // Data defaults and / or cloning
  data = data ? _.clone(data) : data;

  // Check whether we have the compiled template ready in the object or in cache
  var cacheKey = 'template::' + this._cacheKey();
  if(!this.compiled && this.options.cache) {
    this._compiled = this._getCache().get(cacheKey);
  }

  // Render it if we got it…
  if(this._compiled) return this._renderCompiled(data, callback);

  // …or compile it if we don't
  var _this = this;
  this._compile(function(err){
    if(err) return callback(err);
    _this._renderCompiled(data, callback);
  });
};

/**
 * Load the template from disk at `filePath`, render it with given `data`,
 * and invoke `callback(err, compiled)`.
 *
 * @param {String} filePath
 * @param {Object} [data]
 * @param {Function} callback
 * @api public
 */

Template.prototype.loadAndRender = function(filePath, data, callback) {
// if node
  var _this = this;

  // Support callback as 2nd arg
  if(_.isFunction(data) && !callback){
    callback = data;
    data = {};
  }

  this.loadFile(filePath, function onLoaded(err) {
    if(err) return callback(err);
    _this.render(data, callback);
  });
// end
// if browser
// callback(new Error('Client mode does not support reading from file.'));
// end
};

/**
 * Render the compiled template with given `data`, and invoke
 * `callback(err, compiled)`.
 *
 * @param {String} filePath
 * @param {Function} callback
 * @api private
 */

Template.prototype._renderCompiled = function onRendered(data, callback) {
  try {
    // Apply compiled function to itself (needed for function helpers)
    this._compiled.call(this._compiled, this, TEMPLATE_EXPORTS, _, data,
                        token.helpers, callback);
  } catch(err) {
    callback(err);
  }
};

/**
 * Compile the template, and invoke `callback(err, compiled)`.
 *
 * @param {Function} callback
 * @api private
 */

Template.prototype._compile = function(callback) {
  var _this = this;
  if(!_.isString(this.template)) {
    return callback(new Error('Template contents not set'));
  }

  new Compiler(this).compile(function(err, compiled) {

    if(err) return callback(err);

    // Save the compiled template in this object
    _this._compiled = compiled;

    // Cache it, if possible
    var cacheKey = 'template::' + _this._cacheKey();
    if(_this.options.cache) {
      _this._getCache().cache(cacheKey, compiled);
    }

    // Invoke the callback
    callback(null, compiled);
  });
};

/**
 * Render `nested` template with `data`, and invoke `callback(err, rendered)`.
 *
 * @param {String} nested
 * @param {Object} data
 * @param {Function} callback
 * @api private
 */

Template.prototype._nest = function(nested, data, callback) {
  var options = _.clone(this.options);
  if(options.useIsolatedTmplCache) {
    options._cacheAttachment = options._cacheTmplAttachment;
    options.cacheHandler = options.cacheTmplHandler;
    options.cacheOptions = options.cacheTmplOptions;
  }
  new Template(nested, options).render(data, function onDone(err, rendered) {
    if(err) return callback(err);
    callback(null, rendered);
  });
};

/**
 * Render `nested` template with `data`, and invoke `callback(err, rendered)`.
 *
 * @param {String} nested
 * @param {Object} data
 * @param {Function} callback
 * @api private
 */

Template.prototype._renderRelative = function(name, data, rendered, callback) {
  var _this = this;
  var cacheKey;
  var cachedPath;
  var template;

  function onTemplateLocated(err, filePath) {
    if(err) return callback(err);
    if(_this.options.cache) {
      _this._getCache().cache(cacheKey, filePath);
    }
    template.loadFile(filePath, onTemplateLoaded);
  }

  function onTemplateLoaded(err, source) {
    if(err) return callback(err);
    template.render(data, onTemplateRendered);
  }

  function onTemplateRendered(err, result) {
    if(err) return callback(err);
    callback(null, result);
  }

  if(name instanceof Template) {

    template = name;
    template.options._parent = rendered;
    template.render(data, onTemplateRendered);

  } else {

    var options = _.clone(this.options);
    options._parent = rendered;
    template = new Template(options);
    cacheKey = 'path::' + this._cacheKey() + '::' + name;

    // Handle cache
    if(this.options.cache) {
      cachedPath = this._getCache().get(cacheKey);
    }
    if(cachedPath) {
      template.loadFile(cachedPath, onTemplateLoaded);
    } else {
      this.options.lookup(name, this, onTemplateLocated);
    }
  }
};

/**
 * Calculate  and return cache key.
 *
 * @return {String} Cache key.
 * @api private
 */

Template.prototype._cacheKey = function() {
  return this.options.path || this.template || null;
};

/**
 * Module exports
 */

module.exports = Template;


#!/usr/bin/env node

var program = require('commander');
var htmlr = require("../lib/main.js");
var Htmlr = htmlr.Htmlr;
var path = require('path');
var fs = require('fs');

program
  .version(htmlr.version)
  .usage('[options] <file>')
  .option('-d, --data [data]', 'json or file to pass to template')
  .option('-l, --layout [layout]', 'layout file (blank for no layout)')
  .option('-o, --output [filename]', 'output file (blank for just changing extension)')
  .option('-w, --watch', 'watch for file changes and render')
  .parse(process.argv);

// used to fix fs.watch on windows triggering cascading events
// the function fn can only be called once per rate
var rate_limit_fn = function (fn, rate) {
  var allowed = true;
  return function () {
    if (allowed) {
      allowed = false;
      fn.apply(null, [].slice.call(arguments, 0));
      setTimeout(function () {
        allowed = true;
      }, rate);
    }
  }
}

// get optional data from command line as json
// or file containing json or javascript
function get_data (callback) {
  var data;
  try {
    data = program.data ? JSON.parse(program.data) : {};
    callback(data);
  } catch (err) {
    fs.readFile(program.data, function (err, str) {
      str = '' + str;
      if (!err) {
        try {
          data = JSON.parse(str);
          callback(data);
        } catch (err) {
          data = eval(str);
          callback(data);
        }
      }
    });
  }
  return data;
}

function get_template_file(template_file) {
  // append '.htmlr' to end if file doesn't exist
  try {
    fs.statSync(template_file);
  } catch (err) {
    template_file += '.htmlr';
  }
  try {
    fs.statSync(template_file);
  } catch (err) {
    throw new Error('Error: template file "' + template_file + '" not found');
  }
  return template_file;
}

function get_output_file(template_file) {
  return path.join(path.dirname(template_file),
                   path.basename(template_file, path.extname(template_file))
                   + '.html');
}

function render_template(template_file, data, callback) {
  var html;
  fs.readFile(template_file, function (err, str) {
    if (err) {
      console.error('Error: file "' + template_file + '" can not be opened');
    } else {
      try {
        html = htmlr.compile(str, data);
        // check to see if results should be given to a layout file
        if (!program.layout) {
          program.layout = 'layout.htmlr';
        }
        if (typeof program.layout === 'string' && program.layout !== template_file) {
          fs.readFile(program.layout, function (err, str) {
            if (!err) {
              data.body = html; // 'body' is the express property used for layouts
              html = htmlr.compile(str, data);
            }
          });
        }
        callback(html);
      } catch (err) {
        console.error('Error: failed to render "' + template_file + '" -- '
          + err.name + ': ' + err.message);
      }
    }
  });
}

function template_output (template_file, data, output) {
  //try {
    render_template(template_file, data, function (html) {
      if (program.output) {
        if (program.output === true) {
          program.output = get_output_file(template_file);
        }
        fs.writeFile(program.output, html, function (err) {
          if (err) throw err;
          console.log('rendered ' + program.output);
        });
      } else {
        console.log(html);
      }
    });
  //} catch (err) {
  //  console.error('error rendering ' + template_file + ': ' + err);
  //}
}

function hideCursor(){
    process.stdout.write('\033[?25l');
};

function showCursor(){
    process.stdout.write('\033[?25h');
};

function main (program) {
  var data
    , template_file
    , cleanup
    , i, ilen
  ;
  if (program.args.length == 0) {
    console.log(program.helpInformation());
  } else {
    // setup display for watching for file changes
    if (program.watch) {
      console.log('\nwatching...');
      hideCursor();
      cleanup = function () {
        showCursor();
        console.log('\n');
        process.exit();
      }
      try {
        process.on('SIGINT', cleanup);
      } catch (err) {
        // fallback for windows
        try {
          process.on('exit', cleanup);
        } catch (err) {}
      }
    }

    // get data
    get_data(function (data) {
      // loop through all given files outputing results
      for (var i = 0, ilen = program.args.length; i < ilen; ++i) {
        try {
          template_file = get_template_file(program.args[i]);
        } catch (err) {
          console.log(err);
          continue;
        }
        // only allow file change events once per second
        var do_output = (function (the_template_file) {
          return function (event, filename) {
            template_output(the_template_file, data, program.output);
          };
        }(template_file));
        // generate output and maybe set watch event
        if (program.watch) {
          fs.watch(template_file, rate_limit_fn(do_output, 1000));
        }
        do_output();
      }
    })
  }
}

main(program);


/**
 * Utility functions for the pl-copyfind package.
 * No part of this file may be copied or retransmitted without adhering to the GPL license
 * This copyright notice may not be removed, without express permission of the copyright holder.
 * @module pl-copyfind/words.js
 * @license GPL
 * https://github.com/cmroanirgo/pl-copyfind/blob/master/LICENSE.md
 */
"use strict";
var _ = require("./not_");

;(function() {
	var root           = this;
	var previous_exports = root.words;

	//function ispunct(ch) { return /[!"#$%&'()*+,-.\/:;<=>?@\[\\\]^_`{|}~]/.test(ch); }
	function ispunct(ch) {
		ch = ch.charCodeAt(0);
		return (ch>=0x21 && ch<=0x2F)  || (ch>=0x3a && ch<=0x40) || (ch>=0x5B && ch<=0x60) || (ch>=0x7F);
		 }
	function isdigit(ch) { return /[0-9]/.test(ch); }
	function isalpha(ch) { return /[a-zA-Z]/.test(ch); }

	var _words = {

		removePunct: function(str)
		{
			var letters = [];

			for(var i=0;i<str.length;i++)
			{
				var ch = str.charAt(i);
				if(!ispunct(ch))
					letters.push(ch);
			}
			return letters.join("");
		},

		outerPunct: function(str)
		{
			var l=0, r=str.length-1;

			for(var i=0;i<str.length;i++)
			{
				var ch = str.charAt(i);

				if(ispunct(ch))
					l=i;
				else
					break;
			}
			for(var i=str.length-1;i>l;i--)
			{
				var ch = str.charAt(i);
				if(ispunct(ch))
					r=i-1;
				else break;
			}
			if (r<l)
				return "";
			return str.substr(l, r-l+1);
		},

		removeNumbers: function(str)
		{
			var letters = [];

			for(var i=0;i<str.length;i++)
			{
				var ch = str.charAt(i);
				if(!isdigit(ch))
					letters.push(ch);
			}
			return letters.join("");
		},

		toLowerCase: function(str)
		{
			return str.toLowerCase();
		},

		isWord: function(str)
		{
			var wordlen = str.length;
			if(wordlen < 1) return false;
			if( !isalpha(str.charAt(0)) ) return false;
			if( !isalpha(str.charAt(wordlen-1)) ) return false;

			for(var i=1;i<wordlen-2;i++)
			{
				var ch = str.charAt(i);
				if( isalpha(ch) ) continue;
				if( ch == '-' ) continue;
				if( ch == '\'' ) continue;
				return false;
			}
			return true;
		},

		/*hash: function(s) {
			// using Java's hash method (except on empty string)
			var hash = 0, i, l, char;
	      if (s.length == 0) return 1;
	      for (i = 0, l = s.length; i < l; i++) {
	        char = s.charCodeAt(i);
	        hash = ((hash << 5) - hash) + char;
	        hash |= 0; // Convert to 32bit integer
	      }
	      return hash;

		}*/
		hash: function(str)
		{
			var inhash = 0;
			var charcount = 0;
			var char;

			if(str.length == 0)
				return 1;	// if word is null, return 1 as hash value
			else
			for (var i=0; i<str.length; i++)
			{
				char = str.charCodeAt(i);
				inhash=	((inhash << 7)|(inhash >>> 25)) ^ char;	// xor into the rotateleft(7) of inhash
	       		inhash >>>= 0; // Convert to 32bit unsigned integer
			}
			return inhash;
		}

	};

	var words = root.words = _words;

	if( !_.isUndefined(exports)) {
		if( !_.isUndefined(module) && module.exports ) {
			exports = module.exports = words;
		}
		exports.words = words;
	}
	else {
		root.words = words;
	}
}).call(this);


const Botkit = require('../../../botkit/CoreBot');
const logging = require('../../../logging');
const request = require('request');
const querystring = require('querystring');
const async = require('async');

function Slackbot(configuration) {

  configuration = configuration || {};

  configuration.logger = logging('abbott-framework:botkit:slack');

  // Create a core botkit bot
  var slack_botkit = Botkit(configuration || {});

  // Set some default configurations unless they've already been set.

  // Should the RTM connections ingest received messages
  // Developers using the new Events API will set this to false
  // This allows an RTM connection to be kept alive (so bot appears online)
  // but receive messages only via events api
  if (slack_botkit.config.rtm_receive_messages === undefined) {
    slack_botkit.config.rtm_receive_messages = true;
  }

  var spawned_bots = [];

  // customize the bot definition, which will be used when new connections
  // spawn!
  slack_botkit.defineBot(require(__dirname + '/worker.js'));

  // Middleware to track spawned bots and connect existing RTM bots to incoming webhooks
  slack_botkit.middleware.spawn.use(function (worker, next) {

    // lets first check and make sure we don't already have a bot
    // for this team! If we already have an RTM connection, copy it
    // into the new bot so it can be used for replies.

    var existing_bot = null;
    if (worker.config.id) {
      for (var b = 0; b < spawned_bots.length; b++) {
        if (spawned_bots[b].config.id) {
          if (spawned_bots[b].config.id == worker.config.id) {
            // WAIT! We already have a bot spawned here.
            // so instead of using the new one, use the exist one.
            existing_bot = spawned_bots[b];
          }
        }
      }
    }

    if (!existing_bot && worker.config.id) {
      spawned_bots.push(worker);
    } else if (existing_bot) {
      if (existing_bot.rtm) {
        worker.rtm = existing_bot.rtm;
      }
    }
    next();

  });

  // set up configuration for oauth
  // slack_app_config should contain
  // { clientId, clientSecret, scopes}
  // https://api.slack.com/docs/oauth-scopes
  slack_botkit.configureSlackApp = function (slack_app_config, cb) {

    slack_botkit.logger.info('** Configuring app as a Slack App!');
    if (!slack_app_config || !slack_app_config.clientId ||
      !slack_app_config.clientSecret || !slack_app_config.scopes) {
      throw new Error('Missing oauth config details');
    } else {
      slack_botkit.config.clientId = slack_app_config.clientId;
      slack_botkit.config.clientSecret = slack_app_config.clientSecret;
      if (slack_app_config.redirectUri) slack_botkit.config.redirectUri = slack_app_config.redirectUri;
      if (typeof (slack_app_config.scopes) == 'string') {
        slack_botkit.config.scopes = slack_app_config.scopes.split(/\,/);
      } else {
        slack_botkit.config.scopes = slack_app_config.scopes;
      }
      if (cb) cb(null);
    }

    return slack_botkit;

  };

  // set up a web route that is a landing page
  slack_botkit.createHomepageEndpoint = function (webserver) {

    slack_botkit.logger.info('** Serving app landing page at : http://' +
      slack_botkit.config.hostname + ':' + slack_botkit.config.port + '/');

    // FIX THIS!!!
    // this is obvs not right.
    webserver.get('/slack', function (req, res) {

      res.send('Howdy!');

    });

    return slack_botkit;

  };

  // adds the webhook authentication middleware module to the webserver
  function secureWebhookEndpoints() {
    var authenticationMiddleware = require(__dirname + '/middleware/slack_authentication.js');
    // convert a variable argument list to an array, drop the webserver argument
    var tokens = Array.prototype.slice.call(arguments);
    var webserver = tokens.shift();

    slack_botkit.logger.info(
      '** Requiring token authentication for webhook endpoints for Slash commands ' +
      'and outgoing webhooks; configured ' + tokens.length + ' token(s)'
    );

    webserver.use(authenticationMiddleware(tokens));
  }

  // set up a web route for receiving outgoing webhooks and/or slash commands
  slack_botkit.createWebhookEndpoints = function (webserver, authenticationTokens) {

    if (authenticationTokens !== undefined && arguments.length > 1 && arguments[1].length) {
      secureWebhookEndpoints.apply(null, arguments);
    }

    slack_botkit.logger.info(
      '** Serving webhook endpoints for Slash commands and outgoing ' +
      'webhooks at: http://' + slack_botkit.config.hostname + ':' + slack_botkit.config.port + '/slack/receive');
    webserver.post('/slack/receive', function (req, res) {

      // respond to Slack that the webhook has been received.
      res.status(200);

      // Now, pass the webhook into be processed
      slack_botkit.handleWebhookPayload(req, res);

    });

    return slack_botkit;
  };

  slack_botkit.findAppropriateTeam = function (payload, cb) {

    var found_team = null;

    var team_id = payload.team_id || (payload.team && payload.team.id) || null;
    slack_botkit.findTeamById(team_id, function (err, team) {
      if (team) {
        cb(err, team);
      } else {
        if (payload.authed_teams) {
          async.eachSeries(payload.authed_teams, function (team_id, next) {
            slack_botkit.findTeamById(team_id, function (err, team) {
              if (team) {
                found_team = team;
                next();
              } else {
                next(err);
              }
            });
          }, function (err) {
            if (!found_team) {
              cb(err);
            } else {
              cb(null, found_team);
            }
          });
        } else {
          cb(new Error(`could not find team ${team_id}`));
        }
      }
    });
  };

  slack_botkit.handleWebhookPayload = function (req, res) {

    // is this an events api url handshake?
    if (req.body.type === 'url_verification') {
      slack_botkit.logger.debug('Received url handshake');
      res.json({ challenge: req.body.challenge });
      return;
    }

    var payload = req.body;
    if (payload.payload) {
      payload = JSON.parse(payload.payload);
    }

    slack_botkit.findAppropriateTeam(payload, function (err, team) {
      if (err) {
        slack_botkit.logger.error('Could not load team while processing webhook: ', err);
        return;
      } else if (!team) {
        // if this is NOT a slack app, it is ok to spawn a generic bot
        // this is only likely to happen with custom slash commands
        if (!slack_botkit.config.clientId) {
          bot = slack_botkit.spawn({});
        } else {
          return;
        }
      } else {
        // spawn a bot
        bot = slack_botkit.spawn(team);

        // Identify the bot from either team storage or identifyBot()
        bot.team_info = team;

        // The bot identity is only used in handleEventsAPI during this flow
        // Recent changes in Slack will break other integrations as they no longer
        // require a bot and therefore Slack won't send the bot information.
        if (payload.type === 'event_callback') {

          if (!team.bot) {
            slack_botkit.logger.error('No bot identity found.');
            return;
          }

        }

        bot.identity = {
          id: team.bot.user_id,
          name: team.bot.name
        };

      }

      // include the response channel so that they can be used in
      // responding to slash commands and outgoing webhooks
      bot.res = res;

      // pass the payload into Botkit's message handling pipeline!
      slack_botkit.ingest(bot, payload, res);

    });
  };

  // Send a 200 response back to Slack to acknowledge the message.
  slack_botkit.middleware.ingest.use(function sendResponse(bot, message, res, next) {

    if (res && res.statusCode) {
      // this is an http response
      // always send a 200
      res.status(200);

      // conditionally send a response back to Slack to acknowledge the message.
      // we do NOT want to respond to incoming webhooks or slash commands
      // as the response can be used by developers to actually deliver a reply
      if (!message.command && !message.trigger_word & !message.submission) {
        res.send('');
      }
    }
    next();

  });

  /* do delivery confirmations for RTM messages */
  slack_botkit.middleware.ingest.use(function requireDelivery(bot, message, res, next) {
    if (message.ok != undefined) {
      // this is a confirmation of something we sent.
      if (slack_botkit.config.require_delivery) {
        // loop through all active conversations this bot is having
        // and mark messages in conversations as delivered = true
        for (var t = 0; t < slack_botkit.tasks.length; t++) {
          var task = slack_botkit.tasks[t];
          if (task.isActive()) {
            for (var c = 0; c < task.convos.length; c++) {
              var convo = task.convos[c];
              for (var s = 0; s < convo.sent.length; s++) {
                var sent = convo.sent[s];
                if (sent.api_response && sent.api_response.id == message.reply_to) {
                  sent.delivered = true;
                  sent.api_response.ts = message.ts;
                }
              }
            }
          }
        }
      }
      return false;
    }

    next();
  });

  slack_botkit.middleware.categorize.use(function (bot, message, next) {

    var mentionSyntax = '<@' + bot.identity.id + '(\\|' + bot.identity.name.replace('.', '\\.') + ')?>';
    var mention = new RegExp(mentionSyntax, 'i');
    var direct_mention = new RegExp('^' + mentionSyntax, 'i');

    if ('message' == message.type) {

      if (message.text) {
        message.text = message.text.trim();
      }

      // set up a couple of special cases based on subtype
      if (message.subtype && message.subtype == 'channel_join') {
        // someone joined. maybe do something?
        if (message.user == bot.identity.id) {
          message.type = 'bot_channel_join';
        } else {
          message.type = 'user_channel_join';
        }
      } else if (message.subtype && message.subtype == 'group_join') {
        // someone joined. maybe do something?
        if (message.user == bot.identity.id) {
          message.type = 'bot_group_join';
        } else {
          message.type = 'user_group_join';
        }
      } else if (message.subtype) {
        message.type = message.subtype;
      } else if (message.channel.match(/^D/)) {
        // this is a direct message
        message.type = 'direct_message';

        if (message.user == bot.identity.id && message.bot_id) {
          message.type = 'self_message';
        }
        if (!message.text) {
          // message without text is probably an edit
          return false;
        }

        // remove direct mention so the handler doesn't have to deal with it
        message.text = message.text.replace(direct_mention, '')
          .replace(/^\s+/, '').replace(/^\:\s+/, '').replace(/^\s+/, '');

      } else {
        if (!message.text) {
          // message without text is probably an edit
          return false;
        }

        if (message.text.match(direct_mention)) {
          // this is a direct mention
          message.text = message.text.replace(direct_mention, '')
            .replace(/^\s+/, '').replace(/^\:\s+/, '').replace(/^\s+/, '');
          message.type = 'direct_mention';

        } else if (message.text.match(mention)) {
          message.type = 'mention';
        } else {
          message.type = 'ambient';
        }

        if (message.user == bot.identity.id && message.bot_id) {
          message.type = 'self_message';
        }

      }
    }

    // move on to the next stage of the pipeline
    next();

  });

  /* Handler functions for the various ways Slack might send a message to
   * Botkit via webhooks.  These include interactive messages (button clicks),
   * events api (messages sent over web hook), slash commands, and outgoing webhooks
   * (patterns matched in slack that result in a webhook)
   */
  slack_botkit.middleware.normalize.use(function handleInteractiveMessage(bot, message, next) {

    if (message.callback_id) {

      // let's normalize some of these fields to match the rtm message format
      message.user = message.user.id;
      message.channel = message.channel.id;

      // put the action value in the text field
      // this allows button clicks to respond to asks
      if (message.type == 'interactive_message') {
        message.text = message.actions[0].value;

        // handle menus too!
        // take the first selected item
        // TODO: When Slack supports multi-select menus, this will need an update!
        if (message.actions[0].selected_options) {
          message.text = message.actions[0].selected_options[0].value;
        }

        message.type = 'interactive_message_callback';

      } else if (message.type == 'dialog_submission') {
        // message.submissions is where the stuff is
      }
    }

    next();

  });

  slack_botkit.middleware.normalize.use(function handleEventsAPI(bot, message, next) {

    if (message.type == 'event_callback') {

      // var message = {};
      for (var key in message.event) {
        message[key] = message.event[key];
      }

      // let's normalize some of these fields to match the rtm message format
      message.team = message.team_id;
      message.events_api = true;
      message.authed_users = message.authed_users;

      if (bot.identity == undefined || bot.identity.id == null) {
        slack_botkit.logger.error('Could not identify bot');
        return;
      } else if (bot.identity.id === message.user && message.subtype !== 'channel_join' && message.subtype !== 'group_join') {
        // console.error('Got event from this bot user, ignoring it');
        return;
      }

    }
    next();
  });

  slack_botkit.middleware.normalize.use(function handleSlashCommand(bot, message, next) {

    if (message.command) {

      message.user = message.user_id;
      message.channel = message.channel_id;

      message.type = 'slash_command';
    }

    next();

  });

  slack_botkit.middleware.normalize.use(function handleOutgoingWebhook(bot, message, next) {

    if (message.trigger_word) {

      message.user = message.user_id;
      message.channel = message.channel_id;

      message.type = 'outgoing_webhook';
    }

    next();
  });

  slack_botkit.middleware.format.use(function formatForSlack(bot, message, platform_message, next) {

    platform_message.type = message.type || 'message';
    platform_message.channel = message.channel;
    platform_message.text = message.text || null;
    platform_message.username = message.username || null;
    platform_message.thread_ts = message.thread_ts || null;
    platform_message.reply_broadcast = message.reply_broadcast || null;
    platform_message.parse = message.parse || null;
    platform_message.link_names = message.link_names || null;
    platform_message.attachments = message.attachments ?
      JSON.stringify(message.attachments) : null;
    platform_message.unfurl_links = typeof message.unfurl_links !== 'undefined' ? message.unfurl_links : null;
    platform_message.unfurl_media = typeof message.unfurl_media !== 'undefined' ? message.unfurl_media : null;
    platform_message.icon_url = message.icon_url || null;
    platform_message.icon_emoji = message.icon_emoji || null;

    // should this message be sent as an ephemeral message
    if (message.ephemeral) {
      platform_message.ephemeral = true;
      platform_message.user = message.user;
    }

    if (platform_message.icon_url || platform_message.icon_emoji || platform_message.username) {
      platform_message.as_user = false;
    } else {
      platform_message.as_user = platform_message.as_user || true;
    }

    next();

  });

  /* End of webhook handler functions
   * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

  slack_botkit.saveTeam = function (team, cb) {
    slack_botkit.storage.teams.save(team, cb);
  };

  // look up a team's memory and configuration and return it, or
  // return an error!
  slack_botkit.findTeamById = function (id, cb) {
    slack_botkit.storage.teams.get(id, cb);
  };

  // get a team url to redirect the user through oauth process
  slack_botkit.getAuthorizeURL = function (team_id, redirect_params) {

    var scopes = slack_botkit.config.scopes;
    var api_root = slack_botkit.config.api_root ? slack_botkit.config.api_root : 'https://slack.com';

    var url = api_root + '/oauth/authorize' + '?client_id=' +
      slack_botkit.config.clientId + '&scope=' + scopes.join(',') + '&state=botkit';

    if (team_id)
      url += '&team=' + team_id;

    if (slack_botkit.config.redirectUri) {
      var redirect_query = '';
      var redirect_uri = slack_botkit.config.redirectUri;
      if (redirect_params) {
        redirect_query += encodeURIComponent(querystring.stringify(redirect_params));
        redirect_uri = redirect_uri + '?' + redirect_query;
      }
      url += '&redirect_uri=' + redirect_uri;
    }

    return url;

  };

  // set up a web route for redirecting users
  // and collecting authentication details
  // https://api.slack.com/docs/oauth
  // https://api.slack.com/docs/oauth-scopes
  slack_botkit.createOauthEndpoints = function (webserver, callback) {

    slack_botkit.logger.log('** Serving login URL: http://' +
      slack_botkit.config.hostname + ':' + slack_botkit.config.port + '/slack/login');

    if (!slack_botkit.config.clientId) {
      throw new Error(
        'Cannot create oauth endpoints without calling configureSlackApp() with a clientId first');
    }
    if (!slack_botkit.config.clientSecret) {
      throw new Error(
        'Cannot create oauth endpoints without calling configureSlackApp() with a clientSecret first');
    }
    if (!slack_botkit.config.scopes) {
      throw new Error(
        'Cannot create oauth endpoints without calling configureSlackApp() with a list of scopes first');
    }

    var call_api = function (command, options, cb) {

      var api_root = slack_botkit.config.api_root ? slack_botkit.config.api_root : 'https://slack.com';

      slack_botkit.logger.info('** API CALL: ' + api_root + '/api/' + command);
      request.post(api_root + '/api/' + command, function (error, response, body) {
        slack_botkit.logger.debug('Got response', error, body);
        if (!error && response.statusCode == 200) {
          var json = JSON.parse(body);
          if (json.ok) {
            if (cb) cb(null, json);
          } else {
            if (cb) cb(json.error, json);
          }
        } else {
          if (cb) cb(error);
        }
      }).form(options);
    };

    var oauth_access = function (options, cb) {
      call_api('oauth.access', options, cb);
    };

    var auth_test = function (options, cb) {
      call_api('auth.test', options, cb);
    };

    webserver.get('/slack/login', function (req, res) {
      res.redirect(slack_botkit.getAuthorizeURL());
    });

    slack_botkit.logger.info('** Serving oauth return endpoint: http://' +
      slack_botkit.config.hostname + ':' + slack_botkit.config.port + '/slack/oauth');

    webserver.get('/slack/oauth', function (req, res) {

      var code = req.query.code;
      var state = req.query.state;

      var opts = {
        client_id: slack_botkit.config.clientId,
        client_secret: slack_botkit.config.clientSecret,
        code: code
      };

      var redirect_params = {};
      if (slack_botkit.config.redirectUri) {
        Object.assign(redirect_params, req.query);
        delete redirect_params.code;
        delete redirect_params.state;

        var redirect_query = querystring.stringify(redirect_params);
        var redirect_uri = slack_botkit.config.redirectUri;
        if (redirect_query) {
          redirect_uri = redirect_uri + '?' + redirect_query;
        }
        opts.redirect_uri = redirect_uri;
      }

      oauth_access(opts, function (err, auth) {

        if (err) {
          if (callback) {
            callback(err, req, res);
          } else {
            res.status(500).send(err);
          }
          slack_botkit.trigger('oauth_error', [err]);
        } else {

          // auth contains at least:
          // { access_token, scope, team_name}
          // May also contain:
          // { team_id } (not in incoming_webhook scope)
          // info about incoming webhooks:
          // { incoming_webhook: { url, channel, configuration_url} }
          // might also include slash commands:
          // { commands: ??}

          // what scopes did we get approved for?
          var scopes = auth.scope.split(/\,/);

          // temporarily use the token we got from the oauth
          // we need to call auth.test to make sure the token is valid
          // but also so that we reliably have the team_id field!
          //slack_botkit.config.token = auth.access_token;
          auth_test({ token: auth.access_token }, function (err, identity) {

            if (err) {
              if (callback) {
                callback(err, req, res);
              } else {
                res.status(500).send(err);
              }

              slack_botkit.trigger('oauth_error', [err]);

            } else {
              req.identity = identity;

              // we need to deal with any team-level provisioning info
              // like incoming webhooks and bot users
              // and also with the personal access token from the user

              slack_botkit.findTeamById(identity.team_id, function (err, team) {

                var isnew = false;
                if (!team) {
                  isnew = true;
                  team = {
                    id: identity.team_id,
                    createdBy: identity.user_id,
                    url: identity.url,
                    name: identity.team,
                  };
                }

                team.state = state;

                var bot = slack_botkit.spawn(team);

                if (auth.incoming_webhook) {
                  auth.incoming_webhook.token = auth.access_token;
                  auth.incoming_webhook.createdBy = identity.user_id;
                  team.incoming_webhook = auth.incoming_webhook;
                  bot.configureIncomingWebhook(team.incoming_webhook);
                  slack_botkit.trigger('create_incoming_webhook', [bot, team.incoming_webhook]);
                }

                if (auth.bot) {

                  team.bot = {
                    token: auth.bot.bot_access_token,
                    user_id: auth.bot.bot_user_id,
                    createdBy: identity.user_id,
                    app_token: auth.access_token,
                  };
                  bot.configureRTM(team.bot);
                  slack_botkit.trigger('create_bot', [bot, team.bot]);
                }

                slack_botkit.saveTeam(team, function (err, id) {
                  if (err) {
                    slack_botkit.logger.error('An error occurred while saving a team: ', err);
                    if (callback) {
                      callback(err, req, res);
                    } else {
                      res.status(500).send(err);
                    }
                    slack_botkit.trigger('error', [err]);
                  } else {
                    if (isnew) {
                      slack_botkit.trigger('create_team', [bot, team]);
                    } else {
                      slack_botkit.trigger('update_team', [bot, team]);
                    }

                    if (team.bot) {
                      // call auth test on the bot token
                      // to capture its name
                      auth_test({
                        token: team.bot.token
                      }, function (err, auth_data) {
                        team.bot.name = auth_data.user;
                        slack_botkit.saveTeam(team, function (err, id) {
                          if (err) {
                            slack_botkit.logger.error('An error occurred while saving a team: ', err);
                          }
                        });

                      });
                    }

                    slack_botkit.storage.users.get(identity.user_id, function (err, user) {
                      isnew = false;
                      if (!user) {
                        isnew = true;
                        user = {
                          id: identity.user_id,
                          team_id: identity.team_id,
                          user: identity.user,
                        };
                      }

                      // Always update these because the token could become invalid
                      // and scopes could change.
                      user.access_token = auth.access_token;
                      user.scopes = scopes;

                      slack_botkit.storage.users.save(user, function (err, id) {

                        if (err) {
                          slack_botkit.logger.error(
                            'An error occurred while saving a user: ', err);
                          if (callback) {
                            callback(err, req, res);
                          } else {
                            res.status(500).send(err);
                          }
                          slack_botkit.trigger('error', [err]);
                        } else {
                          if (isnew) {
                            slack_botkit.trigger(
                              'create_user',
                              [bot, user, redirect_params]
                            );
                          } else {
                            slack_botkit.trigger(
                              'update_user',
                              [bot, user, redirect_params]
                            );
                          }
                          if (callback) {
                            callback(null, req, res);
                          } else {
                            res.redirect('/');
                          }
                        }
                      });
                    });
                  }
                });
              });
            }
          });
        }
      });
    });

    return slack_botkit;

  };

  return slack_botkit;
}

module.exports = Slackbot;


var request = require('request');

/**
 * Does nothing. Takes no params, returns nothing. It's a no-op!
 */
function noop() {}

/**
 * Returns an interface to the Slack API in the context of the given bot
 *
 * @param {Object} bot The botkit bot object
 * @param {Object} config A config containing auth credentials.
 * @returns {Object} A callback-based Slack API interface.
 */
module.exports = function(bot, config) {

    var api_root = bot.config.api_root ? bot.config.api_root : 'https://slack.com';

    var slack_api = {
        api_url: api_root + '/api/'
    };

    // Slack API methods: https://api.slack.com/methods
    var slackApiMethods = [
        'auth.test',
        'oauth.access',
        'channels.archive',
        'channels.create',
        'channels.history',
        'channels.info',
        'channels.invite',
        'channels.join',
        'channels.kick',
        'channels.leave',
        'channels.list',
        'channels.mark',
        'channels.rename',
        'channels.replies',
        'channels.setPurpose',
        'channels.setTopic',
        'channels.unarchive',
        'chat.delete',
        'chat.postMessage',
        'chat.postEphemeral',
        'chat.update',
        'chat.unfurl',
        'conversations.archive',
        'conversations.close',
        'conversations.create',
        'conversations.history',
        'conversations.info',
        'conversations.invite',
        'conversations.join',
        'conversations.kick',
        'conversations.leave',
        'conversations.list',
        'conversations.members',
        'conversations.open',
        'conversations.rename',
        'conversations.replies',
        'conversations.setPurpose',
        'conversations.setTopic',
        'conversations.unarchive',
        'dialog.open',
        'dnd.endDnd',
        'dnd.endSnooze',
        'dnd.info',
        'dnd.setSnooze',
        'dnd.teamInfo',
        'emoji.list',
        'files.delete',
        'files.info',
        'files.list',
        'files.upload',
        'files.sharedPublicURL',
        'groups.archive',
        'groups.close',
        'groups.create',
        'groups.createChild',
        'groups.history',
        'groups.info',
        'groups.invite',
        'groups.kick',
        'groups.leave',
        'groups.list',
        'groups.mark',
        'groups.open',
        'groups.rename',
        'groups.replies',
        'groups.setPurpose',
        'groups.setTopic',
        'groups.unarchive',
        'im.close',
        'im.history',
        'im.list',
        'im.mark',
        'im.open',
        'im.replies',
        'mpim.close',
        'mpim.history',
        'mpim.list',
        'mpim.mark',
        'mpim.open',
        'mpim.replies',
        'pins.add',
        'pins.list',
        'pins.remove',
        'reactions.add',
        'reactions.get',
        'reactions.list',
        'reactions.remove',
        'reminders.add',
        'reminders.complete',
        'reminders.delete',
        'reminders.info',
        'reminders.list',
        'rtm.start',
        'rtm.connect',
        'search.all',
        'search.files',
        'search.messages',
        'stars.add',
        'stars.list',
        'stars.remove',
        'team.accessLogs',
        'team.info',
        'team.billableInfo',
        'team.integrationLogs',
        'team.profile.get',
        'usergroups.create',
        'usergroups.disable',
        'usergroups.enable',
        'usergroups.list',
        'usergroups.update',
        'usergroups.users.list',
        'usergroups.users.update',
        'users.getPresence',
        'users.info',
        'users.identity',
        'users.list',
        'users.setActive',
        'users.setPresence',
        'users.deletePhoto',
        'users.identity',
        'users.setPhoto',
        'users.profile.get',
        'users.profile.set'
    ];

    /**
     * Calls Slack using a Token for authentication/authorization
     *
     * @param {string} command The Slack API command to call
     * @param {Object} data The data to pass to the API call
     * @param {function} cb A NodeJS-style callback
     */
    slack_api.callAPI = function(command, data, cb, multipart) {
        data.token = data.token || config.token;
        bot.logger.debug(command, data);
        postForm(slack_api.api_url + command, data, cb, multipart);
    };

    /**
     * Calls Slack using OAuth for authentication/authorization
     *
     * @param {string} command The Slack API command to call
     * @param {Object} data The data to pass to the API call
     * @param {function} cb A NodeJS-style callback
     */
    slack_api.callAPIWithoutToken = function(command, data, cb) {
        data.client_id = data.client_id || bot.config.clientId;
        data.client_secret = data.client_secret || bot.config.clientSecret;
        data.redirect_uri = data.redirect_uri || bot.config.redirectUri;
        // DON'T log options: that could expose the client secret!
        postForm(slack_api.api_url + command, data, cb);
    };

    // generate all API methods
    slackApiMethods.forEach(function(slackMethod) {
        // most slack api methods are only two parts, of the form group.method, e.g. auth.test
        // some have three parts: group.subgroup.method, e.g, users.profile.get
        // this method loops through all groups in a method, ensures they exist,
        // then adds the method to the terminal group

        var groups = slackMethod.split('.');
        var method = groups.pop();
        var currentGroup = slack_api;

        groups.forEach(function(nextGroupName) {
            currentGroup[nextGroupName] = currentGroup[nextGroupName] || {};
            currentGroup = currentGroup[nextGroupName];
        });

        currentGroup[method] = function(options, cb) {
            slack_api.callAPI(slackMethod, options, cb);
        };

    });

    // overwrite default behavior
    slack_api.chat.postMessage = function(options, cb) {
        sanitizeOptions(options);
        slack_api.callAPI('chat.postMessage', options, cb);
    };

    slack_api.chat.postEphemeral = function(options, cb) {
        sanitizeOptions(options);
        slack_api.callAPI('chat.postEphemeral', options, cb);
    };

    slack_api.chat.update = function(options, cb) {
        sanitizeOptions(options);
        slack_api.callAPI('chat.update', options, cb);
    };

    // specify that files get uploaded using multipart
    slack_api.files.upload = function(options, cb) {
        slack_api.callAPI('files.upload', options, cb, !!options.file);
    };

    function sanitizeOptions(options) {
        if (options.attachments && typeof(options.attachments) != 'string') {
            try {
                options.attachments = JSON.stringify(options.attachments);
            } catch (err) {
                delete options.attachments;
                bot.logger.error('Could not parse attachments', err);
            }
        }
    }

    return slack_api;

    /**
     * Makes a POST request as a form to the given url with the options as data
     *
     * @param {string} url The URL to POST to
     * @param {Object} formData The data to POST as a form
     * @param {function=} cb An optional NodeJS style callback when the POST completes or errors out.
     */
    function postForm(url, formData, cb, multipart) {
        cb = cb || noop;

        bot.logger.info('** API CALL: ' + url);
        var params = {
            url: url,
            headers: {
                'User-Agent': bot.userAgent(),
            }
        };

        if (multipart === true) {
            params.formData = formData;
        } else {
            params.form = formData;
        }

        request.post(params, function(error, response, body) {
            bot.logger.debug('Got response', error, body);

            if (error) {
                return cb(error);
            }

            if (response.statusCode == 200) {
                var json;
                try {
                    json = JSON.parse(body);
                } catch (parseError) {
                    return cb(parseError);
                }

                return cb((json.ok ? null : json.error), json);
            } else if (response.statusCode == 429) {
                return cb(new Error('Rate limit exceeded'));
            } else {
                return cb(new Error('Invalid response'));
            }
        });
    }
};