diff --git a/app.js b/app.js index 88644fa..a3b10b8 100644 --- a/app.js +++ b/app.js @@ -32,6 +32,9 @@ hashbot.init(slackbot, process.env.MINING_CHANNEL); var claimbot = require('./bots/claimbot'); claimbot.init(slackbot, process.env.CLAIMS_CHANNEL, process.env.RPCUSER, process.env.RPCPASSWORD, process.env.MONGODB_URL); +var pricebot = require('./bots/pricebot'); +pricebot.init(process.env.MARKET_TRADING_CHANNEL); + slackbot.on('start', function() { slackbot.on('message', function(data) { if (data.type == 'team_join') { @@ -63,6 +66,10 @@ slackbot.on('start', function() { if (command === tipbot.command) { tipbot.respond(slackbot, data); } + + if (command === pricebot.command) { + pricebot.respond(slackbot, data); + } } }); }); diff --git a/bots/pricebot.js b/bots/pricebot.js new file mode 100644 index 0000000..944cc93 --- /dev/null +++ b/bots/pricebot.js @@ -0,0 +1,172 @@ +var jp = require('jsonpath'); +var moment = require('moment'); +var numeral = require('numeral'); +var request = require('request'); + +var options = { + defaultCurrency: 'USD', + + // supported currencies and api steps to arrive at the final value + currencies: { + USD: { steps: ['LBCBTC', 'BTCUSD'], format: '$0,0.00' }, + BTC: { steps: ['LBCBTC'], format: '0,0[.][00000000] BTC' } + }, + + // api steps + api: { + LBCBTC: { url: 'https://bittrex.com/api/v1.1/public/getticker?market=BTC-LBC', path: '$.result.Bid' }, + BTCUSD: { url: 'https://blockchain.info/ticker', path: '$.USD.buy' } + }, + + // display date/time format + dtFormat: 'Do MMM YYYY h:mma [UTC]', + + // refresh rate in milliseconds to retrieve a new price (default to 10 minutes) + refreshTime: 600000 +}; + +// store the last retrieved rate +var cachedRates = {}; + +var mktChannel; + +// !price {currency} +// !price {currency} {amount} +var command = '!price'; + +module.exports={ + command: command, + init: init, + respond: respond +}; + +function init(channel_) { + mktChannel = channel_; + if (!channel_) { + console.log('No market and trading channel. Pricebot will only respond to #bot-sandbox and DMs.'); + } + + var currencies = Object.keys(options.currencies); + for (var i = 0; i < currencies.length; i++) { + cachedRates[currencies[i]] = { rate: 0, time: null }; + } +} + +var globalSlackParams = {}; + +function respond(bot, data) { + var channel = data.channel, + words = data.text.trim().split(' ').filter( function(n){return n !== "";} ); + + if (words[0] !== command || (channel != mktChannel && channel !== 'C1TEEBS2Z' && !channel.startsWith('D'))) { + // if the received message isn't starting with the trigger, + // or the channel is not the market-and-trading channel, nor sandbox, nor a DM -> ignore + return; + } + + var currency = (words.length > 1) ? words[1].toUpperCase() : options.defaultCurrency; + var amount = (words.length > 2) ? parseFloat(words[2], 10) : 1; + var showHelp = (isNaN(amount)) || (Object.keys(options.currencies).indexOf(currency) === -1); + + var moveToBotSandbox = showHelp && channel !== 'C1TEEBS2Z' && !channel.startsWith("D"); + if (moveToBotSandbox) { + bot.postMessage(channel, 'Please use <#C1TEEBS2Z|bot-sandbox> to talk to bots.', globalSlackParams); + return; + } + + if (showHelp) { + doHelp(bot, channel); + } else { + doSteps(bot, channel, currency, amount); + } +} + +function doHelp(bot, channel) { + var message = + '`' + command + '`: show the price of 1 LBC in ' + options.defaultCurrency + '\n' + + '`' + command + ' help`: this message\n' + + '`' + command + ' CURRENCY`: show the price of 1 LBC in CURRENCY. Supported values for CURRENCY are *btc* and *usd* (case-insensitive)\n' + + '`' + command + ' CURRENCY AMOUNT`: show the price of AMOUNT LBC in CURRENCY\n'; + + if (!channel.startsWith("D")) { + message = + '*USE <#C1TEEBS2Z|bot-sandbox> FOR HELP*\n' + + message + + '\n' + + '*Everyone will see what I say. Send me a Direct Message if you want to interact privately.*\n' + + 'If I\'m not responding in some channel, you can invite me by @mentioning me.\n'; + } + + bot.postMessage(channel, message, globalSlackParams); +} + +function formatMessage(amount, rate, option) { + var value = numeral(rate.rate * amount).format(option.format); + return '*' + numeral(amount).format('0,0[.][00000000]') + ' LBC = ' + value + '*\n_last updated ' + rate.time.utc().format(options.dtFormat) + '_'; +} + +function doSteps(bot, channel, currency, amount) { + var option = options.currencies[currency]; + var shouldReload = true; + if (cachedRates[currency]) { + var cache = cachedRates[currency]; + shouldReload = cache.time === null || moment().diff(cache.time) >= options.refreshTime; + if (!shouldReload) { + var message = formatMessage(amount, cache, option); + bot.postMessage(channel, message); + } + } + + if (shouldReload) { + // copy the steps array + var steps = []; + for (var i = 0; i < option.steps.length; i++) { + steps.push(option.steps[i]); + } + + processSteps(bot, channel, currency, 0, amount, steps, option); + } +} + +function processSteps(bot, channel, currency, rate, amount, steps, option) { + if (steps.length > 0) { + var pairName = steps[0]; + if (!options.api[pairName]) { + bot.postMessage(channel, 'There was a configuration error. ' + pairName + ' pair was not found.'); + return; + } + + var pair = options.api[pairName]; + request.get(pair.url, function(error, response, body) { + if (error) { + bot.postMessage(channel, err.message ? err.message : 'The request could not be completed at this time. Please try again later.'); + return; + } + var pairRate = 0; + try { + pairRate = jp.query(JSON.parse(body), pair.path); + if (Array.isArray(pairRate) && pairRate.length > 0) { + pairRate = pairRate[0]; + } + } catch (ignored) { + // invalid response or pair rate + } + + if (pairRate > 0) { + rate = (rate === 0) ? pairRate : rate * pairRate; + steps.shift(); + if (steps.length > 0) { + processSteps(bot, channel, currency, rate, amount, steps, option); + return; + } + + // final step, cache and then response + var result = { rate: rate, time: moment() }; + cachedRates[currency] = result; + bot.postMessage(channel, formatMessage(amount, result, option)); + } else { + bot.postMessage(channel, 'The rate returned for the ' + pairName + ' pair was invalid.'); + } + }); + } +} diff --git a/package.json b/package.json index 5a8582d..15b0dd8 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,16 @@ { "name": "hashbot", - "version": "1.0.1", + "version": "1.0.2", "description": "A bot for slack wich displays lbrys current hashrate via the open api.", "main": "app.js", "dependencies": { "bitcoin": "^3.0.1", "imgur": "0.1.7", + "jsonpath": "0.2.11", "moment": "^2.17.1", "mongodb": "^2.2.22", "needle": "^1.0.0", + "numeral": "2.0.6", "request": "^2.81.0", "slackbots": "^0.5.1", "xmlhttprequest": "1.8.0"