<commit_before>const Gdax = require('gdax');
const _ = require('lodash');
const moment = require('moment');

const errors = require('../exchangeErrors');
const retry = require('../exchangeUtils').retry;

const BATCH_SIZE = 100;
const QUERY_DELAY = 350;

const Trader = function(config) {
  this.post_only = true;
  this.use_sandbox = false;
  this.name = 'GDAX';
  this.scanback = false;
  this.scanbackTid = 0;
  this.scanbackResults = [];
  this.asset = config.asset;
  this.currency = config.currency;

  this.api_url = 'https://api.gdax.com';
  this.api_sandbox_url = 'https://api-public.sandbox.gdax.com';

  if (_.isObject(config)) {
    this.key = config.key;
    this.secret = config.secret;
    this.passphrase = config.passphrase;

    this.pair = [config.asset, config.currency].join('-').toUpperCase();
    this.post_only =
      typeof config.post_only !== 'undefined' ? config.post_only : true;

    if (config.sandbox) {
      this.use_sandbox = config.sandbox;
    }

  }

  this.gdax_public = new Gdax.PublicClient(
    this.use_sandbox ? this.api_sandbox_url : undefined
  );
  this.gdax = new Gdax.AuthenticatedClient(
    this.key,
    this.secret,
    this.passphrase,
    this.use_sandbox ? this.api_sandbox_url : undefined
  );
};

const recoverableErrors = [
  'SOCKETTIMEDOUT',
  'TIMEDOUT',
  'CONNRESET',
  'CONNREFUSED',
  'NOTFOUND',
  'Rate limit exceeded',
  'Response code 5',
  'GDAX is currently under maintenance.',
  'HTTP 408 Error',
  'HTTP 504 Error',
  'socket hang up',
  'EHOSTUNREACH'
];

const includes = (str, list) => {
  if(!_.isString(str))
    return false;

  return _.some(list, item => str.includes(item));
}

Trader.prototype.processResponse = function(method, next) {
  return (error, response, body) => {
    if(!error && body && !_.isEmpty(body.message)) {
      error = new Error(body.message);
    }

    if(
      response &&
      response.statusCode < 200 &&
      response.statusCode >= 300
    ) {
      error = new Error(`Response code ${response.statusCode}`);
    }

    if(error) {
      if(includes(error.message, recoverableErrors)) {
        error.notFatal = true;
      }

      if(
        ['buy', 'sell'].includes(method) &&
        error.message.includes('Insufficient funds')
      ) {
        error.retry = 10;
      }

      console.log(error.message);

      return next(error);
    }

    return next(undefined, body);
  }
}

Trader.prototype.getPortfolio = function(callback) {
  const result = (err, data) => {
    if (err) return callback(err);

    var portfolio = data.map(function(account) {
      return {
        name: account.currency.toUpperCase(),
        amount: parseFloat(account.available),
      };
    });
    callback(undefined, portfolio);
  };

  const fetch = cb =>
    this.gdax.getAccounts(this.processResponse('getPortfolio', cb));
  retry(null, fetch, result);
};

Trader.prototype.getTicker = function(callback) {
  const result = (err, data) => {
    if (err) return callback(err);
    callback(undefined, { bid: +data.bid, ask: +data.ask });
  };

  const fetch = cb =>
    this.gdax_public.getProductTicker(this.pair, this.processResponse('getTicker', cb));
  retry(null, fetch, result);
};

Trader.prototype.getFee = function(callback) {
  //https://www.gdax.com/fees
  // const fee = this.asset == 'BTC' ? 0.0025 : 0.003;
  const fee = 0;

  //There is no maker fee, not sure if we need taker fee here
  //If post only is enabled, gdax only does maker trades which are free
  callback(undefined, this.post_only ? 0 : fee);
};

Trader.prototype.roundPrice = function(price) {
  return this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2);
}

Trader.prototype.roundAmount = function(amount) {
  return this.getMaxDecimalsNumber(amount);
}

Trader.prototype.buy = function(amount, price, callback) {
  const buyParams = {
    price: this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2),
    size: this.getMaxDecimalsNumber(amount),
    product_id: this.pair,
    post_only: this.post_only,
  };

  const result = (err, data) => {
    if (err) {
      console.log({buyParams}, err.message);
      return callback(err);
    }
    callback(undefined, data.id);
  };

  const fetch = cb =>
    this.gdax.buy(buyParams, this.processResponse('buy', cb));
  retry(null, fetch, result);
};

Trader.prototype.sell = function(amount, price, callback) {
  const sellParams = {
    price: this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2),
    size: this.getMaxDecimalsNumber(amount),
    product_id: this.pair,
    post_only: this.post_only,
  };

  const result = (err, data) => {
    if (err) {
      console.log({sellParams}, err.message);
      return callback(err);
    }

    if(data.message && data.message.includes('Insufficient funds')) {
      err = new Error(data.message);
      err.retryOnce = true;
      return callback(err);
    }

    callback(undefined, data.id);
  };

  const fetch = cb =>
    this.gdax.sell(sellParams, this.processResponse('sell', cb));
  retry(null, fetch, result);
};

Trader.prototype.checkOrder = function(order, callback) {
  const result = (err, data) => {
    if (err) return callback(err);

    // @link:
    // https://stackoverflow.com/questions/48132078/available-gdax-order-statuses-and-meanings
    var status = data.status;
    if(status == 'pending') {
      // technically not open yet, but will be soon
      return callback(undefined, { executed: false, open: true, filledAmount: 0 });
    } if (status === 'done' || status === 'settled') {
      return callback(undefined, { executed: true, open: false });
    } else if (status === 'rejected') {
      return callback(undefined, { executed: false, open: false });
    } else if(status === 'open' || status === 'active') {
      return callback(undefined, { executed: false, open: true, filledAmount: parseFloat(data.filled_size) });
    }

    callback(new Error('Unknown status ' + status));
  };

  const fetch = cb =>
    this.gdax.getOrder(order, this.processResponse('checkOrder', cb));
  retry(null, fetch, result);
};

Trader.prototype.getOrder = function(order, callback) {
  const result = (err, data) => {
    if (err) return callback(err);

    const price = parseFloat(data.price);
    const amount = parseFloat(data.filled_size);
    const date = moment(data.done_at);
    const fees = {
      // you always pay fee in the base currency on gdax
      [this.currency]: +data.fill_fees
    }
    const feePercent = +data.fill_fees / price / amount * 100;

    callback(undefined, { price, amount, date, fees, feePercent });
  };

  const fetch = cb =>
    this.gdax.getOrder(order, this.processResponse('getOrder', cb));
  retry(null, fetch, result);
};

Trader.prototype.cancelOrder = function(order, callback) {
  // callback for cancelOrder should be true if the order was already filled, otherwise false
  const result = (err, data) => {
    if(err) {
      return callback(null, true);  // need to catch the specific error but usually an error on cancel means it was filled
    }

    return callback(null, false);
  };

  const fetch = cb =>
    this.gdax.cancelOrder(order, this.processResponse('cancelOrder', cb));
  retry(null, fetch, result);
};

Trader.prototype.getTrades = function(since, callback, descending) {
  var lastScan = 0;

  const handle = function(err, data) {
    if (err) return callback(err);

    var result = _.map(data, function(trade) {
      return {
        tid: trade.trade_id,
        amount: parseFloat(trade.size),
        date: moment.utc(trade.time).format('X'),
        price: parseFloat(trade.price),
      };
    });

    if (this.scanback) {
      var last = _.last(data);
      var first = _.first(data);

      // Try to find trade id matching the since date
      if (!this.scanbackTid) {
        // either scan for new ones or we found it.
        if (moment.utc(last.time) < moment.utc(since)) {
          this.scanbackTid = last.trade_id;
        } else {
          console.log('Scanning backwards...' + last.time);
          setTimeout(() => {
            let handler = cb =>
              this.gdax_public.getProductTrades(
                this.pair,
                {
                  after: last.trade_id - BATCH_SIZE * lastScan,
                  limit: BATCH_SIZE,
                },
                this.processResponse('getTrades', cb)
              );
            retry(
              retryForever,
              _.bind(handler, this),
              _.bind(process, this)
            );
          }, QUERY_DELAY);
          lastScan++;
          if (lastScan > 100) {
            lastScan = 10;
          }
        }
      }

      if (this.scanbackTid) {
        // if scanbackTid is set we need to move forward again
        console.log(
          'Backwards: ' +
            last.time +
            ' (' +
            last.trade_id +
            ') to ' +
            first.time +
            ' (' +
            first.trade_id +
            ')'
        );

        this.scanbackResults = this.scanbackResults.concat(result.reverse());

        if (this.scanbackTid != first.trade_id) {
          this.scanbackTid = first.trade_id;
          setTimeout(() => {
            let handler = cb =>
              this.gdax_public.getProductTrades(
                this.pair,
                { after: this.scanbackTid + BATCH_SIZE + 1, limit: BATCH_SIZE },
                this.processResponse('getTrades', cb)
              );
            retry(
              retryForever,
              _.bind(handler, this),
              _.bind(process, this)
            );
          }, QUERY_DELAY);
        } else {
          this.scanback = false;
          this.scanbackTid = 0;

          console.log('Scan finished: data found:' + this.scanbackResults.length);
          callback(null, this.scanbackResults);

          this.scanbackResults = [];
        }
      }
    } else {
      callback(null, result.reverse());
    }
  };

  if (since || this.scanback) {
    this.scanback = true;
    if (this.scanbackTid) {
      let handler = cb =>
        this.gdax_public.getProductTrades(
          this.pair,
          { after: this.scanbackTid + BATCH_SIZE + 1, limit: BATCH_SIZE },
          this.processResponse('getTrades', cb)
        );
      retry(
        retryForever,
        _.bind(handler, this),
        _.bind(process, this)
      );
    } else {
      console.log('Scanning back in the history needed...', since);
    }
  }

  const fetch = cb =>
    this.gdax_public.getProductTrades(
      this.pair,
      { limit: BATCH_SIZE },
      this.processResponse('getTrades', cb)
    );
  retry(null, fetch, handle);
};

