<commit_before>'use strict';

var async   = require('async');

var logger  = require('./logger');

var SATOSHI_FACTOR = 1e8;
var SESSION_TIMEOUT = 60 * 60 * 1000;
var POLLING_RATE = 60 * 1000; // poll each minute

var db = null;

var tickerPlugin = null;
var traderPlugin = null;
var walletPlugin = null;
var idVerifierPlugin = null;

var currentlyUsedPlugins = {
  ticker: null,
  transfer: null,
  trader: null,
  idVerifier: null
};

var cachedConfig = null;
var deviceCurrency = 'USD';

var lastBalances = null;
var lastRates = {};

var balanceInterval = null;
var rateInterval = null;
var tradeInterval = null;

var tradesQueue = [];
var sessions = {};

// that's basically a constructor
exports.init = function init(databaseHandle) {
  if (!databaseHandle) {
    throw new Error('\'db\' is required');
  }

  db = databaseHandle;
};

function loadPlugin(name, config) {

  // plugins definitions
  var moduleMethods = {
    ticker: [ 'ticker' ],
    trader: [ 'balance', 'purchase', 'sell' ],
    wallet: [ 'balance', 'sendBitcoins' ],
    idVerifier: [ 'verifyUser', 'verifyTransaction' ]
  };

  var plugin = null;

  // each used plugin MUST be installed
  try {
    plugin = require('lamassu-' + name);

  } catch(_) {
    throw new Error(name + ' module is not installed. Try running \'npm install --save lamassu-' + name + '\' first');
  }

  // each plugin MUST implement those
  if (typeof plugin.SUPPORTED_MODULES !== 'undefined') {
    if(plugin.SUPPORTED_MODULES === 'string')
      plugin.SUPPORTED_MODULES = [plugin.SUPPORTED_MODULES];
  }

  if(!(plugin.SUPPORTED_MODULES instanceof Array))
    throw new Error('\'' + name + '\' fails to implement *required* \'SUPPORTED_MODULES\' constant');

  plugin.SUPPORTED_MODULES.forEach(function(moduleName) {
    moduleMethods[moduleName].forEach(function(methodName) {
      if (typeof plugin[methodName] !== 'function') {
        throw new Error('\'' + name + '\' declares \'' + moduleName + '\', but fails to implement \'' + methodName + '\' method');
      }
    });
  });

  // each plugin SHOULD implement those
  if (typeof plugin.NAME === 'undefined')
    logger.warn(new Error('\'' + name + '\' fails to implement *recommended* \'NAME\' field'));

  if (typeof plugin.config !== 'function') {
    logger.warn(new Error('\'' + name + '\' fails to implement *recommended* \'config\' method'));
    plugin.config = function() {};
  } else if (config !== null)
    plugin.config(config); // only when plugin supports it, and config is passed

  return plugin;
}

function loadOrConfigPlugin(pluginHandle, pluginType, currency, onChangeCb) {
  var currentName = cachedConfig.exchanges.plugins.current[pluginType];
  var pluginChanged = currentlyUsedPlugins[pluginType] !== currentName;

  if (currentName) { // some plugins may be disabled
    var pluginConfig = cachedConfig.exchanges.plugins.settings[currentName] || {};

    if (currency) pluginConfig.currency = currency;

    if (pluginHandle && !pluginChanged) pluginHandle.config(pluginConfig);
    else pluginHandle = loadPlugin(currentName, pluginConfig);
  }

  if (typeof onChangeCb === 'function') onChangeCb(pluginHandle, currency);

  return pluginHandle;
}

exports.configure = function configure(config) {
  if (config.exchanges.settings.lowBalanceMargin < 1) {
    throw new Error('\'settings.lowBalanceMargin\' has to be >= 1');
  }

  cachedConfig = config;
  deviceCurrency = config.exchanges.settings.currency;

  // TICKER [required] configure (or load)
  loadOrConfigPlugin(
    tickerPlugin,
    'ticker',
    deviceCurrency, // device currency
    function onTickerChange(newTicker, currency) {
      tickerPlugin = newTicker;
      pollRate();
    }
  );

  // WALLET [required] configure (or load)
  loadOrConfigPlugin(
    walletPlugin,
    'transfer',
    null,
    function onWalletChange(newWallet) {
      walletPlugin = newWallet;
      pollBalance();
    }
  );

  // TRADER [optional] configure (or load)
  traderPlugin = loadOrConfigPlugin(
    traderPlugin,
    'trade'
  );

  // ID VERIFIER [optional] configure (or load)
  idVerifierPlugin = loadOrConfigPlugin(
    idVerifierPlugin,
    'idVerifier'
  );
};
exports.getCachedConfig = function getCachedConfig() {
  return cachedConfig;
};

// This is where we record starting trade balance at the beginning
// of the user session
exports.trade = function trade(rawTrade, deviceFingerprint) {
  var sessionInfo = sessions[deviceFingerprint];

  if (!sessionInfo) {
    sessions[deviceFingerprint] = {
      timestamp: Date.now(),
      reaper: setTimeout(function() {
        delete sessions[deviceFingerprint];
      }, SESSION_TIMEOUT)
    };
  }

  tradesQueue.push({
    currency: rawTrade.currency,
    satoshis: rawTrade.satoshis
  });
};

exports.fiatBalance = function fiatBalance() {
  var rawRate = exports.getDeviceRate().rates.ask;
  var commission = cachedConfig.exchanges.settings.commission;

  if (!rawRate || !lastBalances) return null;

  // The rate is actually our commission times real rate.
  var rate = commission * rawRate;

  // `lowBalanceMargin` is our safety net. It's a number > 1, and we divide
  // all our balances by it to provide a safety margin.
  var lowBalanceMargin = cachedConfig.exchanges.settings.lowBalanceMargin;

  // `balance.transferBalance` is the balance of our transfer account (the one
  // we use to send Bitcoins to clients) in satoshis.
  var transferBalance = lastBalances.transferBalance.BTC;

  // Since `transferBalance` is in satoshis, we need to turn it into
  // bitcoins and then fiat to learn how much fiat currency we can exchange.
  //
  // Unit validity proof: [ $ ] = [ (B * 10^8) / 10^8 * $/B ]
  //                      [ $ ] = [ B * $/B ]
  //                      [ $ ] = [ $ ]
  var fiatTransferBalance = ((transferBalance / SATOSHI_FACTOR) * rate) / lowBalanceMargin;

  return fiatTransferBalance;
};

exports.sendBitcoins = function sendBitcoins(deviceFingerprint, tx, callback) {
  db.summonTransaction(deviceFingerprint, tx, function(err, txInfo) {
    if (err) return callback(err);

    if (!txInfo) {
      clearSession(deviceFingerprint);

      return walletPlugin.sendBitcoins(
        tx.toAddress,
        tx.satoshis,
        cachedConfig.exchanges.settings.transactionFee,

        function(err, txHash) {
          if (err) {
            var status = err.name === 'InsufficientFunds' ?
              'insufficientFunds' :
              'failed';

            db.reportTransactionError(tx, err.message, status);
            return callback(err);
          }

          db.completeTransaction(tx, txHash);
          pollBalance();
          callback(null, txHash);
        }
      );
    }

    // Out of bitcoins: special case
    var txErr = null;
    if (txInfo.err) {
      txErr = new Error(txInfo.err);
      if (txInfo.status === 'insufficientFunds') {
        txErr.name = 'InsufficientFunds';
      }
    }

    // transaction exists, but txHash might be null,
    // in which case ATM should continue polling
    pollBalance();
    callback(txErr, txInfo.txHash);
  });
};

/*
 * Polling livecycle
 */
exports.startPolling = function startPolling() {
  executeTrades();

  if (!balanceInterval) {
    balanceInterval = setInterval(pollBalance, POLLING_RATE);
  }

  if (!rateInterval) {
    rateInterval = setInterval(pollRate, POLLING_RATE);
  }

  // Always start trading, even if we don't have a trade exchange configured,
  // since configuration can always change in `Trader#configure`.
  // `Trader#executeTrades` returns early if we don't have a trade exchange
  // configured at the moment.
  if (!tradeInterval) {
    tradeInterval = setInterval(
      executeTrades,
      cachedConfig.exchanges.settings.tradeInterval
    );
  }
};

function pollBalance(callback) {
  logger.debug('collecting balance');

  var jobs = {
    transferBalance: walletPlugin.balance
  };

  // only add if trader is enabled
  // if (traderPlugin) {
  //   // NOTE: we would need to handle when traderCurrency!=deviceCurrency
  //   jobs.tradeBalance = traderPlugin.balance;
  // }

  async.parallel(jobs, function(err, balance) {
    if (err) {
      logger.error(err);
      return callback && callback(err);
    }

    logger.debug('Balance update:', balance);
    balance.timestamp = Date.now();
    lastBalances = balance;

    return callback && callback(null, lastBalances);
  });
}

function pollRate(callback) {
  logger.debug('polling for rates (' + tickerPlugin.NAME + ')');

  tickerPlugin.ticker(deviceCurrency, function(err, resRates) {
    if (err) {
      logger.error(err);
      return callback && callback(err);
    }

    logger.debug('got rates: %j', resRates);
    resRates.timestamp = new Date();
    lastRates = resRates;

    return callback && callback(null, lastRates);
  });
}

/*
 * Getters | Helpers
 */
function getLastRate(currency) {
  if (!lastRates) return null;

  var tmpCurrency = currency || deviceCurrency;
  if (!lastRates[tmpCurrency]) return null;

  return lastRates[tmpCurrency];
}
exports.getDeviceRate = function getDeviceRate() {
  return getLastRate(deviceCurrency);
};

exports.getBalance = function getBalance() {
  if (!lastBalances) return null;

  return lastBalances.transferBalance;
};

function clearSession(deviceFingerprint) {
  var session = sessions[deviceFingerprint];
  if (session) {
    clearTimeout(session.reaper);
    delete sessions[deviceFingerprint];
  }
}

/*
 * Trader functions
 */
function purchase(trade, callback) {
  traderPlugin.purchase(trade.satoshis, null, function(err) {
    if (err) return callback(err);
    pollBalance();
    if (typeof callback === 'function') callback();
  });
}

function consolidateTrades() {
  // NOTE: value in satoshis stays the same no matter the currency
  var consolidatedTrade = {
    currency: deviceCurrency,
    satoshis: tradesQueue.reduce(function(prev, current) {
      return prev + current.satoshis;
    }, 0)
  };

  tradesQueue = [];

  logger.debug('consolidated: ', JSON.stringify(consolidatedTrade));
  return consolidatedTrade;
}

function executeTrades() {
  if (!traderPlugin) return;

  logger.debug('checking for trades');

  var trade = consolidateTrades();

  if (trade.satoshis === 0) {
    logger.debug('rejecting 0 trade');
    return;
  }

  logger.debug('making a trade: %d', trade.satoshis / SATOSHI_FACTOR);
  purchase(trade, function(err) {
    if (err) {
      tradesQueue.push(trade);
      logger.error(err);
    }
  });
}

/*
 * ID Verifier functions
 */
exports.verifyUser = function verifyUser(data, callback) {
  idVerifierPlugin.verifyUser(data, callback);
};

exports.verifyTransaction = function verifyTransaction(data, callback) {
  idVerifierPlugin.verifyTransaction(data, callback);
};
<commit_msg>style(naming): callback name extended<commit_after>'use strict';

var async   = require('async');

var logger  = require('./logger');

var SATOSHI_FACTOR = 1e8;
var SESSION_TIMEOUT = 60 * 60 * 1000;
var POLLING_RATE = 60 * 1000; // poll each minute

var db = null;

var tickerPlugin = null;
var traderPlugin = null;
var walletPlugin = null;
var idVerifierPlugin = null;

var currentlyUsedPlugins = {
  ticker: null,
  transfer: null,
  trader: null,
  idVerifier: null
};

var cachedConfig = null;
var deviceCurrency = 'USD';

var lastBalances = null;
var lastRates = {};

