diff --git a/README.md b/README.md index 0f6b393..d5e94af 100644 --- a/README.md +++ b/README.md @@ -23,52 +23,38 @@ Open config-sample.json in your favourite text editor and edit with your own set "private_key": "5kTSOMEPRIVATEKEY111111111111", "missed_block_threshold": 3, "checking_interval": 10, - "backup_key": "BTSXXXXXXXXXXXXXXXXXX", + "reset_period": 300, + "witness_signing_keys": [ "BTSXXXXXXXXXXXXXXXXXX", "BTSYYYYYYYYYYYYYYY"], "recap_time": 60, - "reset_period": 300 "debug_level": 3, "telegram_token": "", - "telegram_password": "", - "retries_threshold": 3 + "telegram_authorized_users": [""], + "retries_threshold": 3, + "feeds_to_check" : [""], + "feed_publication_threshold": 60, + "feed_checking_interval": 10 } ``` and then save as config.json -`private_key` -The active key of your normal witness-owning account used to sign the witness_update operation. - -`missed_block_threshold` -How many blocks must be missed within a `reset_period` sec window before the script switches your signing key. Recommend to set at 2 or higher since 1 will possibly trigger updates on maintenance intervals (see: https://github.com/bitshares/bitshares-core/issues/504) - -`checking_interval` -How often should the script check for new missed blocks in seconds. - -`backup_key` -The public signing key of your backup witness to be used when switching. - -`recap_time` -The interval in minutes on which bot will auto-notify telegram user of latest stats (if authenticated). - -`reset_period` -The time after which the missed blocks counter is reset for the session in seconds. - -`debug_level` -Logging level. Can be: -0: Minimum - Explicit logging & Errors -1: Info - 0 + Basic logging -2: Verbose - 1 + Verbose logging -3: Transient - 2 + Transient messages -but not currently used. - -`telegram_token` -The telegram access token for your notifications bot. You can get one here: https://telegram.me/BotFather - -`telegram_password` -Your chosen access password through telegram. - -`retries_threshold` -Number of failed connections to API node before the bot notifies you on telegram. +| Key | Description | +| --- | --- | +| `witness_id` | The id of the witness to monitor. | +| `api_node` | Bitshares Websocket url to use to retrieve blockchain information. | +| `private_key` | The active key of your normal witness-owning account used to sign the witness_update operation. | +| `missed_block_threshold` | How many blocks must be missed within a `reset_period` sec window before the script switches your signing key. Recommend to set at 2 or higher since 1 will possibly trigger updates on maintenance intervals (see [bitshares-core#504](https://github.com/bitshares/bitshares-core/issues/504)). | +| `checking_interval` | How often should the script check for new missed blocks in seconds. | +| `witness_signing_keys` | All the public keys of your witness, to switch key if too many blocks are missed. | +| `recap_time` | The interval in minutes on which bot will auto-notify telegram user of latest stats (if authenticated). | +| `reset_period` | The time after which the missed blocks counter is reset for the session in seconds. | +| `debug_level` | Logging level. Can be: _0_ (Minimum - Explicit logging & Errors, _1_ (Info - 0 + Basic logging), _2_ (Verbose - 1 + Verbose logging), _3_. Transient - 2 + Transient messages. Not currently used. | +| `telegram_token` | The telegram access token for your notifications bot. You can create one with [BotFather](https://telegram.me/BotFather). | +| `telegram_authorized_users` | List of userId authorized to interact with the bot. You can get your user Id by talking to the bot, or use a bot like [@userinfobot](https://telegram.me/userinfobot). | +| `retries_threshold` | Number of failed connections to API node before the bot notifies you on telegram. | +| `feeds_to_check`| Array of assets symbols where the price publication should be checked. | +| `feed_publication_threshold` | How many minutes before a feed is considered as missing. | +| `feed_checking_interval` | How often should the script check for unpublished feeds. | ## Running @@ -112,63 +98,48 @@ This will build the image, then run it with `./config.json` file mounted in the Open a chat to your bot and use the following: -`/pass ` - -This is required to authenticate. Otherwise none of the following commands will work. - -`/changepass ` - -This will update your telegram access password and will require you to authenticate again using `/pass` - -`/stats` - -This will return the current configuration and statistics of the monitoring session. - -`/switch` - -This will IMMEDIATELY update your signing key to the currently configured backup key. - -`/new_key ` - -This will set a new backup key in place of the configured one. - -`/new_node wss://` +- `/start`: Introduction message. +- `/help`: Get the list of available commands. +- `/stats`: Return the current statistics of the monitoring session. +- `/settings`: Display current configuration. +- `/switch`: IMMEDIATELY update your signing key to the new available signing key. +- `/signing_keys `: Set a new list of public keys. +- `/new_node wss://`: Set a new API node to connect to. +- `/threshold X`: Set the missed block threshold before updating signing key to X blocks. +- `/interval Y`: Set the checking interval to every Y seconds. +- `/window Z` : Set the time until missed blocks counter is reset to Z seconds. +- `/recap T` : Set the auto-notification interval of latest stats to every T minutes. Set to 0 to disable. +- `/retries N` : Set the threshold for failed API node connection attempts to N times before notifying you in telegram. +- `/feed_publication_threshold X`: Set the feed threshold to X minutes. +- `/feed_checking_interval I`: Set the interval of publication feed check to I minutes. +- `/feeds ...`: Set the feeds to check to the provided list. +- `/reset` : Reset the missed blocks counter in the current time-window. +- `/pause` : Pause monitoring. +- `/resume`: Resume monitoring. + +Send this to @BotFather `/setcommands` to get completion on commands: -This will set a new API node to connect to. - -`/threshold X` - -This will set the missed block threshold before updating signing key to X blocks. - -`/interval Y` - -This will set the checking interval to every Y seconds. - -`/window Z` - -This will set the time until missed blocks counter is reset to Z seconds. - -`/recap T` - -This will set the auto-notification interval of latest stats to every T minutes. Set to 0 to disable. - -`/retries N` - -This will set the threshold for failed API node connection attempts to N times before notifying you in telegram. - -`/reset` - -This will reset the missed blocks counter in the current time-window. - -`/pause` - -This will pause monitoring. - -`/resume` - -This will resume monitoring. +``` +start - Introduction +help - List all commands +stats - Gather statistics +settings - Display current settings +switch - Update signing key to backup +signing_keys - Set signing keys +new_node - Set a new API node to connect to +threshold - Set the missed block threshold +interval - Set the checking interval +window - Set the time until missed blocks counter is reset +recap - Set the auto-notification interval of latest stats +retries - Set the threshold for failed API node connection attempts +feed_publication_threshold - Set the feed threshold +feed_checking_interval - Set the interval of publication feed check +feeds - Set the feeds to check +reset - Reset the missed blocks counter +pause - Pause monitoring +resume - Resume monitoring +``` -------------------------------------------------------------------------------------------------------------------- **DONATIONS WELCOME** @ BTS: clockwork diff --git a/config-sample.json b/config-sample.json index 8426dca..2e70255 100644 --- a/config-sample.json +++ b/config-sample.json @@ -5,10 +5,13 @@ "missed_block_threshold": 3, "checking_interval": 10, "reset_period": 300, - "backup_key": "", + "witness_signing_keys": [], "recap_time": 60, "debug_level": 3, "telegram_token": "", - "telegram_password": "", - "retries_threshold": 3 + "telegram_authorized_users": [], + "retries_threshold": 3, + "feeds_to_check" : [], + "feed_publication_threshold": 60, + "feed_checking_interval": 10 } \ No newline at end of file diff --git a/index.js b/index.js index 638d9b7..fe99a0d 100644 --- a/index.js +++ b/index.js @@ -1,365 +1,313 @@ process.env["NTBA_FIX_319"] = 1; const TelegramBot = require('node-telegram-bot-api'); -const Logger = require('./lib/Logger.js'); -const {Apis} = require('bitsharesjs-ws'); -const {PrivateKey,TransactionBuilder} = require('bitsharesjs'); +const moment = require('moment'); const config = require('./config.json'); +const validate_config = require('./lib/ValidateConfig.js') +const Logger = require('./lib/Logger.js'); +const WitnessMonitor = require('./lib/WitnessMonitor.js') -let apiNode = config.api_node; -let threshold = config.missed_block_threshold; -let interval = config.checking_interval ; -let password= config.telegram_password; -let backupKey = config.backup_key; -let timeWindow = config.reset_period; -let witness = config.witness_id; -let token = config.telegram_token; -let privKey = config.private_key; -let retries = config.retries_threshold; -let auto_stats = config.recap_time; - -let paused = false; -let pKey = PrivateKey.fromWif(privKey); -let logger = new Logger(config.debug_level); -var to; -const bot = new TelegramBot(token, {polling: true}); - -var admin_id = ""; -var total_missed = 0; -var start_missed = 0; -var node_retries=0; -var window_start=0; -var checking=false; - -bot.onText(/\/pass (.+)/, (msg, match) => { +const logger = new Logger(config.debug_level); +const bot = new TelegramBot(config.telegram_token, {polling: true}); - const chatId = msg.from.id; - const pass = match[1]; - if (pass == password) { - bot.sendMessage(chatId, 'Password accepted.'); - admin_id = chatId; - } else { - bot.sendMessage(chatId, 'Password incorrect.'); +function check_config(config) { + const validation_result = validate_config(config); + if (validation_result !== undefined) { + console.log('Invalid configuration file:') + for (let field in validation_result) { + for (let error of validation_result[field]) { + console.log(` - ${field}: ${error}`); + } + } + process.exit(); } +} +function check_authorization(chatId) { + if (config.telegram_authorized_users.includes(chatId)) { + bot.sendMessage(chatId, `You (${chatId}) are not authorized.`); + return false; + } + return true; +} + +function send_stats(recipient_id) { + const current_stats = witness_monitor.current_statistics(); + let stats = [ + `Total missed blocks: \`${current_stats.total_missed}\``, + `Missed blocks in current time window: \`${current_stats.window_missed}\``, + `Total votes: \`${current_stats.total_votes}\` (${current_stats.is_activated ? "active" : "inactive"})`, + `Current signing key: \`${current_stats.signing_key}\``, + `Feed publications: ` + ] + current_stats.feed_publications.forEach(feed_stat => { + stats.push(` - ${feed_stat.toString()}`) + }); + bot.sendMessage(recipient_id, stats.join('\n'), { parse_mode: 'Markdown' }); +} + +function send_settings(recipient_id) { + const settings = [ + `API node: \`${config.api_node}\``, + `Witness monitored: \`${config.witness_id}\``, + `Checking interval: \`${config.checking_interval} sec\``, + `Node failed connection attempt notification threshold: \`${config.retries_threshold}\``, + `Missed block threshold: \`${config.missed_block_threshold}\``, + `Missed block reset time window: \`${config.reset_period} sec\``, + `Public signing keys: ${config.witness_signing_keys.map(k => '`' + k + '`').join(', ')}`, + `Recap time period: \`${config.recap_time} min\``, + `Feeds to check: \`${config.feeds_to_check}\``, + `Feed publication treshold: \`${config.feed_publication_threshold} min\``, + `Feed check interval: \`${config.feed_checking_interval} min\``, + ]; + bot.sendMessage(recipient_id, settings.join('\n'), { parse_mode: 'Markdown' }) +} + +bot.on('polling_error', (error) => { + logger.error(error); }); -bot.onText(/\/changepass (.+)/, (msg, match) => { + + +bot.onText(/\/start/, (msg) => { const chatId = msg.from.id; - const pass = match[1]; - if (admin_id == chatId) { - password=pass; - bot.sendMessage(chatId, 'Password changed. Please authenticate again with /pass .'); - admin_id = 0; + if (config.telegram_authorized_users.includes(chatId)) { + bot.sendMessage(chatId, `Hello ${msg.from.first_name}, type /help to get the list of commands.`); } else { - bot.sendMessage(chatId, "You need to authenticate first."); + bot.sendMessage(chatId, `Hello ${msg.from.first_name}, sorry but there is nothing for you here.`); } +}); +bot.onText(/\/help/, (msg) => { + const help = [ + `\`/stats\`: Return the current configuration and statistics of the monitoring session.`, + `\`/switch\`: IMMEDIATELY update your signing key to the next available signing key.`, + `\`/signing_keys \`: Set a new list of public keys.`, + `\`/new_node wss://\`: Set a new API node to connect to.`, + `\`/threshold X\`: Set the missed block threshold before updating signing key to X blocks.`, + `\`/interval Y\`: Set the checking interval to every Y seconds.`, + `\`/interval Y\`: Set the checking interval to every Y seconds.`, + `\`/window Z\` : Set the time until missed blocks counter is reset to Z seconds.`, + `\`/recap T\` : Set the auto-notification interval of latest stats to every T minutes. Set to 0 to disable.`, + `\`/retries N\` : Set the threshold for failed API node connection attempts to N times before notifying you in telegram.`, + `\`/feed_publication_threshold X\`: Set the feed threshold to X minutes.`, + `\`/feed_checking_interval I\`: Set the interval of publication feed check to I minutes.`, + `\`/feeds ...\`: Set the feeds to check to the provided list.`, + `\`/reset\` : Reset the missed blocks counter in the current time-window.`, + `\`/pause\` : Pause monitoring.`, + `\`/resume\`: Resume monitoring.` + ]; + bot.sendMessage(msg.from.id, help.join('\n'), { parse_mode: 'Markdown' }); }); + bot.onText(/\/reset/, (msg, match) => { const chatId = msg.chat.id; - if (admin_id == chatId) { - start_missed = total_missed; - window_start=Date.now(); - bot.sendMessage(chatId, "Session missed block counter set to 0."); - } else { - bot.sendMessage(chatId, "You need to authenticate first."); + + if (check_authorization(chatId)) { + witness_monitor.reset_missed_block_window(); + bot.sendMessage(chatId, 'Session missed block counter set to 0.'); } }); -bot.onText(/\/new_key (.+)/, (msg, match) => { +bot.onText(/\/signing_keys (.+)/, (msg, match) => { const chatId = msg.chat.id; - const key = match[1]; - if (admin_id == chatId) { - backupKey = key; - bot.sendMessage(chatId, "Backup signing key set to: "+backupKey); - } else { - bot.sendMessage(chatId, "You need to authenticate first."); + const keys = match[1].split(' '); + + if (check_authorization(chatId)) { + config.witness_signing_keys = keys; + bot.sendMessage(chatId, `Signing keys set to: ${config.witness_signing_keys.map(k => '`' + k + '`').join(', ')}`, + { parse_mode: 'Markdown' }); } }); + bot.onText(/\/new_node (.+)/, (msg, match) => { const chatId = msg.chat.id; const node = match[1]; - if (admin_id == chatId) { - apiNode = node; - bot.sendMessage(chatId, "API node set to: "+apiNode); - } else { - bot.sendMessage(chatId, "You need to authenticate first."); + + if (check_authorization(chatId)) { + config.api_node = node; + bot.sendMessage(chatId, `API node set to: ${config.api_node}`); } }); + bot.onText(/\/threshold (.+)/, (msg, match) => { const chatId = msg.chat.id; const thresh = match[1]; - if (admin_id == chatId) { - threshold = thresh; - bot.sendMessage(chatId, "Missed block threshold set to: "+threshold); - } else { - bot.sendMessage(chatId, "You need to authenticate first."); + + if (check_authorization(chatId)) { + config.missed_block_threshold = thresh; + bot.sendMessage(chatId, `Missed block threshold set to: ${config.missed_block_threshold}`); } }); + bot.onText(/\/recap (.+)/, (msg, match) => { const chatId = msg.chat.id; const recap = match[1]; - if (admin_id == chatId) { - auto_stats = recap; - if (auto_stats>0) { - bot.sendMessage(chatId, "Recap time period set to: "+auto_stats+" minutes."); - }else{ - bot.sendMessage(chatId, "Recap disabled."); + + if (check_authorization(chatId)) { + config.recap_time = recap; + if (config.recap_time > 0) { + bot.sendMessage(chatId, `Recap time period set to: ${config.recap_time} minutes.`); + } else { + bot.sendMessage(chatId, 'Recap disabled.'); } - } else { - bot.sendMessage(chatId, "You need to authenticate first."); } - }); + bot.onText(/\/window (.+)/, (msg, match) => { const chatId = msg.chat.id; const wind = match[1]; - if (admin_id == chatId) { - timeWindow = wind; - bot.sendMessage(chatId, "Missed block reset time window set to: "+timeWindow+"s"); - } else { - bot.sendMessage(chatId, "You need to authenticate first."); + + if (check_authorization(chatId)) { + config.reset_period = wind; + bot.sendMessage(chatId, `Missed block reset time window set to: ${config.reset_period}s`); } }); + bot.onText(/\/retries (.+)/, (msg, match) => { const chatId = msg.chat.id; const ret = match[1]; - if (admin_id == chatId) { - retries = ret; - bot.sendMessage(chatId, "Failed node connection attempt notification threshold set to: "+retries); - } else { - bot.sendMessage(chatId, "You need to authenticate first."); + + if (check_authorization(chatId)) { + config.retries_threshold = ret; + bot.sendMessage(chatId, `Failed node connection attempt notification threshold set to: ${config.retries_threshold}`); } }); + bot.onText(/\/interval (.+)/, (msg, match) => { const chatId = msg.chat.id; const new_int = match[1]; - if (admin_id == chatId) { - interval = new_int; - bot.sendMessage(chatId, "Checking interval set to: "+interval+'s.'); - } else { - bot.sendMessage(chatId, "You need to authenticate first."); + + if (check_authorization(chatId)) { + config.checking_interval = new_int; + bot.sendMessage(chatId, `Checking interval set to: ${config.checking_interval}s.`); } - + }); + bot.onText(/\/stats/, (msg, match) => { const chatId = msg.chat.id; + + if (check_authorization(chatId)) { + send_stats(chatId); + } +}); - if (admin_id == chatId) { - bot.sendMessage(chatId, "Checking interval: `" + interval + ' sec`\n'+ - "Node failed connection attempt notification threshold: `" + retries+'`\n'+ - "Missed block threshold: `"+threshold+'`\n'+ - "Missed block reset time window: `"+timeWindow+" sec`\n"+ - "API node: `"+apiNode+'`\n'+ - "Backup signing key: `"+backupKey+'`\n'+ - "Recap time period: `"+auto_stats+' min`\n'+ - "Total missed blocks: `"+total_missed+'`\n'+ - "Missed blocks in current time window: `"+(total_missed - start_missed)+'`',{ - parse_mode: "Markdown" - }); - } else { - bot.sendMessage(chatId, "You need to authenticate first."); +bot.onText(/\/settings/, (msg, match) => { + + const chatId = msg.chat.id; + + if (check_authorization(chatId)) { + send_settings(chatId); } + +}); +bot.onText(/\/feed_checking_interval (.+)/, (msg, match) => { + + const chatId = msg.chat.id; + const new_int = match[1]; + + if (check_authorization(chatId)) { + config.feed_checking_interval = new_int; + witness_monitor.reset_feed_check(); + bot.sendMessage(chatId, `Feed checking interval set to: ${config.feed_checking_interval}m.`); + } + }); -bot.onText(/\/pause/, (msg, match) => { + +bot.onText(/\/feed_publication_threshold (.+)/, (msg, match) => { const chatId = msg.chat.id; + const new_threshold = match[1]; + + if (check_authorization(chatId)) { + config.feed_publication_threshold = new_threshold; + witness_monitor.reset_feed_check(); + bot.sendMessage(chatId, `Feed publication threshold set to: ${config.feed_publication_threshold}m.`); + } + +}); - if (admin_id == chatId) { - paused=true; - bot.sendMessage(chatId, "Witness monitoring paused. Use /resume to resume monitoring."); +bot.onText(/\/feeds (.+)/, (msg, match) => { - } else { - bot.sendMessage(chatId, "You need to authenticate first."); + const chatId = msg.chat.id; + const new_feeds = match[1].split(' '); + + if (check_authorization(chatId)) { + config.feeds_to_check = new_feeds; + witness_monitor.reset_feed_check(); + bot.sendMessage(chatId, `Feeds to check set to: ${config.feeds_to_check}.`); + } + +}); + +bot.onText(/\/pause/, (msg, match) => { + + const chatId = msg.chat.id; + + if (check_authorization(chatId)) { + witness_monitor.pause(); + bot.sendMessage(chatId, 'Witness monitoring paused. Use /resume to resume monitoring.'); } }); + bot.onText(/\/switch/, (msg, match) => { const chatId = msg.chat.id; - if (admin_id == chatId) { - bot.sendMessage(chatId, "Attempting to update signing key..."); - logger.log('Received key update request.'); - Apis.instance(apiNode, true).init_promise.then(() => { - let tr = new TransactionBuilder(); - tr.add_type_operation("witness_update", { - fee: { - amount: 0, - asset_id: '1.3.0' - }, - witness: witness, - witness_account: witness_account, - new_url: '', - new_signing_key: backupKey - }); - - tr.set_required_fees().then(() => { - tr.add_signer(pKey, pKey.toPublicKey().toPublicKeyString()); - tr.broadcast().then(() => { - logger.log('Signing key updated'); - bot.sendMessage(chatId, "Signing key updated. Use /new_key to set the next backup key."); - window_start=Date.now(); - start_missed = total_missed; - if (paused || !checking) { - Apis.close(); - } - },() => { - logger.log('Could not broadcast update_witness tx.'); - bot.sendMessage(chatId, "Could not broadcast update_witness tx. Please check!"); - if (paused || !checking) { - Apis.close(); - } - }); - }); - },() => { - logger.log('Could not update signing key.'); - bot.sendMessage(chatId, "Could not update signing key. Please check!"); - }); - } else { - bot.sendMessage(chatId, "You need to authenticate first."); + if (check_authorization(chatId)) { + bot.sendMessage(chatId, 'Attempting to update signing key...'); + witness_monitor.force_update_signing_key(); } }); + bot.onText(/\/resume/, (msg, match) => { const chatId = msg.chat.id; - if (admin_id == chatId) { - paused=false; - window_start=Date.now(); - try { - clearTimeout(to); - to=setTimeout(checkWitness, interval*1000); - }catch(e){ - to=setTimeout(checkWitness, interval*1000); - } - bot.sendMessage(chatId, "Witness monitoring resumed."); - } else { - bot.sendMessage(chatId, "You need to authenticate first."); + if (check_authorization(chatId)) { + witness_monitor.resume(); + bot.sendMessage(chatId, 'Witness monitoring resumed.'); } }); -logger.log('Starting witness health monitor'); -let first = true; -checkWitness(); -var witness_account; -var lastupdate=0; - -function checkWitness() { - - if (!paused) { - checking=true; - Apis.instance(apiNode, true).init_promise.then(() => { - node_retries=0; - logger.log('Connected to API node: ' + apiNode); - Apis.instance().db_api().exec('get_objects', [ - [witness], false - ]).then((witness) => { - if (first) { - start_missed = witness[0].total_missed; - window_start=Date.now(); - first = false; - } - if ((admin_id!=0) && (auto_stats>0)) { - if (Math.floor((Date.now()-lastupdate)/60000)>=auto_stats) { - lastupdate=Date.now(); - bot.sendMessage(admin_id, "Checking interval: `" + interval + ' sec`\n'+ - "Node failed connection attempt notification threshold: `" + retries+'`\n'+ - "Missed block threshold: `"+threshold+'`\n'+ - "Missed block reset time window: `"+timeWindow+" sec`\n"+ - "API node: `"+apiNode+'`\n'+ - "Backup signing key: `"+backupKey+'`\n'+ - "Recap time period: `"+auto_stats+' min`\n'+ - "Total missed blocks: `"+total_missed+'`\n'+ - "Missed blocks in current time window: `"+(total_missed - start_missed)+'`',{ - parse_mode: "Markdown" - }); - } - } - total_missed = witness[0].total_missed; - if (Math.floor((Date.now()-window_start)/1000)>=timeWindow) { - window_start=Date.now(); - start_missed=total_missed; - } - let missed = total_missed - start_missed; - witness_account = witness[0].witness_account; - logger.log('Total missed blocks: ' + total_missed); - logger.log('Missed since time window start: ' + missed); - if (missed > threshold) { - logger.log('Missed blocks since time window start (' + missed + ') greater than threshold (' + threshold + '). Notifying...'); - logger.log('Switching to backup witness server.'); - bot.sendMessage(admin_id, 'Missed blocks since start (' + missed + ') greater than threshold (' + threshold + ').'); - bot.sendMessage(admin_id, 'Switching to backup witness server.'); - let tr = new TransactionBuilder(); - tr.add_type_operation("witness_update", { - fee: { - amount: 0, - asset_id: '1.3.0' - }, - witness: witness, - witness_account: witness_account, - new_url: '', - new_signing_key: backupKey - }); - - tr.set_required_fees().then(() => { - tr.add_signer(pKey, pKey.toPublicKey().toPublicKeyString()); - tr.broadcast().then(() => { - logger.log('Signing key updated'); - bot.sendMessage(chatId, "Signing key updated. Use /new_key to set the next backup key."); - first = true; - to=setTimeout(checkWitness, interval*1000); - Apis.close(); - checking=false; - },() => { - logger.log('Could not broadcast update_witness tx.'); - bot.sendMessage(chatId, "Could not broadcast update_witness tx. Please check!"); - //first = true; - to=setTimeout(checkWitness, interval*1000); - Apis.close(); - checking=false; - }); - }); - - } else { - logger.log('Status: OK'); - to=setTimeout(checkWitness, interval*1000); - Apis.close(); - checking=false; - } - }); - - }, () => { - - node_retries++; - logger.log('API node unavailable.'); - if (node_retries>retries) { - logger.log('Unable to connect to API node for '+node_retries+' times. Notifying...'); - bot.sendMessage(admin_id, 'Unable to connect to API node for '+node_retries+' times. Please check.'); - } - to=setTimeout(checkWitness, interval*1000); - - Apis.close(); - checking=false; - }); - } -} \ No newline at end of file + +check_config(config); + +const witness_monitor = new WitnessMonitor(config, logger); +var last_recap_send = moment(); +for (let user_id of config.telegram_authorized_users) { + witness_monitor.on('started', () => { + bot.sendMessage(user_id, 'Bot (re)started.'); + send_settings(user_id); + }); + witness_monitor.on('notify', (msg) => { + bot.sendMessage(user_id, msg); + }); + witness_monitor.on('checked', () => { + if (config.recap_time > 0 && moment().diff(last_recap_send, 'minutes') >= config.recap_time) { + last_recap_send = moment(); + send_stats(user_id); + } + }); +} +witness_monitor.start_monitoring(); \ No newline at end of file diff --git a/lib/FeedStat.js b/lib/FeedStat.js new file mode 100644 index 0000000..a5f4d6b --- /dev/null +++ b/lib/FeedStat.js @@ -0,0 +1,28 @@ +const moment = require('moment'); + +class FeedStat { + constructor(name, my_publication_time, my_price, average_price) { + this.name = name; + this.publication_time = my_publication_time; + this._price = my_price; + this._average_price = average_price; + } + + since() { + return moment.duration(moment.utc().diff(moment.utc(this.publication_time))); + } + + spread() { + return (1 - (this._price / this._average_price)) * 100; + } + + toString() { + if (this.publication_time == null || moment.utc(this.publication_time).isBefore('2013-01-01')) { + return `${this.name} not published yet.` + } + + return `${this.name} published at ${this.publication_time} UTC (${this.since().humanize()} ago) with spread ${this.spread()}%` + } +} + +module.exports = FeedStat; \ No newline at end of file diff --git a/lib/ValidateConfig.js b/lib/ValidateConfig.js new file mode 100644 index 0000000..0020bb5 --- /dev/null +++ b/lib/ValidateConfig.js @@ -0,0 +1,111 @@ +var validate = require("validate.js"); + +validate.validators.signing_keys = function(value, options, key, attributes) { + if (!validate.isArray(value)) { + return 'should be an array'; + } + if (value.length < 2) { + return 'should contains at least 2 keys'; + } + + for (let key of value) { + if (!(/TEST.*/.test(key) || /BTS.*/.test(key))) { + return 'is badly formated as it should start by TEST of BTS' + } + } +} + +var constraints = { + "witness_id": { + presence: { allowEmpty: false }, + format: { + pattern: /1\.6\.\d+/, + message: 'should match 1.6.XXX' + } + }, + "api_node": { + presence: { allowEmpty: false }, + format: { + pattern: /wss?:\/\/.*/, + message: 'should be a Websocket url: ws://xxxx, or wss://xxxx' + } + }, + "private_key": { presence: { allowEmpty: false } }, + "missed_block_threshold": { + presence: true, + numericality: { + onlyInteger: true, + greaterThan: 0 + } + }, + "checking_interval": { + presence: true, + numericality: { + onlyInteger: true, + greaterThan: 0 + } + }, + "reset_period": { + presence: true, + numericality: { + onlyInteger: true, + greaterThan: 0 + } + }, + "witness_signing_keys": { + presence: { allowEmpty: false }, + signing_keys: true + }, + "recap_time": { + presence: true, + numericality: { + onlyInteger: true, + greaterThanOrEqualTo: 0 + } + }, + "debug_level": { + presence: true, + numericality: { + onlyInteger: true, + greaterThanOrEqualTo: 0, + lessThanOrEqualTo: 3 + } + }, + "telegram_token": { + presence: { allowEmpty: false }, + format: { + pattern: /.*:.*/, + message: 'should be a valid Telegram token that match: bot_id:token' + } + }, + "telegram_authorized_users": { presence: { allowEmpty: false } }, + "retries_threshold": { + presence: true, + numericality: { + onlyInteger: true, + greaterThan: 0 + } + }, + "feeds_to_check" : {}, + "feed_publication_threshold": { + presence: true, + numericality: { + onlyInteger: true, + greaterThan: 0 + } + }, + "feed_checking_interval": { + presence: true, + numericality: { + onlyInteger: true, + greaterThan: 0 + } + } +} + +function validate_config(config) { + return validate(config, constraints); +} + + +module.exports = validate_config; \ No newline at end of file diff --git a/lib/WitnessMonitor.js b/lib/WitnessMonitor.js new file mode 100644 index 0000000..873d322 --- /dev/null +++ b/lib/WitnessMonitor.js @@ -0,0 +1,256 @@ +const EventEmitter = require('events'); +const {Apis} = require('bitsharesjs-ws'); +const {PrivateKey,TransactionBuilder} = require('bitsharesjs'); +const moment = require('moment'); +const FeedStat = require('./FeedStat.js'); + +class WitnessMonitor extends EventEmitter { + + constructor(config, logger) { + super(); + this._config = config; + this._logger = logger; + this._paused = false; + this._check_witness_promise = null; + this._total_missed = null; + this._start_missed = null; + this._window_start = null; + this._checking = false; + this._witness_account = null; + this._total_votes = null; + this._is_witness_active = null; + this._witness_url = null; + this._witness_current_signing_key = null; + this._feed_stats = new Map(); + this._last_feed_check = null; + this._node_retries = 0; + } + + current_statistics() { + return { + total_missed : this._total_missed, + total_votes : this._total_votes, + is_activated: this._is_witness_active, + window_missed : this._total_missed - this._start_missed, + signing_key : this._witness_current_signing_key, + feed_publications : this._feed_stats + } + } + + reset_missed_block_window() { + this._start_missed = this._total_missed; + this._window_start = moment(); + } + + reset_feed_check() { + this._last_feed_check = null; + } + + force_update_signing_key() { + this._logger.log('Received key update request.'); + Apis.instance(this._config.api_node, true).init_promise.then(() => { + return this.update_signing_key(); + }).catch(() => { + this.notify('Could not update signing key.'); + }).then(() => { + if (this._paused || !this._checking) { + return Apis.close(); + } + }); + } + + find_next_signing_key() { + const i = this._config.witness_signing_keys.findIndex(k => k == this._witness_current_signing_key); + if (i == -1) { + return this._config.witness_signing_keys[0]; + } + return this._config.witness_signing_keys[(i + 1) % this._config.witness_signing_keys.length]; + } + + update_signing_key() { + const backup_signing_key = this.find_next_signing_key(); + const tr = new TransactionBuilder(); + tr.add_type_operation('witness_update', { + fee: { + amount: 0, + asset_id: '1.3.0' + }, + witness: this._config.witness_id, + witness_account: this._witness_account, + new_url: this._witness_url, + new_signing_key: backup_signing_key + }); + + return tr.set_required_fees().then(() => { + const private_key = PrivateKey.fromWif(this._config.private_key); + tr.add_signer(private_key, private_key.toPublicKey().toPublicKeyString()); + return tr.broadcast(); + }) + .then(() => { + this.reset_missed_block_window(); + this.notify(`Signing key updated to: ${backup_signing_key}`); + }).catch(() => { + this.notify('Could not broadcast update_witness tx.'); + }); + + } + + notify(msg) { + this._logger.log(msg); + this.emit('notify', msg) + } + + + extract_feed_data(feeds_to_check, dynamic_assets_data, witness_account) { + const feeds_stats = new Map(); + + feeds_to_check.map((symbol, i) => { + let my_publication_time = null; + let my_price = null; + let average_price = dynamic_assets_data[i]['current_feed']['settlement_price']['base']['amount'] / dynamic_assets_data[i]['current_feed']['settlement_price']['quote']['amount']; + for (const feed of dynamic_assets_data[i]['feeds']) { + if (feed[0] == witness_account) { + my_publication_time = feed[1][0]; + my_price = feed[1][1]['settlement_price']['base']['amount'] / feed[1][1]['settlement_price']['quote']['amount'] + } + } + + feeds_stats.set(symbol, new FeedStat(symbol,my_publication_time, my_price, average_price)); + }); + return feeds_stats; + } + + check_publication_feeds() { + const has_no_feed_check_configured = !('feeds_to_check' in this._config) || this._config.feeds_to_check.length == 0; + const is_not_time_to_check_feeds = this._last_feed_check != null && moment().diff(this._last_feed_check, 'minutes') < this._config.feed_checking_interval + if (has_no_feed_check_configured || is_not_time_to_check_feeds) { + return Promise.resolve(); + } + + return Apis.instance().db_api().exec('lookup_asset_symbols', [this._config.feeds_to_check]) + .then((assets) => { + const dynamic_asset_data_ids = assets.map(a => a['bitasset_data_id']); + return Apis.instance().db_api().exec('get_objects', [dynamic_asset_data_ids]); + }) + .then(dynamic_assets_data => { + this._last_feed_check = moment(); + this._feed_stats = this.extract_feed_data(this._config.feeds_to_check, dynamic_assets_data, this._witness_account); + this._feed_stats.forEach(feed_stat => { + this._logger.log(feed_stat.toString()); + if (feed_stat.publication_time == null) { + this.notify(`No publication found for ${feed_stat.name}.`); + } else { + if (feed_stat.since().as('minutes') > this._config.feed_publication_threshold) { + const price_feed_alert = [ + `More than ${this._config.feed_publication_threshold} minutes elapsed since last publication of ${feed_stat.name}.`, + feed_stat.toString() + ]; + this.notify(price_feed_alert.join('\n')); + } + } + }); + }) + .catch(error => { + this._logger.log(`Unable to retrieve feed stats: ${error}`); + throw error; + }); + } + + + check_missed_blocks() { + let missed = this._total_missed - this._start_missed; + this._logger.log('Total missed blocks: ' + this._total_missed); + this._logger.log('Missed since time window start: ' + missed); + if (missed > this._config.missed_block_threshold) { + const missing_block_alert = [ + `Missed blocks since start (${missed}) greater than threshold (${this._config.missed_block_threshold}).`, + 'Switching to backup witness server.' + ] + this.notify(missing_block_alert.join('\n')); + return this.update_signing_key(); + } else { + this._logger.log('Status: OK'); + } + return Promise.resolve(); + } + + check_activeness() { + return Apis.instance().db_api().exec('get_global_properties', []) + .then((global_properties) => { + const is_currently_active = global_properties.active_witnesses.includes(this._config.witness_id); + if (this._is_witness_active == null || this._is_witness_active != is_currently_active) { + if (this._is_witness_active != null) { + this.notify(`Witness ${this._config.witness_id} has been ${this._is_witness_active ? 'de' : ''}activated!`); + } + this._is_witness_active = is_currently_active; + } + return Promise.resolve(); + }) + .catch(error => { + this._logger.log(`Unable to retrieve global properties: ${error}`); + throw error; + }); + } + + start_monitoring() { + this._logger.log('Starting witness health monitor'); + this.emit('started'); + this.run_monitoring(); + } + + run_monitoring() { + if (!this._paused) { + this._checking = true; + + Apis.instance(this._config.api_node, true).init_promise.then(() => { + this._node_retries = 0; + this._logger.log('Connected to API node: ' + this._config.api_node); + return Apis.instance().db_api().exec('get_objects', [[this._config.witness_id]]).then((witness) => { + this._witness_account = witness[0].witness_account; + this._witness_url = witness[0].url; + this._witness_current_signing_key = witness[0].signing_key; + this._total_missed = witness[0].total_missed; + this._total_votes = witness[0].total_votes; + + const should_reset_window = moment().diff(this._window_start, 'seconds') >= this._config.reset_period + if (this._start_missed === null || should_reset_window) { + this.reset_missed_block_window() + } + + return Promise.all([this.check_activeness(), this.check_missed_blocks(), this.check_publication_feeds()]); + }); + + }).catch((error) => { + this._node_retries++; + this._logger.log(`API node unavailable: ${JSON.stringify(error, null, 4)}`); + if (this._node_retries > this._config.retries_threshold) { + this.notify('Unable to connect to API node for ' + this._node_retries + ' times.'); + } + }).then(() => { + this._check_witness_promise = setTimeout(() => this.run_monitoring(), this._config.checking_interval * 1000); + return Apis.close(); + }).then(() => { + this._checking = false; + this.emit('checked'); + }); + } + + } + + pause() { + this._paused = true; + } + + resume() { + this._paused = false; + this.reset_missed_block_window() + try { + clearTimeout(this._check_witness_promise); + } finally { + this._check_witness_promise = setTimeout(() => this.run_monitoring(), this._config.checking_interval * 1000); + } + + } +} + +module.exports = WitnessMonitor; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 633d38b..6e2f6ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,10 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.1.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" } }, "ansi-styles": { @@ -20,7 +20,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "requires": { - "color-convert": "1.9.1" + "color-convert": "^1.9.0" } }, "array.prototype.findindex": { @@ -28,8 +28,8 @@ "resolved": "https://registry.npmjs.org/array.prototype.findindex/-/array.prototype.findindex-2.0.2.tgz", "integrity": "sha1-WAaNJYh+9QXknckssAxE3O5VsGc=", "requires": { - "define-properties": "1.1.2", - "es-abstract": "1.12.0" + "define-properties": "^1.1.2", + "es-abstract": "^1.5.0" } }, "asn1": { @@ -67,12 +67,18 @@ "resolved": "https://registry.npmjs.org/babel-plugin-add-module-exports/-/babel-plugin-add-module-exports-0.2.1.tgz", "integrity": "sha1-mumh9KjcZ/DN7E9K7aHkOl/2XiU=" }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, "base-x": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.4.tgz", "integrity": "sha512-UYOadoSIkEI/VrRGSG6qp93rp2WdokiAiNYDfGW5qURAY8GiAQkvMbwNNSDYiVJopqv4gCna7xqf4rrNGp+5AA==", "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "^5.0.1" } }, "bcrypt-pbkdf": { @@ -81,7 +87,7 @@ "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", "optional": true, "requires": { - "tweetnacl": "0.14.5" + "tweetnacl": "^0.14.3" } }, "bigi": { @@ -105,7 +111,7 @@ "ecurve": "1.0.6", "event-emitter": "0.3.5", "immutable": "3.8.2", - "safe-buffer": "5.1.2", + "safe-buffer": "^5.1.2", "secure-random": "1.1.1" } }, @@ -114,7 +120,7 @@ "resolved": "https://registry.npmjs.org/bitsharesjs-ws/-/bitsharesjs-ws-1.5.4.tgz", "integrity": "sha512-34JYCgcEwJzA6L8EBIJ4SNAkiWy8vj3OrJHw6/OHYr4+ARctgkpDeHoPms0j6zqV4cIkR5X4RZdctnsm0aNH9Q==", "requires": { - "babel-plugin-add-module-exports": "0.2.1", + "babel-plugin-add-module-exports": "^0.2.1", "ws": "4.1.0" } }, @@ -123,8 +129,8 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "requires": { - "readable-stream": "2.3.6", - "safe-buffer": "5.1.2" + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" } }, "bluebird": { @@ -132,12 +138,28 @@ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, "bs58": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", "requires": { - "base-x": "3.0.4" + "base-x": "^3.0.2" } }, "bytebuffer": { @@ -145,7 +167,7 @@ "resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-5.0.1.tgz", "integrity": "sha1-WC7qSxqHO20CCkjVjfhfC7ps/d0=", "requires": { - "long": "3.2.0" + "long": "~3" } }, "caseless": { @@ -158,9 +180,9 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.4.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" } }, "cipher-base": { @@ -168,8 +190,8 @@ "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", "requires": { - "inherits": "2.0.3", - "safe-buffer": "5.1.2" + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" } }, "co": { @@ -182,7 +204,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", "requires": { - "color-name": "1.1.3" + "color-name": "^1.1.1" } }, "color-name": { @@ -195,9 +217,21 @@ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", "requires": { - "delayed-stream": "1.0.0" + "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -208,11 +242,11 @@ "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "requires": { - "cipher-base": "1.0.4", - "inherits": "2.0.3", - "md5.js": "1.3.4", - "ripemd160": "2.0.2", - "sha.js": "2.4.11" + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" } }, "create-hmac": { @@ -220,12 +254,12 @@ "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "requires": { - "cipher-base": "1.0.4", - "create-hash": "1.2.0", - "inherits": "2.0.3", - "ripemd160": "2.0.2", - "safe-buffer": "5.1.2", - "sha.js": "2.4.11" + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" } }, "crypto-js": { @@ -238,7 +272,7 @@ "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", "requires": { - "es5-ext": "0.10.45" + "es5-ext": "^0.10.9" } }, "dashdash": { @@ -246,7 +280,7 @@ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" } }, "debug": { @@ -267,8 +301,8 @@ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", "requires": { - "foreach": "2.0.5", - "object-keys": "1.0.11" + "foreach": "^2.0.5", + "object-keys": "^1.0.8" } }, "delayed-stream": { @@ -281,13 +315,19 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, "ecc-jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", "optional": true, "requires": { - "jsbn": "0.1.1" + "jsbn": "~0.1.0" } }, "ecurve": { @@ -295,8 +335,8 @@ "resolved": "https://registry.npmjs.org/ecurve/-/ecurve-1.0.6.tgz", "integrity": "sha512-/BzEjNfiSuB7jIWKcS/z8FK9jNjmEWvUV2YZ4RLSmcDtP7Lq0m6FvDuSnJpBlDpGRpfRQeTLGLBI8H+kEv0r+w==", "requires": { - "bigi": "1.4.2", - "safe-buffer": "5.1.2" + "bigi": "^1.1.0", + "safe-buffer": "^5.0.1" } }, "end-of-stream": { @@ -304,7 +344,7 @@ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", "requires": { - "once": "1.4.0" + "once": "^1.4.0" } }, "es-abstract": { @@ -312,11 +352,11 @@ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", "requires": { - "es-to-primitive": "1.1.1", - "function-bind": "1.1.1", - "has": "1.0.3", - "is-callable": "1.1.3", - "is-regex": "1.0.4" + "es-to-primitive": "^1.1.1", + "function-bind": "^1.1.1", + "has": "^1.0.1", + "is-callable": "^1.1.3", + "is-regex": "^1.0.4" } }, "es-to-primitive": { @@ -324,9 +364,9 @@ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", "requires": { - "is-callable": "1.1.3", - "is-date-object": "1.0.1", - "is-symbol": "1.0.1" + "is-callable": "^1.1.1", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.1" } }, "es5-ext": { @@ -334,9 +374,9 @@ "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.45.tgz", "integrity": "sha512-FkfM6Vxxfmztilbxxz5UKSD4ICMf5tSpRFtDNtkAhOxZ0EKtX6qwmXNyH/sFyIbX2P/nU5AMiA9jilWsUGJzCQ==", "requires": { - "es6-iterator": "2.0.3", - "es6-symbol": "3.1.1", - "next-tick": "1.0.0" + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.1", + "next-tick": "1" } }, "es6-iterator": { @@ -344,9 +384,9 @@ "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", "requires": { - "d": "1.0.0", - "es5-ext": "0.10.45", - "es6-symbol": "3.1.1" + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" } }, "es6-symbol": { @@ -354,8 +394,8 @@ "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", "requires": { - "d": "1.0.0", - "es5-ext": "0.10.45" + "d": "1", + "es5-ext": "~0.10.14" } }, "escape-string-regexp": { @@ -368,8 +408,8 @@ "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", "requires": { - "d": "1.0.0", - "es5-ext": "0.10.45" + "d": "1", + "es5-ext": "~0.10.14" } }, "eventemitter3": { @@ -417,11 +457,17 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", "requires": { - "asynckit": "0.4.0", + "asynckit": "^0.4.0", "combined-stream": "1.0.6", - "mime-types": "2.1.18" + "mime-types": "^2.1.12" } }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -432,9 +478,29 @@ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -445,8 +511,8 @@ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", "requires": { - "ajv": "5.5.2", - "har-schema": "2.0.0" + "ajv": "^5.1.0", + "har-schema": "^2.0.0" } }, "has": { @@ -454,7 +520,7 @@ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "requires": { - "function-bind": "1.1.1" + "function-bind": "^1.1.1" } }, "has-flag": { @@ -467,18 +533,24 @@ "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", "requires": { - "inherits": "2.0.3", - "safe-buffer": "5.1.2" + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" } }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.14.1" + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" } }, "immutable": { @@ -486,6 +558,16 @@ "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=" }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", @@ -506,7 +588,7 @@ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", "requires": { - "has": "1.0.3" + "has": "^1.0.1" } }, "is-symbol": { @@ -576,8 +658,8 @@ "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", "requires": { - "hash-base": "3.0.4", - "inherits": "2.0.3" + "hash-base": "^3.0.0", + "inherits": "^2.0.1" } }, "mime": { @@ -595,7 +677,16 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", "requires": { - "mime-db": "1.33.0" + "mime-db": "~1.33.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" } }, "minimist": { @@ -603,6 +694,47 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + } + }, + "moment": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -618,17 +750,17 @@ "resolved": "https://registry.npmjs.org/node-telegram-bot-api/-/node-telegram-bot-api-0.30.0.tgz", "integrity": "sha512-+EeM+fe3Xt81KIPqN3L6s6eK+FK4QaqyDcwCwkY/jqsleERXwwjGlVbf4lJCOZ0uJuF5PfqTmvVNtua7AZfBXg==", "requires": { - "array.prototype.findindex": "2.0.2", - "bl": "1.2.2", - "bluebird": "3.5.1", - "debug": "3.1.0", - "depd": "1.1.2", - "eventemitter3": "3.1.0", - "file-type": "3.9.0", - "mime": "1.6.0", - "pump": "2.0.1", - "request": "2.87.0", - "request-promise": "4.2.2" + "array.prototype.findindex": "^2.0.2", + "bl": "^1.2.1", + "bluebird": "^3.5.1", + "debug": "^3.1.0", + "depd": "^1.1.1", + "eventemitter3": "^3.0.0", + "file-type": "^3.9.0", + "mime": "^1.6.0", + "pump": "^2.0.0", + "request": "^2.83.0", + "request-promise": "^4.2.2" } }, "oauth-sign": { @@ -646,9 +778,15 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { - "wrappy": "1.0.2" + "wrappy": "1" } }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -664,8 +802,8 @@ "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", "requires": { - "end-of-stream": "1.4.1", - "once": "1.4.0" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, "punycode": { @@ -683,13 +821,13 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.2", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, "readline": { @@ -702,26 +840,26 @@ "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.7.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.2", - "har-validator": "5.0.3", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.18", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.2", - "safe-buffer": "5.1.2", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.2.1" + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.1", + "forever-agent": "~0.6.1", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" } }, "request-promise": { @@ -729,10 +867,10 @@ "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.2.tgz", "integrity": "sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=", "requires": { - "bluebird": "3.5.1", + "bluebird": "^3.5.0", "request-promise-core": "1.1.1", - "stealthy-require": "1.1.1", - "tough-cookie": "2.3.4" + "stealthy-require": "^1.1.0", + "tough-cookie": ">=2.3.3" } }, "request-promise-core": { @@ -740,7 +878,7 @@ "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", "requires": { - "lodash": "4.17.10" + "lodash": "^4.13.1" } }, "ripemd160": { @@ -748,8 +886,8 @@ "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", "requires": { - "hash-base": "3.0.4", - "inherits": "2.0.3" + "hash-base": "^3.0.0", + "inherits": "^2.0.1" } }, "safe-buffer": { @@ -767,8 +905,8 @@ "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "requires": { - "inherits": "2.0.3", - "safe-buffer": "5.1.2" + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" } }, "sshpk": { @@ -776,14 +914,14 @@ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "tweetnacl": "~0.14.0" } }, "stealthy-require": { @@ -796,7 +934,7 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "~5.1.0" } }, "supports-color": { @@ -804,7 +942,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "requires": { - "has-flag": "3.0.0" + "has-flag": "^3.0.0" } }, "tough-cookie": { @@ -812,7 +950,7 @@ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", "requires": { - "punycode": "1.4.1" + "punycode": "^1.4.1" } }, "tunnel-agent": { @@ -820,7 +958,7 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "^5.0.1" } }, "tweetnacl": { @@ -839,14 +977,19 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" }, + "validate.js": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/validate.js/-/validate.js-0.12.0.tgz", + "integrity": "sha512-/x2RJSvbqEyxKj0RPN4xaRquK+EggjeVXiDDEyrJzsJogjtiZ9ov7lj/svVb4DM5Q5braQF4cooAryQbUwOxlA==" + }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "requires": { - "assert-plus": "1.0.0", + "assert-plus": "^1.0.0", "core-util-is": "1.0.2", - "extsprintf": "1.3.0" + "extsprintf": "^1.2.0" } }, "wrappy": { @@ -859,8 +1002,8 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz", "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==", "requires": { - "async-limiter": "1.0.0", - "safe-buffer": "5.1.2" + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0" } } } diff --git a/package.json b/package.json index 099b600..b537d25 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "mocha" }, "author": "", "license": "ISC", @@ -12,7 +12,12 @@ "bitsharesjs": "^1.7.9", "chalk": "^2.4.1", "minimist": "^1.2.0", + "moment": "^2.22.2", "node-telegram-bot-api": "^0.30.0", - "readline": "^1.3.0" + "readline": "^1.3.0", + "validate.js": "^0.12.0" + }, + "devDependencies": { + "mocha": "^5.2.0" } } diff --git a/test/test_config_validation.js b/test/test_config_validation.js new file mode 100644 index 0000000..860d42b --- /dev/null +++ b/test/test_config_validation.js @@ -0,0 +1,84 @@ +var assert = require('assert'); +var validate_config = require('../lib/ValidateConfig.js') + +const valid_config = { + "witness_id": "1.6.123", + "api_node": "wss://bitshares.org", + "private_key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "missed_block_threshold": 3, + "checking_interval": 10, + "reset_period": 300, + "witness_signing_keys": [ + "BTSXXXXXXXXXXXXXXXXXXXXXXX", + "TESTXXXXXXXXXXXXXXXXXXXXXX" + ], + "recap_time": 60, + "debug_level": 3, + "telegram_token": "XXXXXXX:YYYYYYYYYY", + "telegram_authorized_users": ["1234"], + "retries_threshold": 3, + "feeds_to_check" : ["HERTZ", "USD"], + "feed_publication_threshold": 60, + "feed_checking_interval": 10 +} + + +describe('#validate_config()', function() { + it('should not find errors', function() { + assert.equal(validate_config(valid_config), undefined, "Should be a valid config."); + }); + + for (let field in valid_config) { + if (!['feeds_to_check'].includes(field)) { + it(`should detect no ${field}`, function() { + var config = Object.assign({}, valid_config); + delete config[field]; + const validation_result = validate_config(config); + assert(field in validation_result); + }); + } + } + + it('should detect bad witness_id format', function() { + var config = Object.assign({}, valid_config); + config.witness_id = 'witness.me'; + assert('witness_id' in validate_config(config)); + }); + + it('should detect bad api_node format', function() { + var config = Object.assign({}, valid_config); + config.api_node = 'http://bitshares.org/rpc'; + assert('api_node' in validate_config(config)); + }); + + it('should detect empty private keys', function() { + var config = Object.assign({}, valid_config); + config.private_key = ''; + assert('private_key' in validate_config(config)); + }); + + it('should detect invalid missed block threshold', function() { + var config = Object.assign({}, valid_config); + config.missed_block_threshold = 0; + assert('missed_block_threshold' in validate_config(config)); + }); + + it('should detect not enough signing keys (as array)', function() { + var config = Object.assign({}, valid_config); + config.witness_signing_keys = ['BTSXXXXXXXXXX']; + assert('witness_signing_keys' in validate_config(config)); + }); + + it('should detect not enough signing keys (as string)', function() { + var config = Object.assign({}, valid_config); + config.witness_signing_keys = 'BTSXXXXXXXXXX'; + assert('witness_signing_keys' in validate_config(config)); + }); + + it('should detect bad signing keys', function() { + var config = Object.assign({}, valid_config); + config.witness_signing_keys = [ 'BTSXXXXXXXXXX', 'BTCXXXXXXXX']; + assert('witness_signing_keys' in validate_config(config)); + }); + +}); \ No newline at end of file