Trader.prototype.getMaxDecimalsNumber = function(number, decimalLimit = 8) {
  var decimalNumber = parseFloat(number);

  // The ^-?\d*\. strips off any sign, integer portion, and decimal point
  // leaving only the decimal fraction.
  // The 0+$ strips off any trailing zeroes.
  var decimalCount = (+decimalNumber).toString().replace(/^-?\d*\.?|0+$/g, '')
    .length;

  var decimalMultiplier = 1;
  for (i = 0; i < decimalLimit; i++) {
    decimalMultiplier *= 10;
  }

  return decimalCount <= decimalLimit
    ? decimalNumber.toString()
    : (
        Math.floor(decimalNumber * decimalMultiplier) / decimalMultiplier
      ).toFixed(decimalLimit);
};

Trader.getCapabilities = function() {
  return {
    name: 'GDAX',
    slug: 'gdax',
    currencies: ['USD', 'EUR', 'GBP', 'BTC'],
    assets: ['BTC', 'LTC', 'ETH', 'BCH'],
    markets: [
      { pair: ['USD', 'BTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
      { pair: ['USD', 'LTC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
      { pair: ['USD', 'ETH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
      { pair: ['USD', 'BCH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
      { pair: ['EUR', 'BTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
      { pair: ['EUR', 'ETH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
      { pair: ['EUR', 'LTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
      { pair: ['EUR', 'BCH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
      { pair: ['GBP', 'BTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
      { pair: ['BTC', 'LTC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
      { pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
      { pair: ['BTC', 'BCH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
    ],
    requires: ['key', 'secret', 'passphrase'],
    providesHistory: 'date',
    providesFullHistory: true,
    tid: 'tid',
    tradable: true,
    forceReorderDelay: false,
    gekkoBroker: 0.6
  };
};

module.exports = Trader;
<commit_msg>[gdax] catch 503 error as recoverable<commit_after>const Gdax = require('gdax');
const _ = require('lodash');
const moment = require('moment');

const errors = require('../exchangeErrors');
const retry = require('../exchangeUtils').retry;

const BATCH_SIZE = 100;
const QUERY_DELAY = 350;

const Trader = function(config) {
  this.post_only = true;
  this.use_sandbox = false;
  this.name = 'GDAX';
  this.scanback = false;
  this.scanbackTid = 0;
  this.scanbackResults = [];
  this.asset = config.asset;
  this.currency = config.currency;

  this.api_url = 'https://api.gdax.com';
  this.api_sandbox_url = 'https://api-public.sandbox.gdax.com';

  if (_.isObject(config)) {
    this.key = config.key;
    this.secret = config.secret;
    this.passphrase = config.passphrase;

    this.pair = [config.asset, config.currency].join('-').toUpperCase();
    this.post_only =
      typeof config.post_only !== 'undefined' ? config.post_only : true;

    if (config.sandbox) {
      this.use_sandbox = config.sandbox;
    }

  }

  this.gdax_public = new Gdax.PublicClient(
    this.use_sandbox ? this.api_sandbox_url : undefined
  );
  this.gdax = new Gdax.AuthenticatedClient(
    this.key,
    this.secret,
    this.passphrase,
    this.use_sandbox ? this.api_sandbox_url : undefined
  );
};

const recoverableErrors = [
  'SOCKETTIMEDOUT',
  'TIMEDOUT',
  'CONNRESET',
  'CONNREFUSED',
  'NOTFOUND',
  'Rate limit exceeded',
  'Response code 5',
  'GDAX is currently under maintenance.',
  'HTTP 408 Error',
  'HTTP 504 Error',
  'HTTP 503 Error',
  'socket hang up',
  'EHOSTUNREACH'
];

const includes = (str, list) => {
  if(!_.isString(str))
    return false;

  return _.some(list, item => str.includes(item));
}

Trader.prototype.processResponse = function(method, next) {
  return (error, response, body) => {
    if(!error && body && !_.isEmpty(body.message)) {
      error = new Error(body.message);
    }

    if(
      response &&
      response.statusCode < 200 &&
      response.statusCode >= 300
    ) {
      error = new Error(`Response code ${response.statusCode}`);
    }

    if(error) {
      if(includes(error.message, recoverableErrors)) {
        error.notFatal = true;
      }

      if(
        ['buy', 'sell'].includes(method) &&
        error.message.includes('Insufficient funds')
      ) {
        error.retry = 10;
      }

      console.log(error.message);

      return next(error);
    }

    return next(undefined, body);
  }
}

Trader.prototype.getPortfolio = function(callback) {
  const result = (err, data) => {
    if (err) return callback(err);

    var portfolio = data.map(function(account) {
      return {
        name: account.currency.toUpperCase(),
        amount: parseFloat(account.available),
      };
    });
    callback(undefined, portfolio);
  };

  const fetch = cb =>
    this.gdax.getAccounts(this.processResponse('getPortfolio', cb));
  retry(null, fetch, result);
};

Trader.prototype.getTicker = function(callback) {
  const result = (err, data) => {
    if (err) return callback(err);
    callback(undefined, { bid: +data.bid, ask: +data.ask });
  };

  const fetch = cb =>
    this.gdax_public.getProductTicker(this.pair, this.processResponse('getTicker', cb));
  retry(null, fetch, result);
};

Trader.prototype.getFee = function(callback) {
  //https://www.gdax.com/fees
  // const fee = this.asset == 'BTC' ? 0.0025 : 0.003;
  const fee = 0;

  //There is no maker fee, not sure if we need taker fee here
  //If post only is enabled, gdax only does maker trades which are free
  callback(undefined, this.post_only ? 0 : fee);
};

Trader.prototype.roundPrice = function(price) {
  return this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2);
}

Trader.prototype.roundAmount = function(amount) {
  return this.getMaxDecimalsNumber(amount);
}

Trader.prototype.buy = function(amount, price, callback) {
  const buyParams = {
    price: this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2),
    size: this.getMaxDecimalsNumber(amount),
    product_id: this.pair,
    post_only: this.post_only,
  };

  const result = (err, data) => {
    if (err) {
      console.log({buyParams}, err.message);
      return callback(err);
    }
    callback(undefined, data.id);
  };

  const fetch = cb =>
    this.gdax.buy(buyParams, this.processResponse('buy', cb));
  retry(null, fetch, result);
};

Trader.prototype.sell = function(amount, price, callback) {
  const sellParams = {
    price: this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2),
    size: this.getMaxDecimalsNumber(amount),
    product_id: this.pair,
    post_only: this.post_only,
  };

  const result = (err, data) => {
    if (err) {
      console.log({sellParams}, err.message);
      return callback(err);
    }

    if(data.message && data.message.includes('Insufficient funds')) {
      err = new Error(data.message);
      err.retryOnce = true;
      return callback(err);
    }

    callback(undefined, data.id);
  };

  const fetch = cb =>
    this.gdax.sell(sellParams, this.processResponse('sell', cb));
  retry(null, fetch, result);
};

Trader.prototype.checkOrder = function(order, callback) {
  const result = (err, data) => {
    if (err) return callback(err);

    // @link:
    // https://stackoverflow.com/questions/48132078/available-gdax-order-statuses-and-meanings
    var status = data.status;
    if(status == 'pending') {
      // technically not open yet, but will be soon
      return callback(undefined, { executed: false, open: true, filledAmount: 0 });
    } if (status === 'done' || status === 'settled') {
      return callback(undefined, { executed: true, open: false });
    } else if (status === 'rejected') {
      return callback(undefined, { executed: false, open: false });
    } else if(status === 'open' || status === 'active') {
      return callback(undefined, { executed: false, open: true, filledAmount: parseFloat(data.filled_size) });
    }

    callback(new Error('Unknown status ' + status));
  };

  const fetch = cb =>
    this.gdax.getOrder(order, this.processResponse('checkOrder', cb));
  retry(null, fetch, result);
};

Trader.prototype.getOrder = function(order, callback) {
  const result = (err, data) => {
    if (err) return callback(err);

    const price = parseFloat(data.price);
    const amount = parseFloat(data.filled_size);
    const date = moment(data.done_at);
    const fees = {
      // you always pay fee in the base currency on gdax
      [this.currency]: +data.fill_fees
    }
    const feePercent = +data.fill_fees / price / amount * 100;

    callback(undefined, { price, amount, date, fees, feePercent });
  };

  const fetch = cb =>
    this.gdax.getOrder(order, this.processResponse('getOrder', cb));
  retry(null, fetch, result);
};

Trader.prototype.cancelOrder = function(order, callback) {
  // callback for cancelOrder should be true if the order was already filled, otherwise false
  const result = (err, data) => {
    if(err) {
      return callback(null, true);  // need to catch the specific error but usually an error on cancel means it was filled
    }

    return callback(null, false);
  };

  const fetch = cb =>
    this.gdax.cancelOrder(order, this.processResponse('cancelOrder', cb));
  retry(null, fetch, result);
};

Trader.prototype.getTrades = function(since, callback, descending) {
  var lastScan = 0;

  const handle = function(err, data) {
    if (err) return callback(err);

    var result = _.map(data, function(trade) {
      return {
        tid: trade.trade_id,
        amount: parseFloat(trade.size),
        date: moment.utc(trade.time).format('X'),
        price: parseFloat(trade.price),
      };
    });

    if (this.scanback) {
      var last = _.last(data);
      var first = _.first(data);

      // Try to find trade id matching the since date
      if (!this.scanbackTid) {
        // either scan for new ones or we found it.
        if (moment.utc(last.time) < moment.utc(since)) {
          this.scanbackTid = last.trade_id;
        } else {
          console.log('Scanning backwards...' + last.time);
          setTimeout(() => {
            let handler = cb =>
              this.gdax_public.getProductTrades(
                this.pair,
                {
                  after: last.trade_id - BATCH_SIZE * lastScan,
                  limit: BATCH_SIZE,
                },
                this.processResponse('getTrades', cb)
              );
            retry(
              retryForever,
              _.bind(handler, this),
              _.bind(process, this)
            );
          }, QUERY_DELAY);
          lastScan++;
          if (lastScan > 100) {
            lastScan = 10;
          }
        }
      }

      if (this.scanbackTid) {
        // if scanbackTid is set we need to move forward again
        console.log(
          'Backwards: ' +
            last.time +
            ' (' +
            last.trade_id +
            ') to ' +
            first.time +
            ' (' +
            first.trade_id +
            ')'
        );

        this.scanbackResults = this.scanbackResults.concat(result.reverse());

        if (this.scanbackTid != first.trade_id) {
          this.scanbackTid = first.trade_id;
          setTimeout(() => {
            let handler = cb =>
              this.gdax_public.getProductTrades(
                this.pair,
                { after: this.scanbackTid + BATCH_SIZE + 1, limit: BATCH_SIZE },
                this.processResponse('getTrades', cb)
              );
            retry(
              retryForever,
              _.bind(handler, this),
              _.bind(process, this)
            );
          }, QUERY_DELAY);
        } else {
          this.scanback = false;
          this.scanbackTid = 0;

          console.log('Scan finished: data found:' + this.scanbackResults.length);
          callback(null, this.scanbackResults);

          this.scanbackResults = [];
        }
      }
    } else {
      callback(null, result.reverse());
    }
  };

  if (since || this.scanback) {
    this.scanback = true;
    if (this.scanbackTid) {
      let handler = cb =>
        this.gdax_public.getProductTrades(
          this.pair,
          { after: this.scanbackTid + BATCH_SIZE + 1, limit: BATCH_SIZE },
          this.processResponse('getTrades', cb)
        );
      retry(
        retryForever,
        _.bind(handler, this),
        _.bind(process, this)
      );
    } else {
      console.log('Scanning back in the history needed...', since);
    }
  }

  const fetch = cb =>
    this.gdax_public.getProductTrades(
      this.pair,
      { limit: BATCH_SIZE },
      this.processResponse('getTrades', cb)
    );
  retry(null, fetch, handle);
};

Trader.prototype.getMaxDecimalsNumber = function(number, decimalLimit = 8) {
  var decimalNumber = parseFloat(number);

  // The ^-?\d*\. strips off any sign, integer portion, and decimal point
  // leaving only the decimal fraction.
  // The 0+$ strips off any trailing zeroes.
  var decimalCount = (+decimalNumber).toString().replace(/^-?\d*\.?|0+$/g, '')
    .length;

  var decimalMultiplier = 1;
  for (i = 0; i < decimalLimit; i++) {
    decimalMultiplier *= 10;
  }

  return decimalCount <= decimalLimit
    ? decimalNumber.toString()
    : (
        Math.floor(decimalNumber * decimalMultiplier) / decimalMultiplier
      ).toFixed(decimalLimit);
};

Trader.getCapabilities = function() {
  return {
    name: 'GDAX',
    slug: 'gdax',
    currencies: ['USD', 'EUR', 'GBP', 'BTC'],
    assets: ['BTC', 'LTC', 'ETH', 'BCH'],
    markets: [
      { pair: ['USD', 'BTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
      { pair: ['USD', 'LTC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
      { pair: ['USD', 'ETH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
      { pair: ['USD', 'BCH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
      { pair: ['EUR', 'BTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
      { pair: ['EUR', 'ETH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
      { pair: ['EUR', 'LTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
      { pair: ['EUR', 'BCH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
      { pair: ['GBP', 'BTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
      { pair: ['BTC', 'LTC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
      { pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
      { pair: ['BTC', 'BCH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
    ],
    requires: ['key', 'secret', 'passphrase'],
    providesHistory: 'date',
    providesFullHistory: true,
    tid: 'tid',
    tradable: true,
    forceReorderDelay: false,
    gekkoBroker: 0.6
  };
};

module.exports = Trader;
<|endoftext|>


<commit_before>/* jshint node:true */
"use strict";

var events = require('events');
var util = require('util');

var _ = require('lodash');
var logging = require('omega-logger');
var pg = require('pg');
var pgTypes = require('pg').types;
var Promise = require('bluebird');

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

var logger = logging.loggerFor(module);

// Type OIDs for built-in types we need to know about.
var byteaTypeOID = 17;

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

/**
 * Connect to the given database.
 *
 * @todo Document parameters.
 */
function connect(connectionInfo)
{
    return new Promise(function(resolve, reject)
    {
        // Set application_name on the connection.
        connectionInfo.application_name = connectionInfo.application_name || 'web-pgq';

        pg.connect(connectionInfo, function(error, client, done)
        {
            if(error)
            {
                logger.error("Error connecting to %j: %s", connectionInfo, error.stack || error);

                return reject(error);
            } // end if

            logger.info("Connected to %j", connectionInfo);

            var conn = new Connection(client, done);

            resolve(
                conn.getSettings()
                    .then(conn.getTypes.bind(conn))
                    .return(conn)
            );
        }); // end pg.connect callback
    }); // end Promise callback
} // end connect

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

/**
 * Wraps a PostgreSQL connection.
 *
 * @param {pg.Client} client - the `node-postgres` client
 * @param {function} done - the function to release the client to the `node-postgres` pool
 */
function Connection(client, done)
{
    this.client = client;
    this.done = done;
} // end Connection

util.inherits(Connection, events.EventEmitter);

/**
 * The definition of a batch of PostgreSQL queries.
 *
 * @typedef {object} Connection#BatchDef
 * @see {@link https://github.com/brianc/node-postgres/wiki/Prepared-Statements}
 *
 * @property {*} queryID - the ID of the query batch (an arbitrary value provided by the client)
 * @property {Array.<Connection#QueryDef>} queries - the collection of queries to run
 * @property {boolean} [rollback] - if not undefined, run the queries in a transaction; if true, roll it back instead
 *          of committing when all finish successfully
 */

/**
 * The definition of a PostgreSQL query.
 *
 * @typedef {object} Connection#QueryDef
 * @see {@link https://github.com/brianc/node-postgres/wiki/Prepared-Statements}
 *
 * @property {string} [name] - if supplied, creates or uses a prepared statement with the given name
 * @property {string} [text] - the text of the SQL query to run or prepare (required unless using a prepared statement)
 * @property {Array} [values] - the values of the query parameters
 */

/**
 * The results of a PostgreSQL query.
 *
 * @typedef {object} Connection#QueryResults
 *
 * @property {number} rowCount - the number of rows in the result set
 * @property {number} affectedRowCount - the number of affected rows reported by the database
 * @property {number} totalTimeMS - the total time the query took to run, in milliseconds
 */

/**
 * The results of a batch of PostgreSQL queries.
 *
 * @typedef {object} Connection#BatchResults
 *
 * @property {string} status - the final status of the batch; one of: `"committed"`, `"committed (no transaction)"`, or
 *          `"rolled back"`
 * @property {number} totalTimeMS - the total time the batch took to run, in milliseconds
 * @property {Array.<Connection#QueryResults>} queryResults - the results object of each query, with some extra
 *          information attached
 */

/**
 * A row has been generated by a database query.
 *
 * @event Connection#row
 *
 * @param {*} queryID - the ID of the query that generated this row
 * @param {object} row - the row
 */

/**
 * An error occurred on this connection.
 *
 * @event Connection#error
 *
 * @param {object} error - the error that occurred
 */

Object.defineProperty(Connection.prototype, 'remoteAddress', {
    enumerable: true,
    get: function()
    {
        return this.client.connection.stream.remoteAddress;
    } // end get
}); // end '

/**
 * "Close" this connection.
 *
 * Actually, this releases the underlying client back to the to the `node-postgres` pool, unless `error` is passed.
 *
 * @param {object} error - a fatal connection error that occurred; forces the connection to be closed instead of being
 *          returned to the pool
 */
Connection.prototype.close = function(error)
{
    this.done(error);

    delete this.client;
    delete this.done;
}; // end Connection#close

Connection.prototype.getSettings = function()
{
    var self = this;
    this.settings = {};
    this.settingDescriptions = {};
    var queryDef = {text: "SHOW ALL"};

    return this._query(queryDef, function(row)
    {
        self.settings[row.name] = row.setting;
        self.settingDescriptions[row.name] = row.description;
    })
    .then(function()
    {
        logger.debug("server settings = %s", logger.dump(self.settings));
        return self.settings;
    });
}; // end Connection#getSettings

Connection.prototype.getTypes = function()
{
    var self = this;
    this.types = {};
    var queryDef = {text: "SELECT t.oid, COALESCE(an.nspname, n.nspname) AS nspname, COALESCE(at.typname, t.typname) AS typname, t.typcategory, n.nspname, t.typname FROM pg_type t LEFT JOIN pg_namespace n ON n.oid = t.typnamespace LEFT JOIN pg_type at ON at.oid = t.typelem LEFT JOIN pg_namespace an ON an.oid = at.typnamespace WHERE t.typtype = 'b' ORDER BY t.oid"};

    return this._query(queryDef, function(row)
    {
        var typeOID = row.oid;
        var typeName = (row.nspname == 'pg_catalog' ? '' : row.nspname + '.') + row.typname +
            (row.typcategory == 'A' ? '[]' : '');

        self.types[typeOID] = typeName;

        switch(typeName)
        {
            case 'public.geometry':
                logger.debug("Setting type parser for data type %s (oid=%s) to that of `bytea`.",
                    logger.dump(typeName), logger.dump(typeOID));

                pgTypes.setTypeParser(typeOID, 'text', pgTypes.getTypeParser(byteaTypeOID));
                break;
        } // end switch
    })
    .then(function()
    {
        return self.types;
    });
}; // end Connection#getTypes

Connection.prototype._query = function(queryDef, onRow)
{
    var self = this;
    this.types = {};

    onRow = onRow || function(row, result)
    {
        result.addRow(row);
    }; // end default onRow;

    if(!self.client)
    {
        return Promise.reject(new Error("Connection has been closed! Can't run query " + util.inspect(queryDef)));
    } // end if

    return new Promise(function(resolve, reject)
    {
        var query = self.client.query(queryDef)
            .on('error', onError)
            .on('row', onRow)
            .on('end', onEnd);

        function onError(error)
        {
            logger.error("Connection#_query got error: %s", error.stack || error);

            reject(error);

            query
                .removeListener('error', onError)
                .removeListener('row', onRow)
                .removeListener('end', onEnd);
        } // end onError

        function onEnd(result)
        {
            resolve(result);

            query
                .removeListener('error', onError)
                .removeListener('row', onRow)
                .removeListener('end', onEnd);
        } // end onEnd
    });
}; // end Connection#_query

/**
 * Execute a batch of queries on this PostgreSQL connection.
 *
 * @param {Connection#BatchDef} batchDef - the definition of the batch of queries to run
 *
 * @returns {Promise.<Connection#BatchResults>} - a promise for the results object of each query once all queries are
 *          finished, with some extra information attached
 */
Connection.prototype.query = function(batchDef)
{
    var self = this;

    var queryID = batchDef.queryID;

    var batchStartTime = process.hrtime();
    if(batchDef.rollback !== undefined)
    {
        // Run the queries in the batch inside a transaction.
        return self.beginTransaction()
            .then(function()
            {
                return Promise.map(batchDef.queries, doQuery, {concurrency: 1})
                    .tap(function()
                    {
                        return (batchDef.rollback ? self.rollback() : self.commit());
                    })
                    .catch(function(error)
                    {
                        return self.rollback()
                            .finally(function()
                            {
                                throw error;
                            });
                    });
            }) // end .then callback
            .then(buildBatchResults(batchDef.rollback ? "rolled back" : "committed"));
    }
    else
    {
        // Run the queries _without_ a trasaction. (on error, the results of some queries in the batch may still have
        // been committed!)
        return Promise.map(batchDef.queries, doQuery, {concurrency: 1})
            .then(buildBatchResults("committed (no transaction)"));
    } // end if

    // Build the Connection#BatchResults object for a successful response.
    function buildBatchResults(status)
    {
        return function(queryResults)
        {
            var totalTime = process.hrtime(batchStartTime);
            var totalTimeMS = totalTime[0] * 1e3 + totalTime[1] / 1e6;

            return {
                status: status,
                queryResults: queryResults,
                totalTimeMS: totalTimeMS
            };
        };
    } // end buildBatchResults

    // Run an individual query from this batch.
    function doQuery(queryDef)
    {
        queryDef.rowMode = "array";

        return new Promise(function(resolve, reject)
        {
            logger.info("Running query %j...", queryID);
            logger.trace("Query %j: %s", queryID, logger.dump(queryDef));

            var startTime = process.hrtime();
            var query = self.client.query(queryDef)
                .on('error', onError)
                .on('row', onRow)
                .on('end', onEnd);

            function onError(error)
            {
                logger.error("Query %j got error: %s", queryID, error.stack || error);

                reject(error);

                query
                    .removeListener('error', onError)
                    .removeListener('row', onRow)
                    .removeListener('end', onEnd);
            } // end onError

            var fieldsEmitted = false;
            function emitFields(fields)
            {
                if(!fieldsEmitted)
                {
                    self.emit('fields', queryID, fields.map(function(field)
                    {
                        field.dataType = self.types[field.dataTypeID];
                        return field;
                    }));

                    fieldsEmitted = true;
                } // end if
            } // end emitFields

            function onRow(row, result)
            {
                emitFields(result.fields);

                result.returnedRowCount = (result.returnedRowCount || 0) + 1;

                self.emit('row', queryID, row);
            } // end onRow

            function onEnd(result)
            {
                var totalTime = process.hrtime(startTime);
                var totalTimeMS = totalTime[0] * 1e3 + totalTime[1] / 1e6;

                result.returnedRowCount = result.returnedRowCount || 0;

                logger.info("Query %j finished. (%s row%s returned, %s row%s affected)", queryID,
                    result.returnedRowCount, result.returnedRowCount == 1 ? '' : 's',
                    result.rowCount, result.rowCount == 1 ? '' : 's'
                );

                emitFields(result.fields);

                var response = _.assign(
                    {
                        rowCount: result.returnedRowCount,
                        affectedRowCount: result.rowCount,
                        totalTimeMS: totalTimeMS
                    },
                    _.omit(result, 'returnedRowCount', 'rowCount', 'rows')
                );
                logger.trace("response: %s", logger.dump(response));

                resolve(response);

                query
                    .removeListener('error', onError)
                    .removeListener('row', onRow)
                    .removeListener('end', onEnd);
            } // end onEnd
        }); // end Promise callback
    } // end doQuery
}; // end Connection#query

/**
 * Begin a transaction.
 *
 * @returns {Promise} - a promise for when the transaction has started
 */
Connection.prototype.beginTransaction = function()
{
    var self = this;

    return self._query('BEGIN TRANSACTION', function() {})
        .catch(function(error)
        {
            return self.rollback()
                .finally(function()
                {
                    throw error;
                });
        });
}; // end Connection#beginTransaction

/**
 * Roll back the current transaction.
 *
 * @returns {Promise} - a promise for when the transaction has been rolled back
 */
Connection.prototype.rollback = function()
{
    var self = this;

    return self._query('ROLLBACK', function() {})
        .catch(function(error)
        {
            // Quoting the node-postgres wiki:
            // "if there was a problem rolling back the query something is seriously messed up."

            // Since the connection is most likely in a bad state, close it instead of returning it to the pool.
            self.close(error);

            throw error;
        });
}; // end Connection#rollback

/**
 * Commit the current transaction.
 *
 * @returns {Promise} - a promise for when the transaction has been committed
 */
Connection.prototype.commit = function()
{
    var self = this;

    return self._query('COMMIT', function() {})
        .catch(function(error)
        {
            // Errors committing a transaction are also a sign that something is pretty messed up.
            // Since the connection is most likely in a bad state, close it instead of returning it to the pool.
            self.close(error);

            throw error;
        });
}; // end Connection#commit

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

module.exports = {
    connect: connect,
    Connection: Connection
};
<commit_msg>Don't clear the `types` object every time we call #_query().<commit_after>/* jshint node:true */
"use strict";

var events = require('events');
var util = require('util');

var _ = require('lodash');
var logging = require('omega-logger');
var pg = require('pg');
var pgTypes = require('pg').types;
var Promise = require('bluebird');

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

var logger = logging.loggerFor(module);

// Type OIDs for built-in types we need to know about.
var byteaTypeOID = 17;

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

/**
 * Connect to the given database.
 *
 * @todo Document parameters.
 */
function connect(connectionInfo)
{
    return new Promise(function(resolve, reject)
    {
        // Set application_name on the connection.
        connectionInfo.application_name = connectionInfo.application_name || 'web-pgq';

        pg.connect(connectionInfo, function(error, client, done)
        {
            if(error)
            {
                logger.error("Error connecting to %j: %s", connectionInfo, error.stack || error);

                return reject(error);
            } // end if

            logger.info("Connected to %j", connectionInfo);

            var conn = new Connection(client, done);

            resolve(
                conn.getSettings()
                    .then(conn.getTypes.bind(conn))
                    .return(conn)
            );
        }); // end pg.connect callback
    }); // end Promise callback
} // end connect

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

/**
 * Wraps a PostgreSQL connection.
 *
 * @param {pg.Client} client - the `node-postgres` client
 * @param {function} done - the function to release the client to the `node-postgres` pool
 */
function Connection(client, done)
{
    this.client = client;
    this.done = done;
} // end Connection

util.inherits(Connection, events.EventEmitter);

/**
 * The definition of a batch of PostgreSQL queries.
 *
 * @typedef {object} Connection#BatchDef
 * @see {@link https://github.com/brianc/node-postgres/wiki/Prepared-Statements}
 *
 * @property {*} queryID - the ID of the query batch (an arbitrary value provided by the client)
 * @property {Array.<Connection#QueryDef>} queries - the collection of queries to run
 * @property {boolean} [rollback] - if not undefined, run the queries in a transaction; if true, roll it back instead
 *          of committing when all finish successfully
 */

/**
 * The definition of a PostgreSQL query.
 *
 * @typedef {object} Connection#QueryDef
 * @see {@link https://github.com/brianc/node-postgres/wiki/Prepared-Statements}
 *
 * @property {string} [name] - if supplied, creates or uses a prepared statement with the given name
 * @property {string} [text] - the text of the SQL query to run or prepare (required unless using a prepared statement)
 * @property {Array} [values] - the values of the query parameters
 */

/**
 * The results of a PostgreSQL query.
 *
 * @typedef {object} Connection#QueryResults
 *
 * @property {number} rowCount - the number of rows in the result set
 * @property {number} affectedRowCount - the number of affected rows reported by the database
 * @property {number} totalTimeMS - the total time the query took to run, in milliseconds
 */

/**
 * The results of a batch of PostgreSQL queries.
 *
 * @typedef {object} Connection#BatchResults
 *
 * @property {string} status - the final status of the batch; one of: `"committed"`, `"committed (no transaction)"`, or
 *          `"rolled back"`
 * @property {number} totalTimeMS - the total time the batch took to run, in milliseconds
 * @property {Array.<Connection#QueryResults>} queryResults - the results object of each query, with some extra
 *          information attached
 */

/**
 * A row has been generated by a database query.
 *
 * @event Connection#row
 *
 * @param {*} queryID - the ID of the query that generated this row
 * @param {object} row - the row
 */

/**
 * An error occurred on this connection.
 *
 * @event Connection#error
 *
 * @param {object} error - the error that occurred
 */

Object.defineProperty(Connection.prototype, 'remoteAddress', {
    enumerable: true,
    get: function()
    {
        return this.client.connection.stream.remoteAddress;
    } // end get
}); // end '

/**
 * "Close" this connection.
 *
 * Actually, this releases the underlying client back to the to the `node-postgres` pool, unless `error` is passed.
 *
 * @param {object} error - a fatal connection error that occurred; forces the connection to be closed instead of being
 *          returned to the pool
 */
Connection.prototype.close = function(error)
{
    this.done(error);

    delete this.client;
    delete this.done;
}; // end Connection#close

Connection.prototype.getSettings = function()
{
    var self = this;
    this.settings = {};
    this.settingDescriptions = {};
    var queryDef = {text: "SHOW ALL"};

    return this._query(queryDef, function(row)
    {
        self.settings[row.name] = row.setting;
        self.settingDescriptions[row.name] = row.description;
    })
    .then(function()
    {
        logger.debug("server settings = %s", logger.dump(self.settings));
        return self.settings;
    });
}; // end Connection#getSettings

Connection.prototype.getTypes = function()
{
    var self = this;
    this.types = {};
    var queryDef = {text: "SELECT t.oid, COALESCE(an.nspname, n.nspname) AS nspname, COALESCE(at.typname, t.typname) AS typname, t.typcategory, n.nspname, t.typname FROM pg_type t LEFT JOIN pg_namespace n ON n.oid = t.typnamespace LEFT JOIN pg_type at ON at.oid = t.typelem LEFT JOIN pg_namespace an ON an.oid = at.typnamespace WHERE t.typtype = 'b' ORDER BY t.oid"};

    return this._query(queryDef, function(row)
    {
        var typeOID = row.oid;
        var typeName = (row.nspname == 'pg_catalog' ? '' : row.nspname + '.') + row.typname +
            (row.typcategory == 'A' ? '[]' : '');

        self.types[typeOID] = typeName;

        switch(typeName)
        {
            case 'public.geometry':
                logger.debug("Setting type parser for data type %s (oid=%s) to that of `bytea`.",
                    logger.dump(typeName), logger.dump(typeOID));

                pgTypes.setTypeParser(typeOID, 'text', pgTypes.getTypeParser(byteaTypeOID));
                break;
        } // end switch
    })
    .then(function()
    {
        return self.types;
    });
}; // end Connection#getTypes

Connection.prototype._query = function(queryDef, onRow)
{
    var self = this;

    onRow = onRow || function(row, result)
    {
        result.addRow(row);
    }; // end default onRow;

    if(!self.client)
    {
        return Promise.reject(new Error("Connection has been closed! Can't run query " + util.inspect(queryDef)));
    } // end if

    return new Promise(function(resolve, reject)
    {
        var query = self.client.query(queryDef)
            .on('error', onError)
            .on('row', onRow)
            .on('end', onEnd);

        function onError(error)
        {
            logger.error("Connection#_query got error: %s", error.stack || error);

            reject(error);

            query
                .removeListener('error', onError)
                .removeListener('row', onRow)
                .removeListener('end', onEnd);
        } // end onError

        function onEnd(result)
        {
            resolve(result);

            query
                .removeListener('error', onError)
                .removeListener('row', onRow)
                .removeListener('end', onEnd);
        } // end onEnd
    });
}; // end Connection#_query

/**
 * Execute a batch of queries on this PostgreSQL connection.
 *
 * @param {Connection#BatchDef} batchDef - the definition of the batch of queries to run
 *
 * @returns {Promise.<Connection#BatchResults>} - a promise for the results object of each query once all queries are
 *          finished, with some extra information attached
 */
Connection.prototype.query = function(batchDef)
{
    var self = this;

    var queryID = batchDef.queryID;

    var batchStartTime = process.hrtime();
    if(batchDef.rollback !== undefined)
    {
        // Run the queries in the batch inside a transaction.
        return self.beginTransaction()
            .then(function()
            {
                return Promise.map(batchDef.queries, doQuery, {concurrency: 1})
                    .tap(function()
                    {
                        return (batchDef.rollback ? self.rollback() : self.commit());
                    })
                    .catch(function(error)
                    {
                        return self.rollback()
                            .finally(function()
                            {
                                throw error;
                            });
                    });
            }) // end .then callback
            .then(buildBatchResults(batchDef.rollback ? "rolled back" : "committed"));
    }
    else
    {
        // Run the queries _without_ a trasaction. (on error, the results of some queries in the batch may still have
        // been committed!)
        return Promise.map(batchDef.queries, doQuery, {concurrency: 1})
            .then(buildBatchResults("committed (no transaction)"));
    } // end if

    // Build the Connection#BatchResults object for a successful response.
    function buildBatchResults(status)
    {
        return function(queryResults)
        {
            var totalTime = process.hrtime(batchStartTime);
            var totalTimeMS = totalTime[0] * 1e3 + totalTime[1] / 1e6;

            return {
                status: status,
                queryResults: queryResults,
                totalTimeMS: totalTimeMS
            };
        };
    } // end buildBatchResults

    // Run an individual query from this batch.
    function doQuery(queryDef)
    {
        queryDef.rowMode = "array";

        return new Promise(function(resolve, reject)
        {
            logger.info("Running query %j...", queryID);
            logger.trace("Query %j: %s", queryID, logger.dump(queryDef));

            var startTime = process.hrtime();
            var query = self.client.query(queryDef)
                .on('error', onError)
                .on('row', onRow)
                .on('end', onEnd);

            function onError(error)
            {
                logger.error("Query %j got error: %s", queryID, error.stack || error);

                reject(error);

                query
                    .removeListener('error', onError)
                    .removeListener('row', onRow)
                    .removeListener('end', onEnd);
            } // end onError

            var fieldsEmitted = false;
            function emitFields(fields)
            {
                if(!fieldsEmitted)
                {
                    self.emit('fields', queryID, fields.map(function(field)
                    {
                        field.dataType = self.types[field.dataTypeID];
                        return field;
                    }));

                    fieldsEmitted = true;
                } // end if
            } // end emitFields

            function onRow(row, result)
            {
                emitFields(result.fields);

                result.returnedRowCount = (result.returnedRowCount || 0) + 1;

                self.emit('row', queryID, row);
            } // end onRow

            function onEnd(result)
            {
                var totalTime = process.hrtime(startTime);
                var totalTimeMS = totalTime[0] * 1e3 + totalTime[1] / 1e6;

                result.returnedRowCount = result.returnedRowCount || 0;

                logger.info("Query %j finished. (%s row%s returned, %s row%s affected)", queryID,
                    result.returnedRowCount, result.returnedRowCount == 1 ? '' : 's',
                    result.rowCount, result.rowCount == 1 ? '' : 's'
                );

                emitFields(result.fields);

                var response = _.assign(
                    {
                        rowCount: result.returnedRowCount,
                        affectedRowCount: result.rowCount,
                        totalTimeMS: totalTimeMS
                    },
                    _.omit(result, 'returnedRowCount', 'rowCount', 'rows')
                );
                logger.trace("response: %s", logger.dump(response));

                resolve(response);

                query
                    .removeListener('error', onError)
                    .removeListener('row', onRow)
                    .removeListener('end', onEnd);
            } // end onEnd
        }); // end Promise callback
    } // end doQuery
}; // end Connection#query

/**
 * Begin a transaction.
 *
 * @returns {Promise} - a promise for when the transaction has started
 */
Connection.prototype.beginTransaction = function()
{
    var self = this;

    return self._query('BEGIN TRANSACTION', function() {})
        .catch(function(error)
        {
            return self.rollback()
                .finally(function()
                {
                    throw error;
                });
        });
}; // end Connection#beginTransaction

/**
 * Roll back the current transaction.
 *
 * @returns {Promise} - a promise for when the transaction has been rolled back
 */
Connection.prototype.rollback = function()
{
    var self = this;

    return self._query('ROLLBACK', function() {})
        .catch(function(error)
        {
            // Quoting the node-postgres wiki:
            // "if there was a problem rolling back the query something is seriously messed up."

            // Since the connection is most likely in a bad state, close it instead of returning it to the pool.
            self.close(error);

            throw error;
        });
}; // end Connection#rollback

/**
 * Commit the current transaction.
 *
 * @returns {Promise} - a promise for when the transaction has been committed
 */
Connection.prototype.commit = function()
{
    var self = this;

    return self._query('COMMIT', function() {})
        .catch(function(error)
        {
            // Errors committing a transaction are also a sign that something is pretty messed up.
            // Since the connection is most likely in a bad state, close it instead of returning it to the pool.
            self.close(error);

            throw error;
        });
}; // end Connection#commit

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

module.exports = {
    connect: connect,
    Connection: Connection
};
<|endoftext|>


<commit_before>// This file contains that retrieve or validate anything related to the current paths ancestry.

import * as t from "@babel/types";
import NodePath from "./index";

/**
 * Call the provided `callback` with the `NodePath`s of all the parents.
 * When the `callback` returns a truthy value, we return that node path.
 */

export function findParent(callback): ?NodePath {
  let path = this;
  while ((path = path.parentPath)) {
    if (callback(path)) return path;
  }
  return null;
}

/**
 * Starting at current `NodePath` and going up the tree, return the first
 * `NodePath` that causes the provided `callback` to return a truthy value.
 */

export function find(callback): ?NodePath {
  let path = this;
  do {
    if (callback(path)) return path;
  } while ((path = path.parentPath));
  return null;
}

/**
 * Get the parent function of the current path.
 */

export function getFunctionParent(): ?NodePath {
  return this.findParent(p => p.isFunction());
}

/**
 * Walk up the tree until we hit a parent node path in a list.
 */

export function getStatementParent(): NodePath {
  let path = this;

  do {
    if (
      !path.parentPath ||
      (Array.isArray(path.container) && path.isStatement())
    ) {
      break;
    } else {
      path = path.parentPath;
    }
  } while (path);

  if (path && (path.isProgram() || path.isFile())) {
    throw new Error(
      "File/Program node, we can't possibly find a statement parent to this",
    );
  }

  return path;
}

/**
 * Get the deepest common ancestor and then from it, get the earliest relationship path
 * to that ancestor.
 *
 * Earliest is defined as being "before" all the other nodes in terms of list container
 * position and visiting key.
 */

export function getEarliestCommonAncestorFrom(
  paths: Array<NodePath>,
): NodePath {
  return this.getDeepestCommonAncestorFrom(paths, function(
    deepest,
    i,
    ancestries,
  ) {
    let earliest;
    const keys = t.VISITOR_KEYS[deepest.type];

    for (const ancestry of (ancestries: Array)) {
      const path = ancestry[i + 1];

      // first path
      if (!earliest) {
        earliest = path;
        continue;
      }

      // handle containers
      if (path.listKey && earliest.listKey === path.listKey) {
        // we're in the same container so check if we're earlier
        if (path.key < earliest.key) {
          earliest = path;
          continue;
        }
      }

      // handle keys
      const earliestKeyIndex = keys.indexOf(earliest.parentKey);
      const currentKeyIndex = keys.indexOf(path.parentKey);
      if (earliestKeyIndex > currentKeyIndex) {
        // key appears before so it's earlier
        earliest = path;
      }
    }

    return earliest;
  });
}

/**
 * Get the earliest path in the tree where the provided `paths` intersect.
 *
 * TODO: Possible optimisation target.
 */

export function getDeepestCommonAncestorFrom(
  paths: Array<NodePath>,
  filter?: Function,
): NodePath {
  if (!paths.length) {
    return this;
  }

  if (paths.length === 1) {
    return paths[0];
  }

  // minimum depth of the tree so we know the highest node
  let minDepth = Infinity;

  // last common ancestor
  let lastCommonIndex, lastCommon;

  // get the ancestors of the path, breaking when the parent exceeds ourselves
  const ancestries = paths.map(path => {
    const ancestry = [];

    do {
      ancestry.unshift(path);
    } while ((path = path.parentPath) && path !== this);

    // save min depth to avoid going too far in
    if (ancestry.length < minDepth) {
      minDepth = ancestry.length;
    }

    return ancestry;
  });

  // get the first ancestry so we have a seed to assess all other ancestries with
  const first = ancestries[0];

  // check ancestor equality
  depthLoop: for (let i = 0; i < minDepth; i++) {
    const shouldMatch = first[i];

    for (const ancestry of (ancestries: Array)) {
      if (ancestry[i] !== shouldMatch) {
        // we've hit a snag
        break depthLoop;
      }
    }

    // next iteration may break so store these so they can be returned
    lastCommonIndex = i;
    lastCommon = shouldMatch;
  }

  if (lastCommon) {
    if (filter) {
      return filter(lastCommon, lastCommonIndex, ancestries);
    } else {
      return lastCommon;
    }
  } else {
    throw new Error("Couldn't find intersection");
  }
}

/**
 * Build an array of node paths containing the entire ancestry of the current node path.
 *
 * NOTE: The current node path is included in this.
 */

export function getAncestry(): Array<NodePath> {
  let path = this;
  const paths = [];
  do {
    paths.push(path);
  } while ((path = path.parentPath));
  return paths;
}

/**
 * A helper to find if `this` path is an ancestor of @param maybeDescendant
 */
export function isAncestor(maybeDescendant: NodePath): boolean {
  return maybeDescendant.isDescendant(this);
}

/**
 * A helper to find if `this` path is a descendant of @param maybeAncestor
 */
export function isDescendant(maybeAncestor: NodePath): boolean {
  return !!this.findParent(parent => parent === maybeAncestor);
}

export function inType(): boolean {
  let path = this;
  while (path) {
    for (const type of (arguments: Array)) {
      if (path.node.type === type) return true;
    }
    path = path.parentPath;
  }

  return false;
}
<commit_msg>docs: updates docs of `findParent` and `find` (#10479) [skip ci]<commit_after>// This file contains that retrieve or validate anything related to the current paths ancestry.

import * as t from "@babel/types";
import NodePath from "./index";

/**
 * Starting at the parent path of the current `NodePath` and going up the
 * tree, return the first `NodePath` that causes the provided `callback`
 * to return a truthy value, or `null` if the `callback` never returns a
 * truthy value.
 */

export function findParent(callback): ?NodePath {
  let path = this;
  while ((path = path.parentPath)) {
    if (callback(path)) return path;
  }
  return null;
}

/**
 * Starting at current `NodePath` and going up the tree, return the first
 * `NodePath` that causes the provided `callback` to return a truthy value,
 * or `null` if the `callback` never returns a truthy value.
 */

export function find(callback): ?NodePath {
  let path = this;
  do {
    if (callback(path)) return path;
  } while ((path = path.parentPath));
  return null;
}

/**
 * Get the parent function of the current path.
 */

export function getFunctionParent(): ?NodePath {
  return this.findParent(p => p.isFunction());
}

/**
 * Walk up the tree until we hit a parent node path in a list.
 */

export function getStatementParent(): NodePath {
  let path = this;

  do {
    if (
      !path.parentPath ||
      (Array.isArray(path.container) && path.isStatement())
    ) {
      break;
    } else {
      path = path.parentPath;
    }
  } while (path);

  if (path && (path.isProgram() || path.isFile())) {
    throw new Error(
      "File/Program node, we can't possibly find a statement parent to this",
    );
  }

  return path;
}

/**
 * Get the deepest common ancestor and then from it, get the earliest relationship path
 * to that ancestor.
 *
 * Earliest is defined as being "before" all the other nodes in terms of list container
 * position and visiting key.
 */

export function getEarliestCommonAncestorFrom(
  paths: Array<NodePath>,
): NodePath {
  return this.getDeepestCommonAncestorFrom(paths, function(
    deepest,
    i,
    ancestries,
  ) {
    let earliest;
    const keys = t.VISITOR_KEYS[deepest.type];

    for (const ancestry of (ancestries: Array)) {
      const path = ancestry[i + 1];

      // first path
      if (!earliest) {
        earliest = path;
        continue;
      }

      // handle containers
      if (path.listKey && earliest.listKey === path.listKey) {
        // we're in the same container so check if we're earlier
        if (path.key < earliest.key) {
          earliest = path;
          continue;
        }
      }

      // handle keys
      const earliestKeyIndex = keys.indexOf(earliest.parentKey);
      const currentKeyIndex = keys.indexOf(path.parentKey);
      if (earliestKeyIndex > currentKeyIndex) {
        // key appears before so it's earlier
        earliest = path;
      }
    }

    return earliest;
  });
}

/**
 * Get the earliest path in the tree where the provided `paths` intersect.
 *
 * TODO: Possible optimisation target.
 */

export function getDeepestCommonAncestorFrom(
  paths: Array<NodePath>,
  filter?: Function,
): NodePath {
  if (!paths.length) {
    return this;
  }

  if (paths.length === 1) {
    return paths[0];
  }

  // minimum depth of the tree so we know the highest node
  let minDepth = Infinity;

  // last common ancestor
  let lastCommonIndex, lastCommon;

  // get the ancestors of the path, breaking when the parent exceeds ourselves
  const ancestries = paths.map(path => {
    const ancestry = [];

    do {
      ancestry.unshift(path);
    } while ((path = path.parentPath) && path !== this);

    // save min depth to avoid going too far in
    if (ancestry.length < minDepth) {
      minDepth = ancestry.length;
    }

    return ancestry;
  });

  // get the first ancestry so we have a seed to assess all other ancestries with
  const first = ancestries[0];

  // check ancestor equality
  depthLoop: for (let i = 0; i < minDepth; i++) {
    const shouldMatch = first[i];

    for (const ancestry of (ancestries: Array)) {
      if (ancestry[i] !== shouldMatch) {
        // we've hit a snag
        break depthLoop;
      }
    }

    // next iteration may break so store these so they can be returned
    lastCommonIndex = i;
    lastCommon = shouldMatch;
  }

  if (lastCommon) {
    if (filter) {
      return filter(lastCommon, lastCommonIndex, ancestries);
    } else {
      return lastCommon;
    }
  } else {
    throw new Error("Couldn't find intersection");
  }
}

/**
 * Build an array of node paths containing the entire ancestry of the current node path.
 *
 * NOTE: The current node path is included in this.
 */

export function getAncestry(): Array<NodePath> {
  let path = this;
  const paths = [];
  do {
    paths.push(path);
  } while ((path = path.parentPath));
  return paths;
}

/**
 * A helper to find if `this` path is an ancestor of @param maybeDescendant
 */
export function isAncestor(maybeDescendant: NodePath): boolean {
  return maybeDescendant.isDescendant(this);
}

/**
 * A helper to find if `this` path is a descendant of @param maybeAncestor
 */
export function isDescendant(maybeAncestor: NodePath): boolean {
  return !!this.findParent(parent => parent === maybeAncestor);
}

export function inType(): boolean {
  let path = this;
  while (path) {
    for (const type of (arguments: Array)) {
      if (path.node.type === type) return true;
    }
    path = path.parentPath;
  }

  return false;
}
<|endoftext|>


<commit_before>import { types, widths } from "../constants";

function getButtonDefaultButtonRules(themeData) {
  return {
    borderWidth: themeData["button.borderWidth"],
    borderStyle: "solid",
    borderColor: "transparent",
    borderRadius: themeData["button.borderRadius"],
    boxShadow: "none",
    boxSizing: "border-box",
    color: themeData["colorScheme.textColor"],
    cursor: "pointer",
    display: "inline-block",
    fontFamily: themeData["button.fontFamily"],
    fontSize: themeData["button.fontSize"],
    fontWeight: themeData["button.fontWeight"],
    lineHeight: themeData["button.lineHeight"],
    margin: "0",
    overflow: "hidden",
    padding: `${themeData["button.verticalPadding"]}
      ${themeData["button.horizontalPadding"]}`,
    position: "relative",
    textAlign: "center",
    textDecoration: "none",
    textOverflow: "ellipsis",
    transitionDuration: "0.3s",
    transitionProperty: "box-shadow",
    transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
    userSelect: "none",
    whiteSpace: "nowrap",
    MozOsxFontSmoothing: "grayscale",
    WebkitFontSmoothing: "antialiased"
  };
}

function getButtonRulesByType(type, themeData) {
  switch (type) {
    case types.SOLID:
    case types.PRIMARY: {
      return {
        background: themeData["button.solid.backgroundColor"],
        color: themeData["button.solid.textColor"],
        "&:visited": {
          color: themeData["button.solid.textColor"]
        },
        "svg *": {
          fill: themeData["button.solid.icon.color"]
        }
      };
    }
    case types.OUTLINE:
    case types.SECONDARY: {
      return {
        background: "none",
        borderColor: themeData["button.outline.borderColor"],
        color: themeData["button.outline.textColor"],
        "&:visited": {
          color: themeData["button.outline.textColor"]
        },
        "svg *": {
          fill: themeData["button.outline.icon.color"]
        }
      };
    }
    case types.FLAT: {
      return {
        background: "none",
        borderColor: themeData["button.flat.borderColor"],
        color: themeData["button.flat.textColor"],
        "&:visited": {
          color: themeData["button.flat.textColor"]
        },
        "svg *": {
          fill: themeData["button.flat.icon.color"]
        }
      };
    }
    default:
      return {};
  }
}

function getButtonRulesByDisabled(type, themeData) {
  return {
    opacity: themeData["component.disabled.opacity"],
    cursor: "default"
  };
}

function getButtonRulesByWidth(width) {
  return width === widths.GROW ? { width: "100%" } : {};
}

function getButtonHoverRulesByType(type, themeData) {
  switch (type) {
    case types.SOLID:
    case types.PRIMARY:
      return {
        background: themeData["button.solid.hover.backgroundColor"],
        boxShadow: `0 0 0 ${themeData["button.halo.width"]} ${
          themeData["button.solid.hover.halo.color"]
        }`
      };
    case types.OUTLINE:
    case types.SECONDARY:
      return {
        borderColor: themeData["button.outline.hover.borderColor"],
        boxShadow: `0 0 0 ${themeData["button.halo.width"]} ${
          themeData["button.outline.hover.halo.color"]
        }`
      };
    case types.FLAT:
      return {
        borderColor: themeData["button.flat.hover.borderColor"],
        boxShadow: `0 0 0 ${themeData["button.halo.width"]} ${
          themeData["button.flat.hover.halo.color"]
        }`
      };
    default:
      return {};
  }
}

function getButtonFocusRulesByType(type, themeData) {
  switch (type) {
    case types.SOLID:
    case types.PRIMARY:
      return {
        background: themeData["button.solid.focus.backgroundColor"],
        boxShadow: `0 0 0 ${themeData["button.halo.width"]} ${
          themeData["button.solid.focus.halo.color"]
        }`,
        outline: `none`
      };
    case types.OUTLINE:
    case types.SECONDARY:
      return {
        background: themeData["button.outline.focus.backgroundColor"],
        borderColor: themeData["button.outline.focus.borderColor"],
        boxShadow: `0 0 0 ${themeData["button.halo.width"]} ${
          themeData["button.outline.focus.halo.color"]
        }`,
        color: themeData["button.outline.focus.textColor"],
        outline: `none`,
        "svg *": {
          fill: themeData["button.outline.focus.icon.color"]
        }
      };
    case types.FLAT:
      return {
        background: themeData["button.flat.focus.backgroundColor"],
        borderColor: themeData["button.flat.focus.borderColor"],
        boxShadow: `0 0 0 ${themeData["button.halo.width"]} ${
          themeData["button.flat.focus.halo.color"]
        }`,
        color: themeData["button.flat.focus.textColor"],
        outline: `none`,
        "svg *": {
          fill: themeData["button.flat.focus.icon.color"]
        }
      };
    default:
      return {};
  }
}

function getButtonPressedRulesByType(type, themeData) {
  switch (type) {
    case types.SOLID:
    case types.PRIMARY:
      return {
        boxShadow: `0 0 0 ${themeData["button.solid.pressed.haloWidth"]} ${
          themeData["button.solid.pressed.halo.color"]
        }`
      };
    case types.OUTLINE:
    case types.SECONDARY:
      return {
        borderColor: themeData["button.outline.pressed.borderColor"],
        boxShadow: `0 0 0 ${themeData["button.outline.pressed.haloWidth"]} ${
          themeData["button.outline.pressed.halo.color"]
        }`
      };
    case types.FLAT:
      return {
        boxShadow: `0 0 0 ${themeData["button.flat.pressed.haloWidth"]} ${
          themeData["button.flat.pressed.halo.color"]
        }`
      };
    default:
      return {};
  }
}

export default function stylesheet(props, themeData, theme) {
  const { disabled, hasFocus, hasHover, isPressed, type, width } = props;
  const iconSize = theme.densityId === "medium-density" ? "24px" : "16px";

  return {
    button: {
      ...getButtonDefaultButtonRules(themeData),
      ...getButtonRulesByType(type, themeData),
      ...(disabled
        ? getButtonRulesByDisabled(type, themeData)
        : {
            ...(hasFocus ? getButtonFocusRulesByType(type, themeData) : {}),
            ...(hasHover ? getButtonHoverRulesByType(type, themeData) : {}),
            ...(isPressed ? getButtonPressedRulesByType(type, themeData) : {})
          }),
      ...getButtonRulesByWidth(width)
    },
    icon: {
      display: "inline-block",
      position: "absolute",
      height: iconSize,
      marginRight: themeData["button.gutter"],
      top: "50%",
      transform: "translateY(-50%)"
    },
    iconText: {
      marginLeft: `calc(${iconSize} + ${themeData["button.gutter"]})`
    }
  };
}
<commit_msg>update<commit_after>import { types, widths } from "../constants";

function getButtonDefaultButtonRules(themeData) {
  return {
    borderWidth: themeData["button.borderWidth"],
    borderStyle: "solid",
    borderColor: "transparent",
    borderRadius: themeData["button.borderRadius"],
    boxSizing: "border-box",
    color: themeData["colorScheme.textColor"],
    cursor: "pointer",
    display: "inline-block",
    fontFamily: themeData["button.fontFamily"],
    fontSize: themeData["button.fontSize"],
    fontWeight: themeData["button.fontWeight"],
    lineHeight: themeData["button.lineHeight"],
    margin: "0",
    overflow: "hidden",
    padding: `${themeData["button.verticalPadding"]}
      ${themeData["button.horizontalPadding"]}`,
    position: "relative",
    textAlign: "center",
    textDecoration: "none",
    textOverflow: "ellipsis",
    transitionDuration: "0.3s",
    transitionProperty: "box-shadow",
    transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
    userSelect: "none",
    whiteSpace: "nowrap",
    MozOsxFontSmoothing: "grayscale",
    WebkitFontSmoothing: "antialiased"
  };
}

function getButtonRulesByType(type, themeData) {
  switch (type) {
    case types.SOLID:
    case types.PRIMARY: {
      return {
        background: themeData["button.solid.backgroundColor"],
        color: themeData["button.solid.textColor"],
        "&:visited": {
          color: themeData["button.solid.textColor"]
        },
        "svg *": {
          fill: themeData["button.solid.icon.color"]
        }
      };
    }
    case types.OUTLINE:
    case types.SECONDARY: {
      return {
        background: "none",
        borderColor: themeData["button.outline.borderColor"],
        color: themeData["button.outline.textColor"],
        "&:visited": {
          color: themeData["button.outline.textColor"]
        },
        "svg *": {
          fill: themeData["button.outline.icon.color"]
        }
      };
    }
    case types.FLAT: {
      return {
        background: "none",
        borderColor: themeData["button.flat.borderColor"],
        color: themeData["button.flat.textColor"],
        "&:visited": {
          color: themeData["button.flat.textColor"]
        },
        "svg *": {
          fill: themeData["button.flat.icon.color"]
        }
      };
    }
    default:
      return {};
  }
}

function getButtonRulesByDisabled(type, themeData) {
  return {
    opacity: themeData["component.disabled.opacity"],
    cursor: "default"
  };
}

function getButtonRulesByWidth(width) {
  return width === widths.GROW ? { width: "100%" } : {};
}

function getButtonHoverRulesByType(type, themeData) {
  switch (type) {
    case types.SOLID:
    case types.PRIMARY:
      return {
        background: themeData["button.solid.hover.backgroundColor"],
        boxShadow: `0 0 0 ${themeData["button.halo.width"]} ${
          themeData["button.solid.hover.halo.color"]
        }`
      };
    case types.OUTLINE:
    case types.SECONDARY:
      return {
        borderColor: themeData["button.outline.hover.borderColor"],
        boxShadow: `0 0 0 ${themeData["button.halo.width"]} ${
          themeData["button.outline.hover.halo.color"]
        }`
      };
    case types.FLAT:
      return {
        borderColor: themeData["button.flat.hover.borderColor"],
        boxShadow: `0 0 0 ${themeData["button.halo.width"]} ${
          themeData["button.flat.hover.halo.color"]
        }`
      };
    default:
      return {};
  }
}

function getButtonFocusRulesByType(type, themeData) {
  switch (type) {
    case types.SOLID:
    case types.PRIMARY:
      return {
        background: themeData["button.solid.focus.backgroundColor"],
        boxShadow: `0 0 0 ${themeData["button.halo.width"]} ${
          themeData["button.solid.focus.halo.color"]
        }`,
        outline: `none`
      };
    case types.OUTLINE:
    case types.SECONDARY:
      return {
        background: themeData["button.outline.focus.backgroundColor"],
        borderColor: themeData["button.outline.focus.borderColor"],
        boxShadow: `0 0 0 ${themeData["button.halo.width"]} ${
          themeData["button.outline.focus.halo.color"]
        }`,
        color: themeData["button.outline.focus.textColor"],
        outline: `none`,
        "svg *": {
          fill: themeData["button.outline.focus.icon.color"]
        }
      };
    case types.FLAT:
      return {
        background: themeData["button.flat.focus.backgroundColor"],
        borderColor: themeData["button.flat.focus.borderColor"],
        boxShadow: `0 0 0 ${themeData["button.halo.width"]} ${
          themeData["button.flat.focus.halo.color"]
        }`,
        color: themeData["button.flat.focus.textColor"],
        outline: `none`,
        "svg *": {
          fill: themeData["button.flat.focus.icon.color"]
        }
      };
    default:
      return {};
  }
}

function getButtonPressedRulesByType(type, themeData) {
  switch (type) {
    case types.SOLID:
    case types.PRIMARY:
      return {
        boxShadow: `0 0 0 ${themeData["button.solid.pressed.haloWidth"]} ${
          themeData["button.solid.pressed.halo.color"]
        }`
      };
    case types.OUTLINE:
    case types.SECONDARY:
      return {
        borderColor: themeData["button.outline.pressed.borderColor"],
        boxShadow: `0 0 0 ${themeData["button.outline.pressed.haloWidth"]} ${
          themeData["button.outline.pressed.halo.color"]
        }`
      };
    case types.FLAT:
      return {
        boxShadow: `0 0 0 ${themeData["button.flat.pressed.haloWidth"]} ${
          themeData["button.flat.pressed.halo.color"]
        }`
      };
    default:
      return {};
  }
}

export default function stylesheet(props, themeData, theme) {
  const { disabled, hasFocus, hasHover, isPressed, type, width } = props;
  const iconSize = theme.densityId === "medium-density" ? "24px" : "16px";

  return {
    button: {
      ...getButtonDefaultButtonRules(themeData),
      ...getButtonRulesByType(type, themeData),
      ...(disabled
        ? getButtonRulesByDisabled(type, themeData)
        : {
            ...(hasFocus ? getButtonFocusRulesByType(type, themeData) : {}),
            ...(hasHover ? getButtonHoverRulesByType(type, themeData) : {}),
            ...(isPressed ? getButtonPressedRulesByType(type, themeData) : {})
          }),
      ...getButtonRulesByWidth(width)
    },
    icon: {
      display: "inline-block",
      position: "absolute",
      height: iconSize,
      marginRight: themeData["button.gutter"],
      top: "50%",
      transform: "translateY(-50%)"
    },
    iconText: {
      marginLeft: `calc(${iconSize} + ${themeData["button.gutter"]})`
    }
  };
}
<|endoftext|>


<commit_before>/*
 * Copyright (C) 2017 Anthony DeDominic <adedomin@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

var express = require('express'),
    ircLine = require('./moose-res').ircLine,
    path = require('path'),
    app = express(),
    bodyParser = require('body-parser'),
    RateLimiter = require('express-rate-limit'),
    config = require(process.env.CONFIG_PATH)

var log

function onErr(res, msg, err, code) {
    // this is an unknown error, so we should log it.
    if (err) {
        log.error(msg, { err: err.toString() })
        msg = err.toString()
    }
    else {
        log.info(msg)
    }

    res.status(code || 500)
    res.send({
        status: 'error',
        msg,
    })
}

function onOk(res, msg) {
    log.info(msg)
    res.send({
        status: 'ok',
        msg,
    })
}

function Web(moosedb, logger) {

    log = logger

    if (config.web.proxied)
        app.enable('trust proxy')

    app.use(bodyParser.json())

    app.use('/new', new RateLimiter({
        windowMs: 60 * 1000, // 1 min
        headers: true,
        max: config.moose.maxNew || 3,
        message: JSON.stringify({
            status: 'error',
            msg: 'stop spamming moose jerk',
        }),
    }))

    app.use('/gallery', new RateLimiter({
        windowMs: 30 * 1000, // 30 sec
        delayAfter: config.moose.maxPageReq || 50,
        delayMs: 150,
        max: 0,
    }))

    var getLimiter = new RateLimiter({
        windowMs: 30 * 1000, // 30 sec
        delayAfter: config.moose.maxGetReq || 10,
        delayMs: 150,
        max: 0,
    })

    app.use('/moose', getLimiter)

    app.use('/irc', getLimiter)

    app.get('/moose/latest', (req, res) => {
        moosedb.getLatest((err, moose) => {
            if (err) return onErr(res, err, moose)
            res.send(moose)
        })
    })

    app.get('/irc/latest', (req, res) => {
        moosedb.getLatest((err, moose) => {
            if (err) return onErr(res, err, moose)
            res.send(ircLine(moose))
        })
    })

    app.get('/moose/random', (req, res) => {
        moosedb.getRandom((err, moose) => {
            if (err) return onErr(res, err, moose)
            res.send(moose)
        })
    })

    app.get('/irc/random', (req, res) => {
        moosedb.getRandom((err, moose) => {
            if (err) return onErr(res, err, moose)
            res.send(ircLine(moose))
        })
    })

    app.get('/moose/:moose', (req, res) => {
        moosedb.getMoose(req.params.moose, (err, moose) => {
            if (err) {
                res.status(404)
                return res.send({
                    status: 'error',
                    msg: 'no such moose',
                })
            }
            res.send(moose)
        })
    })

    app.get('/irc/:moose', (req, res) => {
        moosedb.getMoose(req.params.moose, (err, moose) => {
            if (err) {
                res.status(404)
                return res.send({
                    status: 'error',
                    msg: 'no such moose',
                })
            }
            res.send(ircLine(moose))
        })
    })

    app.get('/gallery/:age?', (req, res) => {
        var name = req.query.q || '',
            page = req.query.p || 0,
            age = req.params.age == 'oldest' ? 1 : -1

        moosedb.getGallery(name, page, age, (err, meese) => {
            if (err) return onErr(res, err, meese)
            res.send(meese)
        })
    })

    app.get('/dump', (req, res) => {
        res.sendFile(path.resolve(path.join(config.moose.db, 'dump.json')))
    })

    app.put('/new', (req, res) => {
        moosedb.newMoose(req.body, (err, msg) => {
            if (err) return onErr(res, err, msg, 422)
            onOk(res, msg)
        })
    })

    app.use('/', express.static(path.join(__dirname, '../public'), {
        index: [ 'index.html' ],
    }))

    app.listen(
        config.web.port || 7512,
        config.web.interface
    )
}

module.exports = Web
<commit_msg>custom mimetype, err<commit_after>/*
 * Copyright (C) 2017 Anthony DeDominic <adedomin@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

var express = require('express'),
    ircLine = require('./moose-res').ircLine,
    path = require('path'),
    app = express(),
    bodyParser = require('body-parser'),
    RateLimiter = require('express-rate-limit'),
    config = require(process.env.CONFIG_PATH)

var log

function onErr(res, msg, err, code) {
    // this is an unknown error, so we should log it.
    if (err) {
        log.error(msg, { err: err.toString() })
        msg = err.toString()
    }
    else {
        log.info(msg)
    }

    res.status(code || 500)
    res.send({
        status: 'error',
        msg,
    })
}

function onOk(res, msg) {
    log.info(msg)
    res.send({
        status: 'ok',
        msg,
    })
}

function Web(moosedb, logger) {

    log = logger

    if (config.web.proxied)
        app.enable('trust proxy')

    app.use(bodyParser.json())

    app.use('/new', new RateLimiter({
        windowMs: 60 * 1000, // 1 min
        headers: true,
        max: config.moose.maxNew || 3,
        message: JSON.stringify({
            status: 'error',
            msg: 'stop spamming moose jerk',
        }),
    }))

    app.use('/gallery', new RateLimiter({
        windowMs: 30 * 1000, // 30 sec
        delayAfter: config.moose.maxPageReq || 50,
        delayMs: 150,
        max: 0,
    }))

    var getLimiter = new RateLimiter({
        windowMs: 30 * 1000, // 30 sec
        delayAfter: config.moose.maxGetReq || 10,
        delayMs: 150,
        max: 0,
    })

    app.use('/moose', getLimiter)

    app.use('/irc', getLimiter)

    app.get('/moose/latest', (req, res) => {
        moosedb.getLatest((err, moose) => {
            if (err) return onErr(res, err, moose)
            res.send(moose)
        })
    })

    app.get('/irc/latest', (req, res) => {
        moosedb.getLatest((err, moose) => {
            if (err) return onErr(res, err, moose)
            res.type('text/irc-art')
            res.send(ircLine(moose))
        })
    })

    app.get('/moose/random', (req, res) => {
        moosedb.getRandom((err, moose) => {
            if (err) return onErr(res, err, moose)
            res.send(moose)
        })
    })

    app.get('/irc/random', (req, res) => {
        moosedb.getRandom((err, moose) => {
            if (err) return onErr(res, err, moose)
            res.type('text/irc-art')
            res.send(ircLine(moose))
        })
    })

    app.get('/moose/:moose', (req, res) => {
        moosedb.getMoose(req.params.moose, (err, moose) => {
            if (err) {
                res.status(404)
                return res.send({
                    status: 'error',
                    msg: 'no such moose',
                })
            }
            res.send(moose)
        })
    })

    app.get('/irc/:moose', (req, res) => {
        moosedb.getMoose(req.params.moose, (err, moose) => {
            if (err) {
                res.status(404)
                res.type('text/plain')
                return res.send(`no such moose: ${req.params.moose}`)
            }
            res.type('text/irc-art')
            res.send(ircLine(moose))
        })
    })

    app.get('/gallery/:age?', (req, res) => {
        var name = req.query.q || '',
            page = req.query.p || 0,
            age = req.params.age == 'oldest' ? 1 : -1

        moosedb.getGallery(name, page, age, (err, meese) => {
            if (err) return onErr(res, err, meese)
            res.send(meese)
        })
    })

    app.get('/dump', (req, res) => {
        res.sendFile(path.resolve(path.join(config.moose.db, 'dump.json')))
    })

    app.put('/new', (req, res) => {
        moosedb.newMoose(req.body, (err, msg) => {
            if (err) return onErr(res, err, msg, 422)
            onOk(res, msg)
        })
    })

    app.use('/', express.static(path.join(__dirname, '../public'), {
        index: [ 'index.html' ],
    }))

    app.listen(
        config.web.port || 7512,
        config.web.interface
    )
}

module.exports = Web
<|endoftext|>