var balanceInterval = null;
var rateInterval = null;
var tradeInterval = null;

var tradesQueue = [];
var sessions = {};

// that's basically a constructor
exports.init = function init(databaseHandle) {
  if (!databaseHandle) {
    throw new Error('\'db\' is required');
  }

  db = databaseHandle;
};

function loadPlugin(name, config) {

  // plugins definitions
  var moduleMethods = {
    ticker: [ 'ticker' ],
    trader: [ 'balance', 'purchase', 'sell' ],
    wallet: [ 'balance', 'sendBitcoins' ],
    idVerifier: [ 'verifyUser', 'verifyTransaction' ]
  };

  var plugin = null;

  // each used plugin MUST be installed
  try {
    plugin = require('lamassu-' + name);

  } catch(_) {
    throw new Error(name + ' module is not installed. Try running \'npm install --save lamassu-' + name + '\' first');
  }

  // each plugin MUST implement those
  if (typeof plugin.SUPPORTED_MODULES !== 'undefined') {
    if(plugin.SUPPORTED_MODULES === 'string')
      plugin.SUPPORTED_MODULES = [plugin.SUPPORTED_MODULES];
  }

  if(!(plugin.SUPPORTED_MODULES instanceof Array))
    throw new Error('\'' + name + '\' fails to implement *required* \'SUPPORTED_MODULES\' constant');

  plugin.SUPPORTED_MODULES.forEach(function(moduleName) {
    moduleMethods[moduleName].forEach(function(methodName) {
      if (typeof plugin[methodName] !== 'function') {
        throw new Error('\'' + name + '\' declares \'' + moduleName + '\', but fails to implement \'' + methodName + '\' method');
      }
    });
  });

  // each plugin SHOULD implement those
  if (typeof plugin.NAME === 'undefined')
    logger.warn(new Error('\'' + name + '\' fails to implement *recommended* \'NAME\' field'));

  if (typeof plugin.config !== 'function') {
    logger.warn(new Error('\'' + name + '\' fails to implement *recommended* \'config\' method'));
    plugin.config = function() {};
  } else if (config !== null)
    plugin.config(config); // only when plugin supports it, and config is passed

  return plugin;
}

function loadOrConfigPlugin(pluginHandle, pluginType, currency, onChangeCallback) {
  var currentName = cachedConfig.exchanges.plugins.current[pluginType];
  var pluginChanged = currentlyUsedPlugins[pluginType] !== currentName;

  if (currentName) { // some plugins may be disabled
    var pluginConfig = cachedConfig.exchanges.plugins.settings[currentName] || {};

    if (currency) pluginConfig.currency = currency;

    if (pluginHandle && !pluginChanged) pluginHandle.config(pluginConfig);
    else pluginHandle = loadPlugin(currentName, pluginConfig);
  }

  if (typeof onChangeCb === 'function') onChangeCb(pluginHandle, currency);

  return pluginHandle;
}

exports.configure = function configure(config) {
  if (config.exchanges.settings.lowBalanceMargin < 1) {
    throw new Error('\'settings.lowBalanceMargin\' has to be >= 1');
  }

  cachedConfig = config;
  deviceCurrency = config.exchanges.settings.currency;

  // TICKER [required] configure (or load)
  loadOrConfigPlugin(
    tickerPlugin,
    'ticker',
    deviceCurrency, // device currency
    function onTickerChange(newTicker, currency) {
      tickerPlugin = newTicker;
      pollRate();
    }
  );

  // WALLET [required] configure (or load)
  loadOrConfigPlugin(
    walletPlugin,
    'transfer',
    null,
    function onWalletChange(newWallet) {
      walletPlugin = newWallet;
      pollBalance();
    }
  );

  // TRADER [optional] configure (or load)
  traderPlugin = loadOrConfigPlugin(
    traderPlugin,
    'trade'
  );

  // ID VERIFIER [optional] configure (or load)
  idVerifierPlugin = loadOrConfigPlugin(
    idVerifierPlugin,
    'idVerifier'
  );
};
exports.getCachedConfig = function getCachedConfig() {
  return cachedConfig;
};

// This is where we record starting trade balance at the beginning
// of the user session
exports.trade = function trade(rawTrade, deviceFingerprint) {
  var sessionInfo = sessions[deviceFingerprint];

  if (!sessionInfo) {
    sessions[deviceFingerprint] = {
      timestamp: Date.now(),
      reaper: setTimeout(function() {
        delete sessions[deviceFingerprint];
      }, SESSION_TIMEOUT)
    };
  }

  tradesQueue.push({
    currency: rawTrade.currency,
    satoshis: rawTrade.satoshis
  });
};

exports.fiatBalance = function fiatBalance() {
  var rawRate = exports.getDeviceRate().rates.ask;
  var commission = cachedConfig.exchanges.settings.commission;

  if (!rawRate || !lastBalances) return null;

  // The rate is actually our commission times real rate.
  var rate = commission * rawRate;

  // `lowBalanceMargin` is our safety net. It's a number > 1, and we divide
  // all our balances by it to provide a safety margin.
  var lowBalanceMargin = cachedConfig.exchanges.settings.lowBalanceMargin;

  // `balance.transferBalance` is the balance of our transfer account (the one
  // we use to send Bitcoins to clients) in satoshis.
  var transferBalance = lastBalances.transferBalance.BTC;

  // Since `transferBalance` is in satoshis, we need to turn it into
  // bitcoins and then fiat to learn how much fiat currency we can exchange.
  //
  // Unit validity proof: [ $ ] = [ (B * 10^8) / 10^8 * $/B ]
  //                      [ $ ] = [ B * $/B ]
  //                      [ $ ] = [ $ ]
  var fiatTransferBalance = ((transferBalance / SATOSHI_FACTOR) * rate) / lowBalanceMargin;

  return fiatTransferBalance;
};

exports.sendBitcoins = function sendBitcoins(deviceFingerprint, tx, callback) {
  db.summonTransaction(deviceFingerprint, tx, function(err, txInfo) {
    if (err) return callback(err);

    if (!txInfo) {
      clearSession(deviceFingerprint);

      return walletPlugin.sendBitcoins(
        tx.toAddress,
        tx.satoshis,
        cachedConfig.exchanges.settings.transactionFee,

        function(err, txHash) {
          if (err) {
            var status = err.name === 'InsufficientFunds' ?
              'insufficientFunds' :
              'failed';

            db.reportTransactionError(tx, err.message, status);
            return callback(err);
          }

          db.completeTransaction(tx, txHash);
          pollBalance();
          callback(null, txHash);
        }
      );
    }

    // Out of bitcoins: special case
    var txErr = null;
    if (txInfo.err) {
      txErr = new Error(txInfo.err);
      if (txInfo.status === 'insufficientFunds') {
        txErr.name = 'InsufficientFunds';
      }
    }

    // transaction exists, but txHash might be null,
    // in which case ATM should continue polling
    pollBalance();
    callback(txErr, txInfo.txHash);
  });
};

/*
 * Polling livecycle
 */
exports.startPolling = function startPolling() {
  executeTrades();

  if (!balanceInterval) {
    balanceInterval = setInterval(pollBalance, POLLING_RATE);
  }

  if (!rateInterval) {
    rateInterval = setInterval(pollRate, POLLING_RATE);
  }

  // Always start trading, even if we don't have a trade exchange configured,
  // since configuration can always change in `Trader#configure`.
  // `Trader#executeTrades` returns early if we don't have a trade exchange
  // configured at the moment.
  if (!tradeInterval) {
    tradeInterval = setInterval(
      executeTrades,
      cachedConfig.exchanges.settings.tradeInterval
    );
  }
};

function pollBalance(callback) {
  logger.debug('collecting balance');

  var jobs = {
    transferBalance: walletPlugin.balance
  };

  // only add if trader is enabled
  // if (traderPlugin) {
  //   // NOTE: we would need to handle when traderCurrency!=deviceCurrency
  //   jobs.tradeBalance = traderPlugin.balance;
  // }

  async.parallel(jobs, function(err, balance) {
    if (err) {
      logger.error(err);
      return callback && callback(err);
    }

    logger.debug('Balance update:', balance);
    balance.timestamp = Date.now();
    lastBalances = balance;

    return callback && callback(null, lastBalances);
  });
}

function pollRate(callback) {
  logger.debug('polling for rates (' + tickerPlugin.NAME + ')');

  tickerPlugin.ticker(deviceCurrency, function(err, resRates) {
    if (err) {
      logger.error(err);
      return callback && callback(err);
    }

    logger.debug('got rates: %j', resRates);
    resRates.timestamp = new Date();
    lastRates = resRates;

    return callback && callback(null, lastRates);
  });
}

/*
 * Getters | Helpers
 */
function getLastRate(currency) {
  if (!lastRates) return null;

  var tmpCurrency = currency || deviceCurrency;
  if (!lastRates[tmpCurrency]) return null;

  return lastRates[tmpCurrency];
}
exports.getDeviceRate = function getDeviceRate() {
  return getLastRate(deviceCurrency);
};

exports.getBalance = function getBalance() {
  if (!lastBalances) return null;

  return lastBalances.transferBalance;
};

function clearSession(deviceFingerprint) {
  var session = sessions[deviceFingerprint];
  if (session) {
    clearTimeout(session.reaper);
    delete sessions[deviceFingerprint];
  }
}

/*
 * Trader functions
 */
function purchase(trade, callback) {
  traderPlugin.purchase(trade.satoshis, null, function(err) {
    if (err) return callback(err);
    pollBalance();
    if (typeof callback === 'function') callback();
  });
}

function consolidateTrades() {
  // NOTE: value in satoshis stays the same no matter the currency
  var consolidatedTrade = {
    currency: deviceCurrency,
    satoshis: tradesQueue.reduce(function(prev, current) {
      return prev + current.satoshis;
    }, 0)
  };

  tradesQueue = [];

  logger.debug('consolidated: ', JSON.stringify(consolidatedTrade));
  return consolidatedTrade;
}

function executeTrades() {
  if (!traderPlugin) return;

  logger.debug('checking for trades');

  var trade = consolidateTrades();

  if (trade.satoshis === 0) {
    logger.debug('rejecting 0 trade');
    return;
  }

  logger.debug('making a trade: %d', trade.satoshis / SATOSHI_FACTOR);
  purchase(trade, function(err) {
    if (err) {
      tradesQueue.push(trade);
      logger.error(err);
    }
  });
}

/*
 * ID Verifier functions
 */
exports.verifyUser = function verifyUser(data, callback) {
  idVerifierPlugin.verifyUser(data, callback);
};

exports.verifyTransaction = function verifyTransaction(data, callback) {
  idVerifierPlugin.verifyTransaction(data, callback);
};
<|endoftext|>


<commit_before>'use strict';

var utils = require('./utils');
var plugins = require('lazy-cache')(require);
var fn = require;
require = plugins;

/**
 * Lazily required module dependencies
 */

require('assemble-loader', 'loader');
require('base-argv', 'argv');
require('base-cli-process', 'cli');
require('base-config', 'config');
require('base-runtimes', 'runtimes');
require = fn;

/**
 * Add logging methods
 */

plugins.logger = function(options) {
  return function() {

    function logger(prop, color) {
      color = color || 'dim';
      return function(msg) {
        var rest = [].slice.call(arguments, 1);
        return console.log
          .bind(console, utils.log.timestamp + (prop ? (' ' + utils.log[prop]) : ''))
          .apply(console, [utils.log[color](msg)].concat(rest));
      };
    };

    Object.defineProperty(this, 'log', {
      configurable: true,
      get: function() {
        function log() {
          return console.log.apply(console, arguments);
        }
        log.path = function(msg) {
          return logger(null, 'dim').apply(null, arguments);
        };
        log.warn = function(msg) {
          return logger('warning', 'yellow').apply(null, arguments);
        };
        log.success = function() {
          return logger('success', 'green').apply(null, arguments);
        };

        log.info = function() {
          return logger('info', 'cyan').apply(null, arguments);
        };

        log.error = function() {
          return logger('error', 'red').apply(null, arguments);
        };
        log.__proto__ = utils.log;
        return log;
      }
    });
  }
};

/**
 * Expose `plugins`
 */

module.exports = plugins;
<commit_msg>adds `log.time`<commit_after>'use strict';

var utils = require('./utils');
var plugins = require('lazy-cache')(require);
var fn = require;
require = plugins;

/**
 * Lazily required module dependencies
 */

require('assemble-loader', 'loader');
require('base-argv', 'argv');
require('base-cli-process', 'cli');
require('base-config', 'config');
require('base-runtimes', 'runtimes');
require = fn;

/**
 * Add logging methods
 */

plugins.logger = function(options) {
  return function() {

    function logger(prop, color) {
      color = color || 'dim';
      return function(msg) {
        var rest = [].slice.call(arguments, 1);
        return console.log
          .bind(console, utils.log.timestamp + (prop ? (' ' + utils.log[prop]) : ''))
          .apply(console, [utils.log[color](msg)].concat(rest));
      };
    };

    Object.defineProperty(this, 'log', {
      configurable: true,
      get: function() {
        function log() {
          return console.log.apply(console, arguments);
        }
        log.path = function(msg) {
          return logger(null, 'dim').apply(null, arguments);
        };
        log.time = function(msg) {
          return logger(null, 'dim').apply(null, arguments);
        };
        log.warn = function(msg) {
          return logger('warning', 'yellow').apply(null, arguments);
        };
        log.success = function() {
          return logger('success', 'green').apply(null, arguments);
        };

        log.info = function() {
          return logger('info', 'cyan').apply(null, arguments);
        };

        log.error = function() {
          return logger('error', 'red').apply(null, arguments);
        };
        log.__proto__ = utils.log;
        return log;
      }
    });
  };
};

/**
 * Expose `plugins`
 */

module.exports = plugins;
<|endoftext|>


<commit_before>'use strict'

//----------------
// Includes: Self
//----------------
var dataMap = require('./data-map')

//----------
// Includes
//----------
var chalk = require('chalk')
var events = require('events')
var hid = require('node-hid')

//-----------
// Variables
//-----------
var dataPrev = Array(12)
var device = ''
var eventEmitter = new events.EventEmitter()
var ledPrev = []
var memoryPrev = {
    'wheel': {
        'turn': 50,
        'shift_left': 0,
        'shift_right': 0,
        'dpad': 0,
        'button_x': 0,
        'button_square': 0,
        'button_triangle': 0,
        'button_circle': 0,
        'button_l2': 0,
        'button_r2': 0,
        'button_l3': 0,
        'button_r3': 0,
        'button_plus': 0,
        'button_minus': 0,
        'spinner': 0,
        'button_spinner': 0,
        'button_share': 0,
        'button_option': 0,
        'button_playstation': 0
    },
    'shifter': {
        'gear': 0
    },
    'pedals': {
        'gas'   : 0,
        'brake' : 0,
        'clutch': 0
    }
}
var options = {
    'autocenter': true,
    'debug': false,
    'range': 900
}

//-----------
// Functions
//-----------
function clone(obj) {
    /*
    Clone an object.
    @param   {Object}  obj  Object to clone.
    @return  {Object}
    */
    if (obj === null || typeof obj !== 'object') {
        return obj
    }

    var temp = obj.constructor()

    for (var key in obj) {
        temp[key] = clone(obj[key])
    }

    return temp
} // clone

function connect(odo, callback) { // Constable Odo takes many forms.
    /*
    Connect to a Logitech G29 wheel.
    @param   {Object, Function}  odo       Options object or callback function.
    @param   {Function}          callback  Callback function.
    */
    if (typeof odo === 'function') {
        callback = odo
    } else {
        userOptions(odo)
    }

    callback = (typeof callback === 'function') ? callback : function() {}

    device = new hid.HID(findWheel())

    device.read(function(err, data) {
        if (err) {
            if (options.debug) {
                console.log(chalk.red('connect -> Error reading from device.'), err)
            }
            callback(err)
        } else {
            forceOff()

            if (data.length === 12) {
                // wheel is already in high precision mode

                if (options.debug) {
                    console.log(chalk.cyan('connect -> Wheel already in high precision mode.'))
                }

                listen(true, callback)
            } else {
                // wheel is not in high precision mode

                if (options.debug) {
                    console.log(chalk.cyan('connect -> Initing'))
                }

                try {
                    // G29 Wheel init from - https://github.com/torvalds/linux/blob/master/drivers/hid/hid-lg4ff.c
                    device.write([0x00, 0xf8, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00])
                    device.write([0x00, 0xf8, 0x09, 0x05, 0x01, 0x01, 0x00, 0x00])

                    // wait for wheel to finish calibrating
                    setTimeout(function() {
                        listen(false, callback)
                    }, 8000)
                } catch(err) {
                    callback(err)
                }
            }
        }
    })

    forceConstant(1) // move the wheel to generate a read event
} // connect

function disconnect() {
    /*
    Disconnect in preparation to connect again or to allow other software to use the wheel.
    */
    device.close()
} // disconnect

function findWheel() {
    /*
    Return the USB location of a Logitech G29 wheel.
    @return  {String}  devicePath  USB path like: USB_046d_c294_fa120000
    */
    var devices = hid.devices()
    var devicePath = ''

    for (var i in devices) {
        // devices[i].product will be set to 'G29 Driving Force Racing Wheel' on Windows and Mac. Linux will not have this key.
        // devices[i].interface should be 0 on Windows and Linux.
        // devices[i].usagePage should be 1 on Windows and Mac.
        if (devices[i].vendorId === 1133 && devices[i].productId === 49743 &&
            (devices[i].interface === 0 || devices[i].usagePage === 1)) {
            devicePath = devices[i].path
            break
        }
    }

    if (devicePath === '') {
        if (options.debug) {
            console.log(chalk.yellow('findWheel -> Oops, could not find a G29 Wheel. Is it plugged in?\n'))
            process.exit()
        }
    } else if (options.debug) {
        console.log(chalk.cyan('findWheel -> Found G29 Wheel at ') + devicePath)
    }

    return devicePath
} // findWheel

function on(str, func) {
    return eventEmitter.on(str, func)
} // on

function once(str, func) {
    return eventEmitter.once(str, func)
} // once

function relay(data) {
    /*
    Relay low level commands directly to the hardware.
    @param  {Object}  data  Array of data to write. For example: [0x00, 0xf8, 0x12, 0x1f, 0x00, 0x00, 0x00, 0x01]
    */
    if (Array.isArray(data)) {
        device.write(data)
    }
} // relay

function setRange() {
    /*
    Set wheel range.
    */
    if (options.range < 270) {
        options.range = 270
    }

    if (options.range > 900) {
        options.range = 900
    }

    var range1 = options.range & 0x00ff
    var range2 = (options.range & 0xff00) >> 8

    device.write([0x00, 0xf8, 0x81, range1, range2, 0x00, 0x00, 0x00])
} // setRange

function userOptions(opt) {
    /*
    Set user options.
    @param  {Object}  opt   Options object originally passed into the connect function.
    */
    if (typeof opt !== 'object') return;

    for (var i in options) {
        if (opt.hasOwnProperty(i)) {
            options[i] = opt[i]
        }
    }

    if (options.debug) {
        console.log(chalk.cyan('userOptions -> '), options)
    }
} // userOptions

//----------------
// Function: LEDs
//----------------
function leds(setting) {
    /*
    Control the shift indicator LEDs using a variety of convience methods.
    @param  {*}  setting  String, Number, or Array setting. Optional. See API documentation for more info.
    */

    // no setting
    if (typeof setting === 'undefined') {
        setting = []
    }

    // percent based settings
    if (typeof setting === 'number') {
        setting = Math.round(setting * 100)

        if (setting > 84) {
            setting = '11111'
        } else if (setting > 69) {
            setting = '1111'
        } else if (setting > 39) {
            setting = '111'
        } else if (setting > 19) {
            setting = '11'
        } else if (setting > 4) {
            setting = '1'
        } else {
            setting = ''
        }
    }

    // string based settings
    if (typeof setting === 'string') {
        setting = setting.split('')
    }

    // array based settings
    if (Array.isArray(setting)) {
        if (ledPrev === setting) {
            return
        }

        var ledValues = [1, 2, 4, 8, 16]

        var ledArray = setting

        // remove any extra elements
        ledArray.splice(5, ledArray.length - 5)

        var len = ledArray.length

        setting = 0

        for (var i = 0; i < len; i++) {
            if (parseInt(ledArray[i]) === 1) {
                setting = setting + ledValues[i]
            }
        }

        /*
        Setting should be a number from 0 to 31

            From outside in, mirrored on each side.

            0 = No LEDs
            1 = Green One
            2 = Green Two
            4 = Orange One
            8 = Orange Two
            16 = Red

            31 = All LEDs
        */

        try {
            device.write([0x00, 0xf8, 0x12, setting, 0x00, 0x00, 0x00, 0x01])

            // update global variable for next time
            ledPrev = setting
        } catch(err) {
            // do nothing
        }
    }
} // leds

//------------------
// Functions: Force
//------------------
function autoCenter() {
    /*
    Set wheel autocentering based on existing options.
    */
    var option = options.autocenter

    if (option) {
        // auto-center on
        device.write([0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])

        if (Array.isArray(option) && option.length === 2) {
            // custom auto-center

            // byte 3-4 is effect strength, 0x00 to 0x0f
            option[0] = Math.round(option[0] * 15)

            // byte 5 is the rate the effect strength rises as the wheel turns, 0x00 to 0xff
            option[1] = Math.round(option[1] * 255)

            device.write([0x00, 0xfe, 0x0d, option[0], option[0], option[1], 0x00, 0x00, 0x00])
        } else {
            // use default strength profile
            device.write([0x00, 0xfe, 0x0d, 0x07, 0x07, 0xff, 0x00, 0x00, 0x00])
        }
    } else {
        // auto-center off
        device.write([0x00, 0xf5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
    }
} // autoCenter

function forceConstant(number) {
    /*
    Set or disable a constant force effect.
    @param  {Number}  number  Number between 0 and 1. Optional.
    */
    if (typeof number === 'undefined') number = 0.5

    if (number === 0.5) {
        forceOff(1)
        return
    }

    number = Math.round(Math.abs(number - 1) * 255)

    device.write([0x00, 0x11, 0x00, number, 0x00, 0x00, 0x00, 0x00])
} // forceConstant

function forceFriction(number) {
    /*
    Set or disable the ammount of friction present when turning the wheel.
    @param  {Number}  number  Number between 0 and 1. Optional.
    */
    if (typeof number === 'undefined') number = 0

    if (number === 0) {
        forceOff(2)
        return
    }

    number = Math.round(number * 15)

    device.write([0x00, 0x21, 0x02, number, 0x00, number, 0x00, 0x00])
} // forceFriction

function forceOff(slot) {
    /*
    Turn off all force effects except auto-centering.
    @param  {Number}  slot  Number between 0 and 4. Optional.
    */
    // Great info at http://wiibrew.org/wiki/Logitech_USB_steering_wheel, especially about writing to more than one effect slot.
    if (typeof slot === 'undefined') {
        slot = 0xf3
    } else {
        if (slot === 0) {
            slot = 0xf3
        } else {
            slot = parseInt('0x' + slot + '0')
        }
    }

    // turn off effects (except for auto-center)
    device.write([0x00, slot, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
} // forceOff

//------------------
// Function: Listen
//------------------
function listen(ready, callback) {
    /*
    @param  {Boolean}   ready     True if the wheel is ready for commands. Optional.
    @param  {Function}  callback  Optional callback function.
    */
    if (!ready) {
        device.close()
        device = new hid.HID(findWheel())
    }

    setRange()
    autoCenter()

    device.on("data", function(data) {
        // reset memory
        var memory = clone(memoryPrev)
        var memoryCache = clone(memoryPrev)

        var dataDiffPositions = []

        // find out if anything has changed since the last event
        var dataLength = data.length
        for (var i = 0; i < dataLength; i++) {
            if (data[i] !== dataPrev[i]) {
                dataDiffPositions.push(i)
            }
        }

        if (dataDiffPositions.length === 0) {
            return
        }

        memory = dataMap(dataDiffPositions, data, memory)

        //-------------------------
        // Figure out what changed
        //-------------------------
        var memoryDiff = {}
        var count = 0

        for (var o in memoryCache) {
            for (var y in memory[o]) {
                if (memory[o][y] != memoryCache[o][y]) {
                    if (!memoryDiff.hasOwnProperty(o)) {
                        memoryDiff[o] = {}
                    }
                    eventEmitter.emit(o + '-' + y, memory[o][y]) // for example, wheel-turn
                    memoryDiff[o][y] = memory[o][y]
                    count = count + 1
                }
            }
        }

        if (count > 0) {
            if (options.debug) {
                console.log(memoryDiff)
            }

            // emit changes only
            eventEmitter.emit('changes', memoryDiff)
        }

        // emit everything in all event
        eventEmitter.emit('all', memory)

        // emit raw data
        eventEmitter.emit('data', data)

        // set global variables for next event
        memoryPrev = memory
        dataPrev = data
    })

    device.on("error", function(err) {
        if (options.debug) {
            console.log(chalk.red('device error -> '), JSON.stringify(err), err)
        }
    })

    leds(0)

    if (options.debug) {
        console.log(chalk.cyan('listen -> listening'))
    }

    callback(null)
} // listen

//---------
// Exports
//---------
module.exports.connect = connect
module.exports.disconnect = disconnect

// events
module.exports.emitter = eventEmitter
module.exports.on = on
module.exports.once = once

// leds
module.exports.leds = leds

// force
module.exports.forceConstant = forceConstant
module.exports.forceFriction = forceFriction
module.exports.forceOff = forceOff

// advanced
module.exports.relay = relay
<commit_msg>os compatibility<commit_after>'use strict'

//----------------
// Includes: Self
//----------------
var dataMap = require('./data-map')

//----------
// Includes
//----------
var chalk = require('chalk')
var events = require('events')
var hid = require('node-hid')
var os = require('os')

//-----------
// Variables
//-----------
var dataPrev = Array(12)
var device = ''
var eventEmitter = new events.EventEmitter()
var ledPrev = []
var memoryPrev = {
    'wheel': {
        'turn': 50,
        'shift_left': 0,
        'shift_right': 0,
        'dpad': 0,
        'button_x': 0,
        'button_square': 0,
        'button_triangle': 0,
        'button_circle': 0,
        'button_l2': 0,
        'button_r2': 0,
        'button_l3': 0,
        'button_r3': 0,
        'button_plus': 0,
        'button_minus': 0,
        'spinner': 0,
        'button_spinner': 0,
        'button_share': 0,
        'button_option': 0,
        'button_playstation': 0
    },
    'shifter': {
        'gear': 0
    },
    'pedals': {
        'gas'   : 0,
        'brake' : 0,
        'clutch': 0
    }
}
var options = {
    'autocenter': true,
    'debug': false,
    'range': 900
}
var platform = os.platform()
var prependWrite = false

//-------------
// OS Specific
//-------------
if (platform === 'win32' || platform === 'win64') {
    prependWrite = true
}

//-----------
// Functions
//-----------
function clone(obj) {
    /*
    Clone an object.
    @param   {Object}  obj  Object to clone.
    @return  {Object}
    */
    if (obj === null || typeof obj !== 'object') {
        return obj
    }

    var temp = obj.constructor()

    for (var key in obj) {
        temp[key] = clone(obj[key])
    }

    return temp
} // clone

function connect(odo, callback) { // Constable Odo takes many forms.
    /*
    Connect to a Logitech G29 wheel.
    @param   {Object, Function}  odo       Options object or callback function.
    @param   {Function}          callback  Callback function.
    */
    if (typeof odo === 'function') {
        callback = odo
    } else {
        userOptions(odo)
    }

    callback = (typeof callback === 'function') ? callback : function() {}

    device = new hid.HID(findWheel())

    device.read(function(err, data) {
        if (err) {
            if (options.debug) {
                console.log(chalk.red('connect -> Error reading from device.'), err)
            }
            callback(err)
        } else {
            forceOff()

            if (data.length === 12) {
                // wheel is already in high precision mode

                if (options.debug) {
                    console.log(chalk.cyan('connect -> Wheel already in high precision mode.'))
                }

                listen(true, callback)
            } else {
                // wheel is not in high precision mode

                if (options.debug) {
                    console.log(chalk.cyan('connect -> Initing'))
                }

                try {
                    // G29 Wheel init from - https://github.com/torvalds/linux/blob/master/drivers/hid/hid-lg4ff.c
                    relayOS([0xf8, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00])
                    relayOS([0xf8, 0x09, 0x05, 0x01, 0x01, 0x00, 0x00])

                    // wait for wheel to finish calibrating
                    setTimeout(function() {
                        listen(false, callback)
                    }, 8000)
                } catch(err) {
                    callback(err)
                }
            }
        }
    })

    forceConstant(1) // move the wheel to generate a read event
} // connect

function disconnect() {
    /*
    Disconnect in preparation to connect again or to allow other software to use the wheel.
    */
    device.close()
} // disconnect

function findWheel() {
    /*
    Return the USB location of a Logitech G29 wheel.
    @return  {String}  devicePath  USB path like: USB_046d_c294_fa120000
    */
    var devices = hid.devices()
    var devicePath = ''

    for (var i in devices) {
        // devices[i].product will be set to 'G29 Driving Force Racing Wheel' on Windows and Mac. Linux will not have this key.
        // devices[i].interface should be 0 on Windows and Linux.
        // devices[i].usagePage should be 1 on Windows and Mac.
        if (devices[i].vendorId === 1133 && devices[i].productId === 49743 &&
            (devices[i].interface === 0 || devices[i].usagePage === 1)) {
            devicePath = devices[i].path
            break
        }
    }

    if (devicePath === '') {
        if (options.debug) {
            console.log(chalk.yellow('findWheel -> Oops, could not find a G29 Wheel. Is it plugged in?\n'))
            process.exit()
        }
    } else if (options.debug) {
        console.log(chalk.cyan('findWheel -> Found G29 Wheel at ') + devicePath)
    }

    return devicePath
} // findWheel

function on(str, func) {
    return eventEmitter.on(str, func)
} // on

function once(str, func) {
    return eventEmitter.once(str, func)
} // once

function relay(data) {
    /*
    Relay low level commands directly to the hardware.
    @param  {Object}  data  Array of data to write. For example: [0x00, 0xf8, 0x12, 0x1f, 0x00, 0x00, 0x00, 0x01]
    */
    if (Array.isArray(data)) {
        device.write(data)
    }
} // relay

function relayOS(data) {
    /*
    Relay low level commands directly to the hardware after applying OS specific tweaks, if needed.
    @param  {Object}  data  Array of data to write. For example: [0xf8, 0x12, 0x1f, 0x00, 0x00, 0x00, 0x01]
    */
    if (prependWrite) {
        data.unshift(0x00)
    }

    device.write(data)
}

function setRange() {
    /*
    Set wheel range.
    */
    if (options.range < 270) {
        options.range = 270
    }

    if (options.range > 900) {
        options.range = 900
    }

    var range1 = options.range & 0x00ff
    var range2 = (options.range & 0xff00) >> 8

    relayOS([0xf8, 0x81, range1, range2, 0x00, 0x00, 0x00])
} // setRange

function userOptions(opt) {
    /*
    Set user options.
    @param  {Object}  opt   Options object originally passed into the connect function.
    */
    if (typeof opt !== 'object') return;

    for (var i in options) {
        if (opt.hasOwnProperty(i)) {
            options[i] = opt[i]
        }
    }

    if (options.debug) {
        console.log(chalk.cyan('userOptions -> '), options)
    }
} // userOptions

//----------------
// Function: LEDs
//----------------
function leds(setting) {
    /*
    Control the shift indicator LEDs using a variety of convience methods.
    @param  {*}  setting  String, Number, or Array setting. Optional. See API documentation for more info.
    */

    // no setting
    if (typeof setting === 'undefined') {
        setting = []
    }

    // percent based settings
    if (typeof setting === 'number') {
        setting = Math.round(setting * 100)

        if (setting > 84) {
            setting = '11111'
        } else if (setting > 69) {
            setting = '1111'
        } else if (setting > 39) {
            setting = '111'
        } else if (setting > 19) {
            setting = '11'
        } else if (setting > 4) {
            setting = '1'
        } else {
            setting = ''
        }
    }

    // string based settings
    if (typeof setting === 'string') {
        setting = setting.split('')
    }

    // array based settings
    if (Array.isArray(setting)) {
        if (ledPrev === setting) {
            return
        }

        var ledValues = [1, 2, 4, 8, 16]

        var ledArray = setting

        // remove any extra elements
        ledArray.splice(5, ledArray.length - 5)

        var len = ledArray.length

        setting = 0

        for (var i = 0; i < len; i++) {
            if (parseInt(ledArray[i]) === 1) {
                setting = setting + ledValues[i]
            }
        }

        /*
        Setting should be a number from 0 to 31

            From outside in, mirrored on each side.

            0 = No LEDs
            1 = Green One
            2 = Green Two
            4 = Orange One
            8 = Orange Two
            16 = Red

            31 = All LEDs
        */

        try {
            relayOS([0xf8, 0x12, setting, 0x00, 0x00, 0x00, 0x01])

            // update global variable for next time
            ledPrev = setting
        } catch(err) {
            // do nothing
        }
    }
} // leds

//------------------
// Functions: Force
//------------------
function autoCenter() {
    /*
    Set wheel autocentering based on existing options.
    */
    var option = options.autocenter

    if (option) {
        // auto-center on
        relayOS([0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])

        if (Array.isArray(option) && option.length === 2) {
            // custom auto-center

            // byte 3-4 is effect strength, 0x00 to 0x0f
            option[0] = Math.round(option[0] * 15)

            // byte 5 is the rate the effect strength rises as the wheel turns, 0x00 to 0xff
            option[1] = Math.round(option[1] * 255)

            relayOS([0xfe, 0x0d, option[0], option[0], option[1], 0x00, 0x00, 0x00])
        } else {
            // use default strength profile
            relayOS([0xfe, 0x0d, 0x07, 0x07, 0xff, 0x00, 0x00, 0x00])
        }
    } else {
        // auto-center off
        relayOS([0xf5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
    }
} // autoCenter

function forceConstant(number) {
    /*
    Set or disable a constant force effect.
    @param  {Number}  number  Number between 0 and 1. Optional.
    */
    if (typeof number === 'undefined') number = 0.5

    if (number === 0.5) {
        forceOff(1)
        return
    }

    number = Math.round(Math.abs(number - 1) * 255)

    relayOS([0x11, 0x00, number, 0x00, 0x00, 0x00, 0x00])
} // forceConstant

function forceFriction(number) {
    /*
    Set or disable the ammount of friction present when turning the wheel.
    @param  {Number}  number  Number between 0 and 1. Optional.
    */
    if (typeof number === 'undefined') number = 0

    if (number === 0) {
        forceOff(2)
        return
    }

    number = Math.round(number * 15)

    relayOS([0x21, 0x02, number, 0x00, number, 0x00, 0x00])
} // forceFriction

function forceOff(slot) {
    /*
    Turn off all force effects except auto-centering.
    @param  {Number}  slot  Number between 0 and 4. Optional.
    */
    // Great info at http://wiibrew.org/wiki/Logitech_USB_steering_wheel, especially about writing to more than one effect slot.
    if (typeof slot === 'undefined') {
        slot = 0xf3
    } else {
        if (slot === 0) {
            slot = 0xf3
        } else {
            slot = parseInt('0x' + slot + '0')
        }
    }

    // turn off effects (except for auto-center)
    relayOS([slot, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
} // forceOff

//------------------
// Function: Listen
//------------------
function listen(ready, callback) {
    /*
    @param  {Boolean}   ready     True if the wheel is ready for commands. Optional.
    @param  {Function}  callback  Optional callback function.
    */
    if (!ready) {
        device.close()
        device = new hid.HID(findWheel())
    }

    setRange()
    autoCenter()

    device.on("data", function(data) {
        // reset memory
        var memory = clone(memoryPrev)
        var memoryCache = clone(memoryPrev)

        var dataDiffPositions = []

        // find out if anything has changed since the last event
        var dataLength = data.length
        for (var i = 0; i < dataLength; i++) {
            if (data[i] !== dataPrev[i]) {
                dataDiffPositions.push(i)
            }
        }

        if (dataDiffPositions.length === 0) {
            return
        }

        memory = dataMap(dataDiffPositions, data, memory)

        //-------------------------
        // Figure out what changed
        //-------------------------
        var memoryDiff = {}
        var count = 0

        for (var o in memoryCache) {
            for (var y in memory[o]) {
                if (memory[o][y] != memoryCache[o][y]) {
                    if (!memoryDiff.hasOwnProperty(o)) {
                        memoryDiff[o] = {}
                    }
                    eventEmitter.emit(o + '-' + y, memory[o][y]) // for example, wheel-turn
                    memoryDiff[o][y] = memory[o][y]
                    count = count + 1
                }
            }
        }

        if (count > 0) {
            if (options.debug) {
                console.log(memoryDiff)
            }

            // emit changes only
            eventEmitter.emit('changes', memoryDiff)
        }

        // emit everything in all event
        eventEmitter.emit('all', memory)

        // emit raw data
        eventEmitter.emit('data', data)

        // set global variables for next event
        memoryPrev = memory
        dataPrev = data
    })

    device.on("error", function(err) {
        if (options.debug) {
            console.log(chalk.red('device error -> '), JSON.stringify(err), err)
        }
    })

    leds(0)

    if (options.debug) {
        console.log(chalk.cyan('listen -> listening'))
    }

    callback(null)
} // listen

//---------
// Exports
//---------
module.exports.connect = connect
module.exports.disconnect = disconnect

// events
module.exports.emitter = eventEmitter
module.exports.on = on
module.exports.once = once

// leds
module.exports.leds = leds

// force
module.exports.forceConstant = forceConstant
module.exports.forceFriction = forceFriction
module.exports.forceOff = forceOff

// advanced
module.exports.relay = relay
<|endoftext|>


<commit_before>'use strict';

module.exports = Nodal => {

  const async = require('async');

  let expect = require('chai').expect;

  describe('Nodal.Composer', function() {

    let db = new Nodal.Database();

    let schemaUser = {
      table: 'users',
      columns: [
        {name: 'id', type: 'serial'},
        {name: 'username', type: 'string'},
        {name: 'organization_location_id', type: 'int'},
        {name: 'created_at', type: 'datetime'},
        {name: 'updated_at', type: 'datetime'}
      ]
    };

    let schemaMembership = {
      table: 'memberships',
      columns: [
        {name: 'id', type: 'serial'},
        {name: 'user_id', type: 'int'},
        {name: 'organization_id', type: 'int'},
        {name: 'created_at', type: 'datetime'},
        {name: 'updated_at', type: 'datetime'}
      ]
    };

    let schemaOrganizationLocations = {
      table: 'organization_locations',
      columns: [
        {name: 'id', type: 'serial'},
        {name: 'organization_id', type: 'int'},
        {name: 'organization_authorization_access_code', type: 'string'},
        {name: 'location', type: 'string'},
        {name: 'created_at', type: 'datetime'},
        {name: 'updated_at', type: 'datetime'}
      ]
    };

    class User extends Nodal.Model {}

    User.setDatabase(db);
    User.setSchema(schemaUser);

    class Membership extends Nodal.Model {}

    Membership.setDatabase(db);
    Membership.setSchema(schemaMembership);
    Membership.joinsTo(User, {multiple: true, as: 'memberships'});
    Membership.joinsTo(User, {multiple: true, via: 'organization_id', name: 'organization', as: 'members'});

    class OrganizationLocations extends Nodal.Model {}

    OrganizationLocations.setDatabase(db);
    OrganizationLocations.setSchema(schemaOrganizationLocations);
    OrganizationLocations.joinsTo(User, {multiple: true, via: 'organization_id', name: 'organization', as: 'organizationLocations'});

    User.joinsTo(OrganizationLocations, {multiple: true, via: 'organization_location_id', name: 'organizationLocation', as: 'engineeringStaffMembers'});

    before(function(done) {

      db.connect(Nodal.my.Config.db.main);

      db.transaction(
        [schemaUser, schemaMembership, schemaOrganizationLocations].map(schema => {
          return [
            db.adapter.generateDropTableQuery(schema.table, true),
            db.adapter.generateCreateTableQuery(schema.table, schema.columns)
          ].join(';');
        }).join(';'),
        function(err, result) {

          expect(err).to.equal(null);

          let users = Nodal.ModelArray.from([
            new User({username: 'francis'}),
            new User({username: 'felicia'}),
            new User({username: 'gregory'}),
            new User({username: 'georgia'}),
            new User({username: 'facebook'}),
            new User({username: 'google'}),
            new User({username: 'sergey', organization_location_id: 1})
          ]);

          let memberships = Nodal.ModelArray.from([
            new Membership({user_id: 1, organization_id: 5}),
            new Membership({user_id: 2, organization_id: 5}),
            new Membership({user_id: 3, organization_id: 6}),
            new Membership({user_id: 4, organization_id: 6})
          ]);

          let organizationLocations = Nodal.ModelArray.from([
            new OrganizationLocations({organization_id: 6, organization_authorization_access_code: 'secret_password', location: 'Mountain View'})
          ]);

          async.series([
            users.saveAll.bind(users),
            memberships.saveAll.bind(memberships),
            organizationLocations.saveAll.bind(organizationLocations)
          ], (err) => {
            expect(err).to.not.exist;
            done();
          });
        }
      );

    });

    after(function(done) {

      db.close(function() {
        done();
      });

    });

    it('Should query all users (6)', function(done) {

      User.query()
        .end((err, users) => {

          expect(err).to.equal(null);
          expect(users).to.be.an.instanceOf(Nodal.ModelArray);
          expect(users.length).to.equal(7);
          done();

        });

    });

    it('Should find Georgia as a member of Google', (done) => {

      Membership.query()
        .join('user')
        .join('organization')
        .where({
          user__username: 'georgia',
          organization__username: 'google'
        })
        .end((err, memberships) => {

          expect(err).to.equal(null);
          expect(memberships).to.be.an.instanceOf(Nodal.ModelArray);
          expect(memberships.length).to.equal(1);
          expect(memberships[0].joined('user').get('username')).to.equal('georgia');
          expect(memberships[0].joined('organization').get('username')).to.equal('google');
          done();

        });

    });

    it('Should truncate joined model names when querying', (done) => {

      User.query()
        .join('memberships')
        .join('memberships__organization')
        .join('memberships__organization__organizationLocations')
        .join('memberships__organization__organizationLocations__engineeringStaffMembers')
        .where({
          username: 'georgia',
          memberships__organization__username: 'google'
        })
        .end((err, users) => {

          expect(err).to.equal(null);
          expect(users).to.be.an.instanceOf(Nodal.ModelArray);
          expect(users.length).to.equal(1);
          expect(users[0].joined('memberships').length).to.equal(1);
          expect(users[0].joined('memberships')[0].joined('organization').get('username')).to.equal('google');
          let hq = users[0].joined('memberships')[0].joined('organization').joined('organizationLocations')[0];
          expect(hq).to.exist;
          expect(hq.get('location')).to.equal('Mountain View');
          expect(hq.get('organization_authorization_access_code')).to.equal('secret_password');
          expect(hq.joined('engineeringStaffMembers').length).to.equal(1);
          expect(hq.joined('engineeringStaffMembers')[0].get('username')).to.equal('sergey');
          done();

        });

    });

    it('Should truncate joined model names when querying', (done) => {

      User.query()
        .join('members')
        .join('members__user', {username: 'georgia'}, {username: 'gregory'})
        .where({username: 'google'})
        .end((err, organizations) => {

          expect(err).to.equal(null);
          expect(organizations).to.be.an.instanceOf(Nodal.ModelArray);
          expect(organizations.length).to.equal(1);
          expect(organizations[0].joined('members').length).to.equal(2);
          expect(organizations[0].joined('members')[0].joined('user')).to.exist;
          expect(organizations[0].joined('members')[1].joined('user')).to.exist;
          done();

        });

    });

  });

};
<commit_msg>Improve test case<commit_after>'use strict';

module.exports = Nodal => {

  const async = require('async');

  let expect = require('chai').expect;

  describe('Nodal.Composer', function() {

    let db = new Nodal.Database();

    let schemaUser = {
      table: 'users',
      columns: [
        {name: 'id', type: 'serial'},
        {name: 'username', type: 'string'},
        {name: 'organization_location_id', type: 'int'},
        {name: 'created_at', type: 'datetime'},
        {name: 'updated_at', type: 'datetime'}
      ]
    };

    let schemaMembership = {
      table: 'memberships',
      columns: [
        {name: 'id', type: 'serial'},
        {name: 'user_id', type: 'int'},
        {name: 'organization_id', type: 'int'},
        {name: 'created_at', type: 'datetime'},
        {name: 'updated_at', type: 'datetime'}
      ]
    };

    let schemaOrganizationLocations = {
      table: 'organization_locations',
      columns: [
        {name: 'id', type: 'serial'},
        {name: 'organization_id', type: 'int'},
        {name: 'organization_authorization_access_code', type: 'string'},
        {name: 'location', type: 'string'},
        {name: 'created_at', type: 'datetime'},
        {name: 'updated_at', type: 'datetime'}
      ]
    };

    class User extends Nodal.Model {}

    User.setDatabase(db);
    User.setSchema(schemaUser);

    class Membership extends Nodal.Model {}

    Membership.setDatabase(db);
    Membership.setSchema(schemaMembership);
    Membership.joinsTo(User, {multiple: true, as: 'memberships'});
    Membership.joinsTo(User, {multiple: true, via: 'organization_id', name: 'organization', as: 'members'});

    class OrganizationLocations extends Nodal.Model {}

    OrganizationLocations.setDatabase(db);
    OrganizationLocations.setSchema(schemaOrganizationLocations);
    OrganizationLocations.joinsTo(User, {multiple: true, via: 'organization_id', name: 'organization', as: 'organizationLocations'});

    User.joinsTo(OrganizationLocations, {multiple: true, via: 'organization_location_id', name: 'organizationLocation', as: 'engineeringStaffMembers'});

    before(function(done) {

      db.connect(Nodal.my.Config.db.main);

      db.transaction(
        [schemaUser, schemaMembership, schemaOrganizationLocations].map(schema => {
          return [
            db.adapter.generateDropTableQuery(schema.table, true),
            db.adapter.generateCreateTableQuery(schema.table, schema.columns)
          ].join(';');
        }).join(';'),
        function(err, result) {

          expect(err).to.equal(null);

          let users = Nodal.ModelArray.from([
            new User({username: 'francis'}),
            new User({username: 'felicia'}),
            new User({username: 'gregory'}),
            new User({username: 'georgia'}),
            new User({username: 'gilliam'}),
            new User({username: 'facebook'}),
            new User({username: 'google'}),
            new User({username: 'sergey', organization_location_id: 1}),
          ]);

          let memberships = Nodal.ModelArray.from([
            new Membership({user_id: 1, organization_id: 6}),
            new Membership({user_id: 2, organization_id: 6}),
            new Membership({user_id: 3, organization_id: 7}),
            new Membership({user_id: 4, organization_id: 7}),
            new Membership({user_id: 5, organization_id: 7})
          ]);

          let organizationLocations = Nodal.ModelArray.from([
            new OrganizationLocations({organization_id: 7, organization_authorization_access_code: 'secret_password', location: 'Mountain View'})
          ]);

          async.series([
            users.saveAll.bind(users),
            memberships.saveAll.bind(memberships),
            organizationLocations.saveAll.bind(organizationLocations)
          ], (err) => {
            expect(err).to.not.exist;
            done();
          });
        }
      );

    });

    after(function(done) {

      db.close(function() {
        done();
      });

    });

    it('Should query all users (8)', function(done) {

      User.query()
        .end((err, users) => {

          expect(err).to.equal(null);
          expect(users).to.be.an.instanceOf(Nodal.ModelArray);
          expect(users.length).to.equal(8);
          done();

        });

    });

    it('Should find Georgia as a member of Google', (done) => {

      Membership.query()
        .join('user')
        .join('organization')
        .where({
          user__username: 'georgia',
          organization__username: 'google'
        })
        .end((err, memberships) => {

          expect(err).to.equal(null);
          expect(memberships).to.be.an.instanceOf(Nodal.ModelArray);
          expect(memberships.length).to.equal(1);
          expect(memberships[0].joined('user').get('username')).to.equal('georgia');
          expect(memberships[0].joined('organization').get('username')).to.equal('google');
          done();

        });

    });

    it('Should truncate joined model names when querying', (done) => {

      User.query()
        .join('memberships')
        .join('memberships__organization')
        .join('memberships__organization__organizationLocations')
        .join('memberships__organization__organizationLocations__engineeringStaffMembers')
        .where({
          username: 'georgia',
          memberships__organization__username: 'google'
        })
        .end((err, users) => {

          expect(err).to.equal(null);
          expect(users).to.be.an.instanceOf(Nodal.ModelArray);
          expect(users.length).to.equal(1);
          expect(users[0].joined('memberships').length).to.equal(1);
          expect(users[0].joined('memberships')[0].joined('organization').get('username')).to.equal('google');
          let hq = users[0].joined('memberships')[0].joined('organization').joined('organizationLocations')[0];
          expect(hq).to.exist;
          expect(hq.get('location')).to.equal('Mountain View');
          expect(hq.get('organization_authorization_access_code')).to.equal('secret_password');
          expect(hq.joined('engineeringStaffMembers').length).to.equal(1);
          expect(hq.joined('engineeringStaffMembers')[0].get('username')).to.equal('sergey');
          done();

        });

    });

    it('Should filter comparisons in nested join statements properly', (done) => {

      User.query()
        .join('members')
        .join('members__user', {username: 'georgia'}, {username: 'gregory'})
        .where({username: 'google'})
        .end((err, organizations) => {

          expect(err).to.equal(null);
          expect(organizations).to.be.an.instanceOf(Nodal.ModelArray);
          expect(organizations.length).to.equal(1);
          expect(organizations[0].joined('members').length).to.equal(3);
          expect(organizations[0].joined('members').find((member) => {
            return member.joined('user') && member.joined('user').get('username') === 'georgia';
          })).to.exist;
          expect(organizations[0].joined('members').find((member) => {
            return member.joined('user') && member.joined('user').get('username') === 'gregory';
          })).to.exist;
          expect(organizations[0].joined('members').find((member) => {
            return member.joined('user') && member.joined('user').get('username') === 'gilliam';
          })).to.not.exist;
          done();

        });

    });

  });

};
<|endoftext|>


<commit_before>var TradeOfferManager = require('./index.js');
var ETradeOfferState = TradeOfferManager.ETradeOfferState;
var EOfferFilter = TradeOfferManager.EOfferFilter;
var deepEqual = require('deep-equal');
var clone = require('clone');

/*
 * pollData is an object which has the following structure:
 *  - `offersSince` is the STANDARD unix time (Math.floor(Date.now() / 1000)) of the last known offer change
 *  - `sent` is an object whose keys are offer IDs for known offers we've sent and whose values are the last known states of those offers
 *  - `received` is the same as `sent`, for offers we've received
 *  - `toAccept` is an object whose keys are offer IDs for offers we've tried to accept, but Steam failed. Values are JavaScript unix times (in milliseconds) of the last time when we attempted to accept.
 *  - `offerData` is an object whose keys are offer IDs. Values are objects mapping arbitrary keys to arbitrary values.
 *    Some keys are reserved for offer-specific options. These are:
 *  	- `cancelTime` - The time, in milliseconds, after which the offer should be canceled automatically. Defaults to the TradeOfferManager's set cancelTime.
 *      - `pendingCancelTime` - Ditto `cancelTime`, except only for offers which are CreatedNeedsConfirmation.
 */

TradeOfferManager.prototype.doPoll = function() {
	if(this.hasShutDown) {
		// In case a race condition causes this to be called after we've shutdown
		return;
	}

	if(!this.apiKey || Date.now() - this._lastPoll < 1000) {
		// Either we don't have an API key, or we last polled less than a second ago... we shouldn't spam the API
		// Reset the timer to poll one second after the last one
		this._resetPollTimer(Date.now() - this._lastPoll);
		return;
	}

	this._lastPoll = Date.now();
	clearTimeout(this._pollTimer);

	var offersSince = 0;
	if(this.pollData.offersSince) {
		// It looks like sometimes Steam can be dumb and backdate a modified offer. We need to handle this.
		// Let's add a 30-minute buffer.
		offersSince = this.pollData.offersSince - 1800;
	}

	var fullUpdate = false;
	if (Date.now() - this._lastPollFullUpdate >= 120000) {
		fullUpdate = true;
		this._lastPollFullUpdate = Date.now();
		offersSince = 1;
	}

	this.emit('debug', 'Doing trade offer poll since ' + offersSince + (fullUpdate ? ' (full update)' : ''));
	this.getOffers(EOfferFilter.ActiveOnly, new Date(offersSince * 1000), function(err, sent, received) {
		if(err) {
			this.emit('debug', "Error getting trade offers for poll: " + err.message);
			this.emit('pollFailure', err);
			this._resetPollTimer();
			return;
		}

		var origPollData = clone(this.pollData);

		if (fullUpdate) {
			// We can only purge stuff if this is a full update; otherwise, lack of an offer's presence doesn't mean Steam forgot about it
			var trackedIds = sent.map(offerId).concat(received.map(offerId)); // OfferIDs that are active in Steam's memory
			var oldIds = Object.keys(this.pollData.sent || {}).concat(Object.keys(this.pollData.received || {})); // OfferIDs that we have in our poll data

			oldIds.forEach(function(offerID) {
				if (trackedIds.indexOf(offerID) == -1) {
					// This offer is no longer in Steam's memory. Let's clean it up.
					var found = false;

					if (this.pollData.sent && this.pollData.sent[offerID]) {
						found = found || delete this.pollData.sent[offerID];
					}

					if (this.pollData.received && this.pollData.received[offerID]) {
						found = found || delete this.pollData.received[offerID];
					}

					if (found) {
						this.emit('debug', "Cleaning up stale offer #" + offerID + " from poll data");
					}
				}
			}.bind(this));
		}

		var offers = this.pollData.sent || {};

		sent.forEach(function(offer) {
			if(!offers[offer.id]) {
				// We sent this offer, but we have no record of it! Good job Steam
				// Apparently offers can appear in the API before the send() call has returned, so we'll need to add a delay
				if(Date.now() - offer.created.getTime() > 3000) {
					this.emit('unknownOfferSent', offer);
					offers[offer.id] = offer.state;
				}
			} else if(!offer.isGlitched() && offer.state != offers[offer.id]) {
				// We sent this offer, and it has now changed state
				this.emit('sentOfferChanged', offer, offers[offer.id]);
				offers[offer.id] = offer.state;
			}

			if(offer.state == ETradeOfferState.Active) {
				// The offer is still Active, and we sent it. See if it's time to cancel it automatically.
				var cancelTime = this.cancelTime;

				// Check if this offer has a custom cancelTime
				var customCancelTime = offer.data('cancelTime');
				if(typeof customCancelTime !== 'undefined') {
					cancelTime = customCancelTime;
				}

				if(cancelTime && (Date.now() - offer.updated.getTime() >= cancelTime)) {
					offer.cancel(function(err) {
						if(!err) {
							this.emit('sentOfferCanceled', offer, 'cancelTime');
						} else {
							this.emit('debug', "Can't auto-cancel offer #" + offer.id + ": " + err.message);
						}
					}.bind(this));
				}
			}

			if(offer.state == ETradeOfferState.CreatedNeedsConfirmation && this.pendingCancelTime) {
				// The offer needs to be confirmed to be sent. Let's see if the maximum time has elapsed before we cancel it.
				var pendingCancelTime = this.pendingCancelTime;

				var customPendingCancelTime = offer.data('pendingCancelTime');
				if(typeof customPendingCancelTime !== 'undefined') {
					pendingCancelTime = customPendingCancelTime;
				}

				if(pendingCancelTime && (Date.now() - offer.created.getTime() >= pendingCancelTime))  {
					offer.cancel(function(err) {
						if(!err) {
							this.emit('sentPendingOfferCanceled', offer);
						} else {
							this.emit('debug', "Can't auto-canceling pending-confirmation offer #" + offer.id + ": " + err.message);
						}
					}.bind(this));
				}
			}
		}.bind(this));

		if(this.cancelOfferCount) {
			var sentActive = sent.filter(function(offer) {
				return offer.state == ETradeOfferState.Active;
			});

			if(sentActive.length >= this.cancelOfferCount) {
				// We have too many offers out. Let's cancel the oldest.
				// Use updated since that reflects when it was confirmed, if necessary.
				var oldest = sentActive[0];
				for(var i = 1; i < sentActive.length; i++) {
					if(sentActive[i].updated.getTime() < oldest.updated.getTime()) {
						oldest = sentActive[i];
					}
				}

				// Make sure it's old enough
				if(Date.now() - oldest.updated.getTime() >= this.cancelOfferCountMinAge) {
					oldest.cancel(function(err) {
						if(!err) {
							this.emit('sentOfferCanceled', oldest, 'cancelOfferCount');
						}
					}.bind(this));
				}
			}
		}

		this.pollData.sent = offers;
		offers = this.pollData.received || {};

		received.forEach(function(offer) {
			if (offer.isGlitched()) {
				return;
			}

			if(!offers[offer.id] && offer.state == ETradeOfferState.Active) {
				this.emit('newOffer', offer);
			} else if(offers[offer.id] && offer.state != offers[offer.id]) {
				this.emit('receivedOfferChanged', offer, offers[offer.id]);
			}

			offers[offer.id] = offer.state;
		}.bind(this));

		this.pollData.received = offers;

		// Find the latest update time
		var latest = this.pollData.offersSince || 0;
		sent.concat(received).forEach(function(offer) {
			var updated = Math.floor(offer.updated.getTime() / 1000);
			if(updated > latest) {
				latest = updated;
			}
		});

		this.pollData.offersSince = latest;

		// Check if any offers which we want to accept are still Active
		if(this.pollData.toAccept) {
			var offer, offerID;
			for(offerID in this.pollData.toAccept) {
				offer = received.filter(function(item) {
					return item.id == offerID;
				});

				if(!offer[0] || offer[0].state != ETradeOfferState.Active) {
					// It's no longer active, so we're done here
					delete this.pollData.toAccept[offerID];
					continue;
				}

				// The value of this.pollData.toAccept[offerID] is the unix time in milliseconds when we last tried to accept it
				if(Date.now() - this.pollData.toAccept[offerID] > 60000) {
					// We last tried to accept this offer over a minute ago. Try again.
					this.pollData.toAccept[offerID] = Date.now();
					offer[0].accept();
				}
			}
		}

		this.emit('pollSuccess');

		// If something has changed, emit the event
		if(!deepEqual(origPollData, this.pollData)) {
			this.emit('pollData', this.pollData);
		}

		this._resetPollTimer();
	}.bind(this));
};

function offerId(offer) {
	return String(offer.id);
}

TradeOfferManager.prototype._resetPollTimer = function(time) {
	if(time || this.pollInterval >= 1000) {
		clearTimeout(this._pollTimer);
		this._pollTimer = setTimeout(this.doPoll.bind(this), time || this.pollInterval);
	}
};
<commit_msg>Use EOfferFilter.All for full updates<commit_after>var TradeOfferManager = require('./index.js');
var ETradeOfferState = TradeOfferManager.ETradeOfferState;
var EOfferFilter = TradeOfferManager.EOfferFilter;
var deepEqual = require('deep-equal');
var clone = require('clone');

/*
 * pollData is an object which has the following structure:
 *  - `offersSince` is the STANDARD unix time (Math.floor(Date.now() / 1000)) of the last known offer change
 *  - `sent` is an object whose keys are offer IDs for known offers we've sent and whose values are the last known states of those offers
 *  - `received` is the same as `sent`, for offers we've received
 *  - `toAccept` is an object whose keys are offer IDs for offers we've tried to accept, but Steam failed. Values are JavaScript unix times (in milliseconds) of the last time when we attempted to accept.
 *  - `offerData` is an object whose keys are offer IDs. Values are objects mapping arbitrary keys to arbitrary values.
 *    Some keys are reserved for offer-specific options. These are:
 *  	- `cancelTime` - The time, in milliseconds, after which the offer should be canceled automatically. Defaults to the TradeOfferManager's set cancelTime.
 *      - `pendingCancelTime` - Ditto `cancelTime`, except only for offers which are CreatedNeedsConfirmation.
 */

TradeOfferManager.prototype.doPoll = function() {
	if(this.hasShutDown) {
		// In case a race condition causes this to be called after we've shutdown
		return;
	}

	if(!this.apiKey || Date.now() - this._lastPoll < 1000) {
		// Either we don't have an API key, or we last polled less than a second ago... we shouldn't spam the API
		// Reset the timer to poll one second after the last one
		this._resetPollTimer(Date.now() - this._lastPoll);
		return;
	}

	this._lastPoll = Date.now();
	clearTimeout(this._pollTimer);

	var offersSince = 0;
	if(this.pollData.offersSince) {
		// It looks like sometimes Steam can be dumb and backdate a modified offer. We need to handle this.
		// Let's add a 30-minute buffer.
		offersSince = this.pollData.offersSince - 1800;
	}

	var fullUpdate = false;
	if (Date.now() - this._lastPollFullUpdate >= 120000) {
		fullUpdate = true;
		this._lastPollFullUpdate = Date.now();
		offersSince = 1;
	}

	this.emit('debug', 'Doing trade offer poll since ' + offersSince + (fullUpdate ? ' (full update)' : ''));
	this.getOffers(fullUpdate ? EOfferFilter.All : EOfferFilter.ActiveOnly, new Date(offersSince * 1000), function(err, sent, received) {
		if(err) {
			this.emit('debug', "Error getting trade offers for poll: " + err.message);
			this.emit('pollFailure', err);
			this._resetPollTimer();
			return;
		}

		var origPollData = clone(this.pollData);

		if (fullUpdate) {
			// We can only purge stuff if this is a full update; otherwise, lack of an offer's presence doesn't mean Steam forgot about it
			var trackedIds = sent.map(offerId).concat(received.map(offerId)); // OfferIDs that are active in Steam's memory
			var oldIds = Object.keys(this.pollData.sent || {}).concat(Object.keys(this.pollData.received || {})); // OfferIDs that we have in our poll data

			oldIds.forEach(function(offerID) {
				if (trackedIds.indexOf(offerID) == -1) {
					// This offer is no longer in Steam's memory. Let's clean it up.
					var found = false;

					if (this.pollData.sent && this.pollData.sent[offerID]) {
						found = found || delete this.pollData.sent[offerID];
					}

					if (this.pollData.received && this.pollData.received[offerID]) {
						found = found || delete this.pollData.received[offerID];
					}

					if (found) {
						this.emit('debug', "Cleaning up stale offer #" + offerID + " from poll data");
					}
				}
			}.bind(this));
		}

		var offers = this.pollData.sent || {};

		sent.forEach(function(offer) {
			if(!offers[offer.id]) {
				// We sent this offer, but we have no record of it! Good job Steam
				// Apparently offers can appear in the API before the send() call has returned, so we'll need to add a delay
				if(Date.now() - offer.created.getTime() > 3000) {
					this.emit('unknownOfferSent', offer);
					offers[offer.id] = offer.state;
				}
			} else if(!offer.isGlitched() && offer.state != offers[offer.id]) {
				// We sent this offer, and it has now changed state
				this.emit('sentOfferChanged', offer, offers[offer.id]);
				offers[offer.id] = offer.state;
			}

			if(offer.state == ETradeOfferState.Active) {
				// The offer is still Active, and we sent it. See if it's time to cancel it automatically.
				var cancelTime = this.cancelTime;

				// Check if this offer has a custom cancelTime
				var customCancelTime = offer.data('cancelTime');
				if(typeof customCancelTime !== 'undefined') {
					cancelTime = customCancelTime;
				}

				if(cancelTime && (Date.now() - offer.updated.getTime() >= cancelTime)) {
					offer.cancel(function(err) {
						if(!err) {
							this.emit('sentOfferCanceled', offer, 'cancelTime');
						} else {
							this.emit('debug', "Can't auto-cancel offer #" + offer.id + ": " + err.message);
						}
					}.bind(this));
				}
			}

			if(offer.state == ETradeOfferState.CreatedNeedsConfirmation && this.pendingCancelTime) {
				// The offer needs to be confirmed to be sent. Let's see if the maximum time has elapsed before we cancel it.
				var pendingCancelTime = this.pendingCancelTime;

				var customPendingCancelTime = offer.data('pendingCancelTime');
				if(typeof customPendingCancelTime !== 'undefined') {
					pendingCancelTime = customPendingCancelTime;
				}

				if(pendingCancelTime && (Date.now() - offer.created.getTime() >= pendingCancelTime))  {
					offer.cancel(function(err) {
						if(!err) {
							this.emit('sentPendingOfferCanceled', offer);
						} else {
							this.emit('debug', "Can't auto-canceling pending-confirmation offer #" + offer.id + ": " + err.message);
						}
					}.bind(this));
				}
			}
		}.bind(this));

		if(this.cancelOfferCount) {
			var sentActive = sent.filter(function(offer) {
				return offer.state == ETradeOfferState.Active;
			});

			if(sentActive.length >= this.cancelOfferCount) {
				// We have too many offers out. Let's cancel the oldest.
				// Use updated since that reflects when it was confirmed, if necessary.
				var oldest = sentActive[0];
				for(var i = 1; i < sentActive.length; i++) {
					if(sentActive[i].updated.getTime() < oldest.updated.getTime()) {
						oldest = sentActive[i];
					}
				}

				// Make sure it's old enough
				if(Date.now() - oldest.updated.getTime() >= this.cancelOfferCountMinAge) {
					oldest.cancel(function(err) {
						if(!err) {
							this.emit('sentOfferCanceled', oldest, 'cancelOfferCount');
						}
					}.bind(this));
				}
			}
		}

		this.pollData.sent = offers;
		offers = this.pollData.received || {};

		received.forEach(function(offer) {
			if (offer.isGlitched()) {
				return;
			}

			if(!offers[offer.id] && offer.state == ETradeOfferState.Active) {
				this.emit('newOffer', offer);
			} else if(offers[offer.id] && offer.state != offers[offer.id]) {
				this.emit('receivedOfferChanged', offer, offers[offer.id]);
			}

			offers[offer.id] = offer.state;
		}.bind(this));

		this.pollData.received = offers;

		// Find the latest update time
		var latest = this.pollData.offersSince || 0;
		sent.concat(received).forEach(function(offer) {
			var updated = Math.floor(offer.updated.getTime() / 1000);
			if(updated > latest) {
				latest = updated;
			}
		});

		this.pollData.offersSince = latest;

		// Check if any offers which we want to accept are still Active
		if(this.pollData.toAccept) {
			var offer, offerID;
			for(offerID in this.pollData.toAccept) {
				offer = received.filter(function(item) {
					return item.id == offerID;
				});

				if(!offer[0] || offer[0].state != ETradeOfferState.Active) {
					// It's no longer active, so we're done here
					delete this.pollData.toAccept[offerID];
					continue;
				}

				// The value of this.pollData.toAccept[offerID] is the unix time in milliseconds when we last tried to accept it
				if(Date.now() - this.pollData.toAccept[offerID] > 60000) {
					// We last tried to accept this offer over a minute ago. Try again.
					this.pollData.toAccept[offerID] = Date.now();
					offer[0].accept();
				}
			}
		}

		this.emit('pollSuccess');

		// If something has changed, emit the event
		if(!deepEqual(origPollData, this.pollData)) {
			this.emit('pollData', this.pollData);
		}

		this._resetPollTimer();
	}.bind(this));
};

function offerId(offer) {
	return String(offer.id);
}

TradeOfferManager.prototype._resetPollTimer = function(time) {
	if(time || this.pollInterval >= 1000) {
		clearTimeout(this._pollTimer);
		this._pollTimer = setTimeout(this.doPoll.bind(this), time || this.pollInterval);
	}
};
<|endoftext|>


<commit_before>// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt

// parent, args, callback
frappe.upload = {
	make: function(opts) {
		if(!opts.args) opts.args = {};
		var $upload = $(frappe.render_template("upload", {opts:opts})).appendTo(opts.parent);
		var $file_input = $upload.find(".input-upload-file");

		// bind pseudo browse button
		$upload.find(".btn-browse").on("click",
			function() { $file_input.click(); });

		$file_input.on("change", function() {
			if (this.files.length > 0) {
				$upload.find(".web-link-wrapper").addClass("hidden");

				var $uploaded_file_display = $(repl('
\
					<button type="button" class="btn btn-default btn-sm \
						text-ellipsis uploaded-filename-display">%(filename)s\
					</button>\
					<button type="button" class="btn btn-default btn-sm uploaded-file-remove">\
						×</button>\

', {filename: this.files[0].name}))
				.appendTo($upload.find(".uploaded-filename").removeClass("hidden").empty());

				$uploaded_file_display.find(".uploaded-filename-display").on("click", function() {
					$file_input.click();
				});

				$uploaded_file_display.find(".uploaded-file-remove").on("click", function() {
					$file_input.val("");
					$file_input.trigger("change");
				});

			} else {
				$upload.find(".uploaded-filename").addClass("hidden")
				$upload.find(".web-link-wrapper").removeClass("hidden");
			}
		});

		if(!opts.btn) {
			opts.btn = $('<button class="btn btn-default btn-sm">' + __("Attach")
				+ '
').appendTo($upload);
		} else {
			$(opts.btn).unbind("click");
		}

		// get the first file
		opts.btn.click(function() {
			// convert functions to values

			if(opts.get_params) {
				opts.args.params = opts.get_params();
			}

			opts.args.file_url = $upload.find('[name="file_url"]').val();

			var fileobj = $upload.find(":file").get(0).files[0];
			frappe.upload.upload_file(fileobj, opts.args, opts);
		});
	},
	upload_file: function(fileobj, args, opts) {
		if(!fileobj && !args.file_url) {
			if(opts.on_no_attach) {
				opts.on_no_attach();
			} else {
				msgprint(__("Please attach a file or set a URL"));
			}
			return;
		}

		var dataurl = null;
		var _upload_file = function() {
			if(opts.on_attach) {
				opts.on_attach(args, dataurl)
			} else {
				var msgbox = msgprint(__("Uploading..."));
				return frappe.call({
					"method": "uploadfile",
					args: args,
					callback: function(r) {
						if(!r._server_messages)
							msgbox.hide();
						if(r.exc) {
							// if no onerror, assume callback will handle errors
							opts.onerror ? opts.onerror(r) : opts.callback(null, null, r);
							return;
						}
						var attachment = r.message;
						opts.callback(attachment, r);
						$(document).trigger("upload_complete", attachment);
					}
				});
			}
		}

		if(args.file_url) {
			_upload_file();
		} else {
			var freader = new FileReader();

			freader.onload = function() {
				args.filename = fileobj.name;
				if(opts.options && opts.options.toLowerCase()=="image") {
					if(!(/\.(gif|jpg|jpeg|tiff|png|svg)$/i).test(args.filename)) {
						msgprint(__("Only image extensions (.gif, .jpg, .jpeg, .tiff, .png, .svg) allowed"));
						return;
					}
				}

				if((opts.max_width || opts.max_height) && (/\.(gif|jpg|jpeg|tiff|png)$/i).test(args.filename)) {
					frappe.utils.resize_image(freader, function(_dataurl) {
						dataurl = _dataurl;
						args.filedata = _dataurl.split(",")[1];
						console.log("resized!")
						_upload_file();
					})
				} else {
					dataurl = freader.result;
					args.filedata = freader.result.split(",")[1];
					_upload_file();
				}
			};

			freader.readAsDataURL(fileobj);
		}
	},
	get_string: function(dataURI) {
		// remove filename
		var parts = dataURI.split(',');
		if(parts[0].indexOf(":")===-1) {
			var a = parts[2];
		} else {
			var a = parts[1];
		}

		return decodeURIComponent(escape(atob(a)));

	}
}
<commit_msg>[fix] freeze btn during upload<commit_after>// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt

// parent, args, callback
frappe.upload = {
	make: function(opts) {
		if(!opts.args) opts.args = {};
		var $upload = $(frappe.render_template("upload", {opts:opts})).appendTo(opts.parent);
		var $file_input = $upload.find(".input-upload-file");

		// bind pseudo browse button
		$upload.find(".btn-browse").on("click",
			function() { $file_input.click(); });

		$file_input.on("change", function() {
			if (this.files.length > 0) {
				$upload.find(".web-link-wrapper").addClass("hidden");

				var $uploaded_file_display = $(repl('
\
					<button type="button" class="btn btn-default btn-sm \
						text-ellipsis uploaded-filename-display">%(filename)s\
					</button>\
					<button type="button" class="btn btn-default btn-sm uploaded-file-remove">\
						×</button>\

', {filename: this.files[0].name}))
				.appendTo($upload.find(".uploaded-filename").removeClass("hidden").empty());

				$uploaded_file_display.find(".uploaded-filename-display").on("click", function() {
					$file_input.click();
				});

				$uploaded_file_display.find(".uploaded-file-remove").on("click", function() {
					$file_input.val("");
					$file_input.trigger("change");
				});

			} else {
				$upload.find(".uploaded-filename").addClass("hidden")
				$upload.find(".web-link-wrapper").removeClass("hidden");
			}
		});

		if(!opts.btn) {
			opts.btn = $('<button class="btn btn-default btn-sm">' + __("Attach")
				+ '
').appendTo($upload);
		} else {
			$(opts.btn).unbind("click");
		}

		// get the first file
		opts.btn.click(function() {
			// convert functions to values

			if(opts.get_params) {
				opts.args.params = opts.get_params();
			}

			opts.args.file_url = $upload.find('[name="file_url"]').val();

			var fileobj = $upload.find(":file").get(0).files[0];
			frappe.upload.upload_file(fileobj, opts.args, opts);
		});
	},
	upload_file: function(fileobj, args, opts) {
		if(!fileobj && !args.file_url) {
			if(opts.on_no_attach) {
				opts.on_no_attach();
			} else {
				msgprint(__("Please attach a file or set a URL"));
			}
			return;
		}

		var dataurl = null;
		var _upload_file = function() {
			if(opts.on_attach) {
				opts.on_attach(args, dataurl)
			} else {
				var msgbox = msgprint(__("Uploading..."));
				return frappe.call({
					"method": "uploadfile",
					args: args,
					callback: function(r) {
						if(!r._server_messages)
							msgbox.hide();
						if(r.exc) {
							// if no onerror, assume callback will handle errors
							opts.onerror ? opts.onerror(r) : opts.callback(null, null, r);
							return;
						}
						var attachment = r.message;
						opts.callback(attachment, r);
						$(document).trigger("upload_complete", attachment);
					},
					btn: opts.btn
				});
			}
		}

		if(args.file_url) {
			_upload_file();
		} else {
			var freader = new FileReader();

			freader.onload = function() {
				args.filename = fileobj.name;
				if(opts.options && opts.options.toLowerCase()=="image") {
					if(!(/\.(gif|jpg|jpeg|tiff|png|svg)$/i).test(args.filename)) {
						msgprint(__("Only image extensions (.gif, .jpg, .jpeg, .tiff, .png, .svg) allowed"));
						return;
					}
				}

				if((opts.max_width || opts.max_height) && (/\.(gif|jpg|jpeg|tiff|png)$/i).test(args.filename)) {
					frappe.utils.resize_image(freader, function(_dataurl) {
						dataurl = _dataurl;
						args.filedata = _dataurl.split(",")[1];
						console.log("resized!")
						_upload_file();
					})
				} else {
					dataurl = freader.result;
					args.filedata = freader.result.split(",")[1];
					_upload_file();
				}
			};

			freader.readAsDataURL(fileobj);
		}
	},
	get_string: function(dataURI) {
		// remove filename
		var parts = dataURI.split(',');
		if(parts[0].indexOf(":")===-1) {
			var a = parts[2];
		} else {
			var a = parts[1];
		}

		return decodeURIComponent(escape(atob(a)));

	}
}
<|endoftext|>


<commit_before>var assert = require("assert");
var path = require("path");

describe("Remote", function() {
  var reposPath = path.resolve("test/repos/workdir/.git");

  var Repository = require("../../lib/repository");
  var Remote = require("../../lib/remote");

  before(function() {
    var test = this;

    return Repository.open(reposPath).then(function(repository) {
      test.repository = repository;

      return Remote.load(repository, "origin").then(function(remote) {
        test.remote = remote;
      });
    });
  });

  after(function() {
    return Remote.load(this.repository, "origin2").then(function(remote) {
      remote.delete();
    });
  });

  it("can load a remote", function() {
    assert.ok(this.remote instanceof Remote);
  });

  it("can read the remote url", function() {
    assert.equal(this.remote.url(), "https://github.com/nodegit/nodegit");
  });

  it("can read the remote name", function() {
    assert.equal(this.remote.name(), "origin");
  });

  it("can create a new remote", function() {
    var repository = this.repository;
    var url = "https://github.com/nodegit/nodegit";

    return Remote.create(repository, "origin2", url).then(function() {
      return Remote.load(repository, "origin2").then(function(remote) {
        assert(remote.url(), "https://github.com/nodegit/nodegit");
      });
    });
  });

  it("can delete a remote", function() {
    var repository = this.repository;
    var url = "https://github.com/nodegit/nodegit";

    return Remote.create(repository, "origin3", url).then(function() {
      return Remote.load(repository, "origin3").then(function(remote) {
        remote.delete();
      });
    });
  });
});
<commit_msg>Fixed remote.js test<commit_after>var assert = require("assert");
var path = require("path");

describe("Remote", function() {
  var reposPath = path.resolve("test/repos/workdir/.git");

  var Repository = require("../../lib/repository");
  var Remote = require("../../lib/remote");

  before(function() {
    var test = this;

    return Repository.open(reposPath).then(function(repository) {
      test.repository = repository;

      return Remote.load(repository, "origin").then(function(remote) {
        test.remote = remote;
      });
    });
  });

  after(function() {
    return Remote.load(this.repository, "origin2").then(function(remote) {
      remote.delete();
    });
  });

  it("can load a remote", function() {
    assert.ok(this.remote instanceof Remote);
  });

  it("can read the remote url", function() {
    assert.equal(this.remote.url(), "https://github.com/nodegit/nodegit.git");
  });

  it("can read the remote name", function() {
    assert.equal(this.remote.name(), "origin");
  });

  it("can create a new remote", function() {
    var repository = this.repository;
    var url = "https://github.com/nodegit/nodegit";

    return Remote.create(repository, "origin2", url).then(function() {
      return Remote.load(repository, "origin2").then(function(remote) {
        assert(remote.url(), "https://github.com/nodegit/nodegit");
      });
    });
  });

  it("can delete a remote", function() {
    var repository = this.repository;
    var url = "https://github.com/nodegit/nodegit";

    return Remote.create(repository, "origin3", url).then(function() {
      return Remote.load(repository, "origin3").then(function(remote) {
        remote.delete();
      });
    });
  });
});
<|endoftext|>


<commit_before>var _ = require('lodash');

var commands = require('./commands');

function Program(options) {
  if (options === undefined) {
    options = { src: '' };
  } else if (_.isString(options)) {
    options = { src: options };
  }
  this.minValue = _.isUndefined(options.minValue) ? -0xfffffff : options.minValue;
  this.maxValue = _.isUndefined(options.maxValue) ? 0xfffffff : options.maxValue;
  this.wrap = options.wrap || false;
  this.src = options.src;
  this.reset();
}

Program.prototype.reset = function() {
  this.tape = [0];
  this.tapeIndex = 0;
  this.srcIndex = 0;
  this.input = [];
  this.results = [];
  this.finished = false;
  this.errors = [];
};

Program.prototype.step = function step() {
  if (this.finished) { return; }
  commands[this.src[this.srcIndex]](this);
  this.srcIndex ++;
  if (this.errors.length || (this.srcIndex >= this.src.length)) {
    this.finished = true;
  }
};

Program.prototype.run = function(input) {
  this.addInput(input);
  while (!this.finished) { this.step(); }
  return this.resultString();
};

Program.prototype.addInput = function(toAdd) {
  if (_.isArray(toAdd)) {
    this.input = this.input.concat(toAdd);
  } else if (_.isNumber(toAdd)) {
    this.input.push(toAdd);
  } else if (_.isString(toAdd)) {
    for (var i = 0, len = toAdd.length; i < len; i ++) {
      this.input.push(toAdd.charCodeAt(i));
    }
  }
};

Program.prototype.resultString = function() {
  return String.fromCharCode.apply(String, this.results);
};

Program.prototype.flushResults = function() {
  var result = this.results;
  this.results = [];
  return result;
};

module.exports = Program;
<commit_msg>allow comments<commit_after>var _ = require('lodash');

var commands = require('./commands');

function Program(options) {
  if (options === undefined) {
    options = { src: '' };
  } else if (_.isString(options)) {
    options = { src: options };
  }
  this.minValue = _.isUndefined(options.minValue) ? -0xfffffff : options.minValue;
  this.maxValue = _.isUndefined(options.maxValue) ? 0xfffffff : options.maxValue;
  this.wrap = options.wrap || false;
  this.src = options.src;
  this.reset();
}

Program.prototype.reset = function() {
  this.tape = [0];
  this.tapeIndex = 0;
  this.srcIndex = 0;
  this.input = [];
  this.results = [];
  this.finished = false;
  this.errors = [];
};

Program.prototype.step = function step() {
  if (this.finished) { return; }
  var command = commands[this.src[this.srcIndex]];
  if (command) { command(this); }
  this.srcIndex ++;
  if (this.errors.length || (this.srcIndex >= this.src.length)) {
    this.finished = true;
  }
};

Program.prototype.run = function(input) {
  this.addInput(input);
  while (!this.finished) { this.step(); }
  return this.resultString();
};

Program.prototype.addInput = function(toAdd) {
  if (_.isArray(toAdd)) {
    this.input = this.input.concat(toAdd);
  } else if (_.isNumber(toAdd)) {
    this.input.push(toAdd);
  } else if (_.isString(toAdd)) {
    for (var i = 0, len = toAdd.length; i < len; i ++) {
      this.input.push(toAdd.charCodeAt(i));
    }
  }
};

Program.prototype.resultString = function() {
  return String.fromCharCode.apply(String, this.results);
};

Program.prototype.flushResults = function() {
  var result = this.results;
  this.results = [];
  return result;
};

module.exports = Program;
<|endoftext|>