From b3ec7e4e5c737119368a4eaa70c0da00419e2d8b Mon Sep 17 00:00:00 2001 From: Zapata Date: Sun, 8 Jul 2018 02:55:43 +0200 Subject: [PATCH 01/15] Clean code - factorize code (authen, error handling in promise, update signing key, send recap) - remove config variables - use template strings - consistent code style --- index.js | 410 +++++++++++++++++++++++++------------------------------ 1 file changed, 183 insertions(+), 227 deletions(-) diff --git a/index.js b/index.js index 638d9b7..d8ee715 100644 --- a/index.js +++ b/index.js @@ -5,67 +5,115 @@ const {Apis} = require('bitsharesjs-ws'); const {PrivateKey,TransactionBuilder} = require('bitsharesjs'); const config = require('./config.json'); -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; +const logger = new Logger(config.debug_level); +const bot = new TelegramBot(config.telegram_token, {polling: true}); +const pKey = PrivateKey.fromWif(config.private_key); + +var paused = false; +var check_witness_promise; +var admin_id = null; +var total_missed = null; +var start_missed = null; +var node_retries = 0; +var window_start = 0; +var checking = false; +var witness_account; +var lastupdate = 0; + +function isAuthenticated(chatId) { + if (admin_id != chatId) { + bot.sendMessage(chatId, "You need to authenticate first."); + return false; + } + return true; +} + +function reset_missed_block_window() { + start_missed = total_missed; + window_start = Date.now(); +} + +function update_signing_key(recipient_id) { + let tr = new TransactionBuilder(); + tr.add_type_operation('witness_update', { + fee: { + amount: 0, + asset_id: '1.3.0' + }, + witness: config.witness_id, + witness_account: witness_account, + new_url: '', + new_signing_key: config.backup_key + }); + + return tr.set_required_fees().then(() => { + tr.add_signer(pKey, pKey.toPublicKey().toPublicKeyString()); + return tr.broadcast(); + }) + .then(() => { + logger.log('Signing key updated'); + bot.sendMessage(recipient_id, 'Signing key updated. Use /new_key to set the next backup key.'); + reset_missed_block_window(); + }).catch(() => { + logger.log('Could not broadcast update_witness tx.'); + bot.sendMessage(recipient_id, 'Could not broadcast update_witness tx. Please check!'); + }); + +} + +function send_recap(recipient_id) { + bot.sendMessage(recipient_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\` +API node: \`${config.api_node}\` +Backup signing key: \`${config.backup_key}\` +Recap time period: \`${config.recap_time} min\` +Total missed blocks: \`${total_missed}\` +Missed blocks in current time window: \`${total_missed - start_missed}\``,{ + parse_mode: "Markdown" + }); + +} + +bot.on('polling_error', (error) => { + logger.error(error); +}); bot.onText(/\/pass (.+)/, (msg, match) => { const chatId = msg.from.id; const pass = match[1]; - if (pass == password) { - bot.sendMessage(chatId, 'Password accepted.'); + if (pass == config.telegram_password) { admin_id = chatId; + bot.sendMessage(chatId, `Password accepted. New admin is ${admin_id}`); } else { bot.sendMessage(chatId, 'Password incorrect.'); } }); + bot.onText(/\/changepass (.+)/, (msg, match) => { const chatId = msg.from.id; const pass = match[1]; - if (admin_id == chatId) { - password=pass; + if (isAuthenticated(chatId)) { + config.telegram_password = pass; bot.sendMessage(chatId, 'Password changed. Please authenticate again with /pass .'); - admin_id = 0; - } else { - bot.sendMessage(chatId, "You need to authenticate first."); + admin_id = null; } }); + 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 (isAuthenticated(chatId)) { + reset_missed_block_window(); + bot.sendMessage(chatId, 'Session missed block counter set to 0.'); } }); @@ -74,168 +122,126 @@ bot.onText(/\/new_key (.+)/, (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."); + + if (isAuthenticated(chatId)) { + config.backup_key = key; + bot.sendMessage(chatId, `Backup signing key set to: ${config.backup_key}`); } }); + 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 (isAuthenticated(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 (isAuthenticated(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 (isAuthenticated(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 (isAuthenticated(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 (isAuthenticated(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 (isAuthenticated(chatId)) { + config.checking_interval = new_int; + bot.sendMessage(chatId, `Checking interval set to: ${config.checking_interval}s.`); } - + }); -bot.onText(/\/stats/, (msg, match) => { +bot.onText(/\/stats/, (msg, match) => { const chatId = msg.chat.id; - - 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."); + + if (isAuthenticated(chatId)) { + send_recap(chatId); } - + }); + bot.onText(/\/pause/, (msg, match) => { const chatId = msg.chat.id; - if (admin_id == chatId) { - paused=true; - bot.sendMessage(chatId, "Witness monitoring paused. Use /resume to resume monitoring."); - - } else { - bot.sendMessage(chatId, "You need to authenticate first."); + if (isAuthenticated(chatId)) { + paused = true; + 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..."); + if (isAuthenticated(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(); - } - }); - }); - },() => { + Apis.instance(config.api_node, true).init_promise.then(() => { + return update_signing_key(chatId); + }).catch(() => { logger.log('Could not update signing key.'); - bot.sendMessage(chatId, "Could not update signing key. Please check!"); + bot.sendMessage(chatId, 'Could not update signing key. Please check!'); + }).then(() => { + if (paused || !checking) { + return Apis.close(); + } }); - } else { - bot.sendMessage(chatId, "You need to authenticate first."); } }); @@ -243,123 +249,73 @@ bot.onText(/\/resume/, (msg, match) => { const chatId = msg.chat.id; - if (admin_id == chatId) { - paused=false; - window_start=Date.now(); + if (isAuthenticated(chatId)) { + paused = false; + window_start = Date.now(); try { - clearTimeout(to); - to=setTimeout(checkWitness, interval*1000); - }catch(e){ - to=setTimeout(checkWitness, interval*1000); + clearTimeout(check_witness_promise); + } finally { + check_witness_promise = setTimeout(checkWitness, config.checking_interval * 1000); } - bot.sendMessage(chatId, "Witness monitoring resumed."); - } else { - bot.sendMessage(chatId, "You need to authenticate first."); + 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 + checking = true; + Apis.instance(config.api_node, true).init_promise.then(() => { + node_retries = 0; + logger.log('Connected to API node: ' + config.api_node); + return Apis.instance().db_api().exec('get_objects', [ + [config.witness_id], false ]).then((witness) => { - if (first) { - start_missed = witness[0].total_missed; - window_start=Date.now(); - first = false; + witness_account = witness[0].witness_account; + total_missed = witness[0].total_missed; + + const should_reset_window = Math.floor((Date.now() - window_start) / 1000) >= config.reset_period + if (start_missed === null || should_reset_window) { + reset_missed_block_window() } - 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" - }); + + if ((admin_id != null) && (config.recap_time > 0)) { + if (Math.floor((Date.now() - lastupdate) / 60000) >= config.recap_time) { + lastupdate = Date.now(); + send_recap(admin_id); } } - 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...'); + if (missed > config.missed_block_threshold) { + logger.log(`Missed blocks since time window start (${missed}) greater than threshold (${config.missed_block_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, `Missed blocks since start (${missed}) greater than threshold (${config.missed_block_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; - }); - }); - + return update_signing_key(); } else { logger.log('Status: OK'); - to=setTimeout(checkWitness, interval*1000); - Apis.close(); - checking=false; } }); - }, () => { - + }).catch(() => { + 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.'); + if (node_retries > config.retries_threshold) { + 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; - }); + }).then(() => { + check_witness_promise = setTimeout(checkWitness, config.checking_interval * 1000); + return Apis.close(); + }).then(() => checking = false); } } \ No newline at end of file From a77f760daab3ddf70465b23a000bcbe08a5a64d0 Mon Sep 17 00:00:00 2001 From: Zapata Date: Sun, 8 Jul 2018 03:45:10 +0200 Subject: [PATCH 02/15] Improve documentation - review README layout - add /start, /help - add instructions to get the completion. --- README.md | 137 +++++++++++++++++++----------------------------------- index.js | 26 +++++++++++ 2 files changed, 75 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index 7b798b2..10b0dfd 100644 --- a/README.md +++ b/README.md @@ -35,40 +35,18 @@ Open config-sample.json in your favourite text editor and edit with your own set 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 | +| --- | --- | +| `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. | +| `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. Not currently used. | +| `telegram_token` | The telegram access token for your notifications bot. You can create one with [BotFather](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. | ## Running @@ -112,58 +90,41 @@ 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. +- `/start`: Introduction message. +- `/help`: Get the list of available commands. +- `/pass ` : Required to authenticate, otherwise no command will work. +- `/changepass `: Update your telegram access password and requires you to authenticate again using `/pass` +- `/stats`: Return the current configuration and statistics of the monitoring session. +- `/switch`: IMMEDIATELY update your signing key to the currently configured backup key. +- `/new_key `: Set a new backup key in place of the configured one. +- `/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. +- `/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: -`/new_node wss://` - -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 +pass - Authenticate +changepass - Update authentication password +stats - Gather statistics +switch - Update signing key to backup +new_key - Set a new backup key +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 +reset - Reset the missed blocks counter +pause - Pause monitoring +resume - Resume monitoring +``` \ No newline at end of file diff --git a/index.js b/index.js index d8ee715..70d80d9 100644 --- a/index.js +++ b/index.js @@ -80,6 +80,32 @@ bot.on('polling_error', (error) => { logger.error(error); }); + +bot.onText(/\/start/, (msg) => { + bot.sendMessage(msg.from.id, 'Hello, please authentificate first with `/pass`.', + { parse_mode: "Markdown" }); +}); + +bot.onText(/\/help/, (msg) => { + bot.sendMessage(msg.from.id, +`\`/pass \` : Required to authenticate, otherwise no command will work. +\`/changepass \`: Update your telegram access password and requires you to authenticate again using \`/pass\` +\`/stats\`: Return the current configuration and statistics of the monitoring session. +\`/switch\`: IMMEDIATELY update your signing key to the currently configured backup key. +\`/new_key \`: Set a new backup key in place of the configured one. +\`/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. +\`/reset\` : Reset the missed blocks counter in the current time-window. +\`/pause\` : Pause monitoring. +\`/resume\`: Resume monitoring.`, + { parse_mode: "Markdown" }); +}); + bot.onText(/\/pass (.+)/, (msg, match) => { const chatId = msg.from.id; From 4a487a943c1930b80a91453afab0821fabc4f44d Mon Sep 17 00:00:00 2001 From: Zapata Date: Sun, 8 Jul 2018 22:43:58 +0200 Subject: [PATCH 03/15] Add price feed check. --- index.js | 134 ++++++++++++++------- package-lock.json | 291 ++++++++++++++++++++++++---------------------- package.json | 1 + 3 files changed, 248 insertions(+), 178 deletions(-) diff --git a/index.js b/index.js index 70d80d9..9bbad6d 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ const Logger = require('./lib/Logger.js'); const {Apis} = require('bitsharesjs-ws'); const {PrivateKey,TransactionBuilder} = require('bitsharesjs'); const config = require('./config.json'); +const moment = require('moment'); const logger = new Logger(config.debug_level); const bot = new TelegramBot(config.telegram_token, {polling: true}); @@ -15,10 +16,12 @@ var admin_id = null; var total_missed = null; var start_missed = null; var node_retries = 0; -var window_start = 0; +var window_start = null; var checking = false; var witness_account; -var lastupdate = 0; +var last_recap_send = null; +var last_publication_times = null; +var last_feed_check = null; function isAuthenticated(chatId) { if (admin_id != chatId) { @@ -30,7 +33,7 @@ function isAuthenticated(chatId) { function reset_missed_block_window() { start_missed = total_missed; - window_start = Date.now(); + window_start = moment(); } function update_signing_key(recipient_id) { @@ -62,17 +65,20 @@ function update_signing_key(recipient_id) { } function send_recap(recipient_id) { - bot.sendMessage(recipient_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\` -API node: \`${config.api_node}\` -Backup signing key: \`${config.backup_key}\` -Recap time period: \`${config.recap_time} min\` -Total missed blocks: \`${total_missed}\` -Missed blocks in current time window: \`${total_missed - start_missed}\``,{ - parse_mode: "Markdown" - }); + const stats = [ + `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\``, + `API node: \`${config.api_node}\``, + `Backup signing key: \`${config.backup_key}\``, + `Recap time period: \`${config.recap_time} min\``, + `Total missed blocks: \`${total_missed}\``, + `Missed blocks in current time window: \`${total_missed - start_missed}\``, + `Feeds to check: \`${config.feeds_to_check}\``, + `Last publication times: \`${last_publication_times}\`` + ] + bot.sendMessage(recipient_id, stats.join('\n'), { parse_mode: 'Markdown' }); } @@ -261,8 +267,7 @@ bot.onText(/\/switch/, (msg, match) => { Apis.instance(config.api_node, true).init_promise.then(() => { return update_signing_key(chatId); }).catch(() => { - logger.log('Could not update signing key.'); - bot.sendMessage(chatId, 'Could not update signing key. Please check!'); + notify(admin_id, 'Could not update signing key.'); }).then(() => { if (paused || !checking) { return Apis.close(); @@ -277,7 +282,7 @@ bot.onText(/\/resume/, (msg, match) => { if (isAuthenticated(chatId)) { paused = false; - window_start = Date.now(); + window_start = moment(); try { clearTimeout(check_witness_promise); } finally { @@ -289,6 +294,71 @@ bot.onText(/\/resume/, (msg, match) => { }); +function notify(recipient_id, msg) { + logger.log(msg); + if (recipient_id != null) { + bot.setMessage(recipient_id, msg); + } +} + +function find_last_publication_time(dynamic_assets_data, witness_account) { + return dynamic_assets_data.map(dynamic_assets_data => { + for (const feed of dynamic_assets_data['feeds']) { + if (feed[0] == witness_account) { + return feed[1][0]; + } + } + return null; + }); +} + +function check_publication_feeds() { + const has_no_feed_check_configured = !('feeds_to_check' in config) || config.feeds_to_check.length == 0; + const is_not_time_to_check_feeds = last_feed_check != null && moment().diff(last_feed_check, 'minutes') < 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', [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 => { + last_feed_check = moment(); + last_publication_times = find_last_publication_time(dynamic_assets_data, witness_account); + const formatted_result = config.feeds_to_check.map((x, i) => `${x} (${last_publication_times[i]})`) + logger.log(`Publication times: ${formatted_result.join(', ')}`); + for (let i = 0; i < config.feeds_to_check.length; ++i) { + if (last_publication_times[i] == null) { + notify(admin_id, `No publication found for ${config.feeds_to_check[i]}.`); + } else { + const minutes_since_last_publication = moment.utc().diff(moment.utc(last_publication_times[i]), 'minutes') + if (minutes_since_last_publication > config.feed_publication_threshold) { + notify(admin_id, `More than ${config.feed_publication_threshold} minutes elapsed since last publication of ${config.feeds_to_check[i]}.`); + notify(admin_id, `Last publication happened at ${moment.utc(last_publication_times[i]).local().format()}, ${minutes_since_last_publication} minutes ago.`); + } + } + } + }); + +} + +function check_missed_blocks() { + let missed = total_missed - start_missed; + logger.log('Total missed blocks: ' + total_missed); + logger.log('Missed since time window start: ' + missed); + if (missed > config.missed_block_threshold) { + notify(admin_id, `Missed blocks since start (${missed}) greater than threshold (${config.missed_block_threshold}).`); + notify(admin_id, 'Switching to backup witness server.'); + return update_signing_key(); + } else { + logger.log('Status: OK'); + } + return Promise.resolve(); +} + + logger.log('Starting witness health monitor'); checkWitness(); @@ -299,45 +369,31 @@ function checkWitness() { Apis.instance(config.api_node, true).init_promise.then(() => { node_retries = 0; logger.log('Connected to API node: ' + config.api_node); - return Apis.instance().db_api().exec('get_objects', [ - [config.witness_id], false - ]).then((witness) => { + return Apis.instance().db_api().exec('get_objects', [[config.witness_id]]).then((witness) => { witness_account = witness[0].witness_account; total_missed = witness[0].total_missed; - const should_reset_window = Math.floor((Date.now() - window_start) / 1000) >= config.reset_period + const should_reset_window = moment().diff(window_start, 'seconds') >= config.reset_period if (start_missed === null || should_reset_window) { reset_missed_block_window() } if ((admin_id != null) && (config.recap_time > 0)) { - if (Math.floor((Date.now() - lastupdate) / 60000) >= config.recap_time) { - lastupdate = Date.now(); + if (moment().diff(last_recap_send, 'minutes') >= config.recap_time) { + last_recap_send = moment(); send_recap(admin_id); } } - let missed = total_missed - start_missed; - logger.log('Total missed blocks: ' + total_missed); - logger.log('Missed since time window start: ' + missed); - if (missed > config.missed_block_threshold) { - logger.log(`Missed blocks since time window start (${missed}) greater than threshold (${config.missed_block_threshold}). Notifying...`); - logger.log('Switching to backup witness server.'); - bot.sendMessage(admin_id, `Missed blocks since start (${missed}) greater than threshold (${config.missed_block_threshold}).`); - bot.sendMessage(admin_id, 'Switching to backup witness server.'); - return update_signing_key(); - } else { - logger.log('Status: OK'); - } + return Promise.all([check_missed_blocks(), check_publication_feeds()]); }); - }).catch(() => { - + }).catch((error) => { + console.log(JSON.stringify(error, null, 4)); node_retries++; logger.log('API node unavailable.'); if (node_retries > config.retries_threshold) { - 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.'); + notify(admin_id, 'Unable to connect to API node for ' + node_retries + ' times.'); } }).then(() => { check_witness_promise = setTimeout(checkWitness, config.checking_interval * 1000); diff --git a/package-lock.json b/package-lock.json index 633d38b..d55ea8b 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": { @@ -72,7 +72,7 @@ "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 +81,7 @@ "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", "optional": true, "requires": { - "tweetnacl": "0.14.5" + "tweetnacl": "^0.14.3" } }, "bigi": { @@ -105,7 +105,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 +114,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 +123,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": { @@ -137,7 +137,7 @@ "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 +145,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 +158,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 +168,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 +182,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,7 +195,7 @@ "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" } }, "core-util-is": { @@ -208,11 +208,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 +220,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 +238,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 +246,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 +267,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": { @@ -287,7 +287,7 @@ "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", "optional": true, "requires": { - "jsbn": "0.1.1" + "jsbn": "~0.1.0" } }, "ecurve": { @@ -295,8 +295,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 +304,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 +312,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 +324,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 +334,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 +344,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 +354,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 +368,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,9 +417,9 @@ "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" } }, "function-bind": { @@ -432,7 +432,7 @@ "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" } }, "har-schema": { @@ -445,8 +445,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 +454,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,8 +467,8 @@ "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" } }, "http-signature": { @@ -476,9 +476,9 @@ "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": { @@ -506,7 +506,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 +576,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 +595,7 @@ "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" } }, "minimist": { @@ -603,6 +603,19 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, + "moment": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" + }, + "moment-timezone": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.21.tgz", + "integrity": "sha512-j96bAh4otsgj3lKydm3K7kdtA3iKf2m6MY2iSYCzCm5a1zmHo1g+aK3068dDEeocLZQIS9kU8bsdQHLqEvgW0A==", + "requires": { + "moment": ">= 2.9.0" + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -618,17 +631,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,7 +659,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { - "wrappy": "1.0.2" + "wrappy": "1" } }, "performance-now": { @@ -664,8 +677,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 +696,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 +715,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 +742,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 +753,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 +761,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 +780,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 +789,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 +809,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 +817,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 +825,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 +833,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": { @@ -844,9 +857,9 @@ "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 +872,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..49bf61a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "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" } From 73d539a2af8f80589800e2b2d3f80c8b4a623eb3 Mon Sep 17 00:00:00 2001 From: Zapata Date: Mon, 9 Jul 2018 01:37:48 +0200 Subject: [PATCH 04/15] Clean feed check implementation. - Split bot and witness monitoring in 2 files for better lisibility. - Add feed check related commands. - Add feed check related documentation. --- README.md | 15 ++- index.js | 287 ++++++++++++------------------------------ lib/WitnessMonitor.js | 218 ++++++++++++++++++++++++++++++++ 3 files changed, 313 insertions(+), 207 deletions(-) create mode 100644 lib/WitnessMonitor.js diff --git a/README.md b/README.md index 10b0dfd..87b6184 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,9 @@ and then save as config.json | `telegram_token` | The telegram access token for your notifications bot. You can create one with [BotFather](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. | +| `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 @@ -103,9 +106,12 @@ Open a chat to your bot and use the following: - `/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. -- `/reset` : Reset the missed blocks counter in the current time-window. -- `/pause` : Pause monitoring. -- `/resume`: Resume monitoring. +- `/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: @@ -124,6 +130,9 @@ 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 diff --git a/index.js b/index.js index 9bbad6d..0a65b3f 100644 --- a/index.js +++ b/index.js @@ -1,27 +1,15 @@ 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 config = require('./config.json'); const moment = require('moment'); +const config = require('./config.json'); +const Logger = require('./lib/Logger.js'); +const WitnessMonitor = require('./lib/WitnessMonitor.js') const logger = new Logger(config.debug_level); const bot = new TelegramBot(config.telegram_token, {polling: true}); -const pKey = PrivateKey.fromWif(config.private_key); -var paused = false; -var check_witness_promise; var admin_id = null; -var total_missed = null; -var start_missed = null; -var node_retries = 0; -var window_start = null; -var checking = false; -var witness_account; var last_recap_send = null; -var last_publication_times = null; -var last_feed_check = null; function isAuthenticated(chatId) { if (admin_id != chatId) { @@ -31,55 +19,8 @@ function isAuthenticated(chatId) { return true; } -function reset_missed_block_window() { - start_missed = total_missed; - window_start = moment(); -} - -function update_signing_key(recipient_id) { - let tr = new TransactionBuilder(); - tr.add_type_operation('witness_update', { - fee: { - amount: 0, - asset_id: '1.3.0' - }, - witness: config.witness_id, - witness_account: witness_account, - new_url: '', - new_signing_key: config.backup_key - }); - - return tr.set_required_fees().then(() => { - tr.add_signer(pKey, pKey.toPublicKey().toPublicKeyString()); - return tr.broadcast(); - }) - .then(() => { - logger.log('Signing key updated'); - bot.sendMessage(recipient_id, 'Signing key updated. Use /new_key to set the next backup key.'); - reset_missed_block_window(); - }).catch(() => { - logger.log('Could not broadcast update_witness tx.'); - bot.sendMessage(recipient_id, 'Could not broadcast update_witness tx. Please check!'); - }); - -} - function send_recap(recipient_id) { - const stats = [ - `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\``, - `API node: \`${config.api_node}\``, - `Backup signing key: \`${config.backup_key}\``, - `Recap time period: \`${config.recap_time} min\``, - `Total missed blocks: \`${total_missed}\``, - `Missed blocks in current time window: \`${total_missed - start_missed}\``, - `Feeds to check: \`${config.feeds_to_check}\``, - `Last publication times: \`${last_publication_times}\`` - ] - bot.sendMessage(recipient_id, stats.join('\n'), { parse_mode: 'Markdown' }); - + bot.sendMessage(recipient_id, witness_monitor.format_recap(), { parse_mode: 'Markdown' }); } bot.on('polling_error', (error) => { @@ -93,23 +34,27 @@ bot.onText(/\/start/, (msg) => { }); bot.onText(/\/help/, (msg) => { - bot.sendMessage(msg.from.id, -`\`/pass \` : Required to authenticate, otherwise no command will work. -\`/changepass \`: Update your telegram access password and requires you to authenticate again using \`/pass\` -\`/stats\`: Return the current configuration and statistics of the monitoring session. -\`/switch\`: IMMEDIATELY update your signing key to the currently configured backup key. -\`/new_key \`: Set a new backup key in place of the configured one. -\`/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. -\`/reset\` : Reset the missed blocks counter in the current time-window. -\`/pause\` : Pause monitoring. -\`/resume\`: Resume monitoring.`, - { parse_mode: "Markdown" }); + const help = [ + `\`/pass \` : Required to authenticate, otherwise no command will work.`, + `\`/changepass \`: Update your telegram access password and requires you to authenticate again using \`/pass\``, + `\`/stats\`: Return the current configuration and statistics of the monitoring session.`, + `\`/switch\`: IMMEDIATELY update your signing key to the currently configured backup key.`, + `\`/new_key \`: Set a new backup key in place of the configured one.`, + `\`/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(/\/pass (.+)/, (msg, match) => { @@ -144,7 +89,7 @@ bot.onText(/\/reset/, (msg, match) => { const chatId = msg.chat.id; if (isAuthenticated(chatId)) { - reset_missed_block_window(); + witness_monitor.reset_missed_block_window(); bot.sendMessage(chatId, 'Session missed block counter set to 0.'); } @@ -246,12 +191,53 @@ bot.onText(/\/stats/, (msg, match) => { }); +bot.onText(/\/feed_checking_interval (.+)/, (msg, match) => { + + const chatId = msg.chat.id; + const new_int = match[1]; + + if (isAuthenticated(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(/\/feed_publication_threshold (.+)/, (msg, match) => { + + const chatId = msg.chat.id; + const new_threshold = match[1]; + + if (isAuthenticated(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.`); + } + +}); + +bot.onText(/\/feeds (.+)/, (msg, match) => { + + const chatId = msg.chat.id; + const new_feeds = match[1].split(' '); + + if (isAuthenticated(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 (isAuthenticated(chatId)) { - paused = true; + witness_monitor.pause(); bot.sendMessage(chatId, 'Witness monitoring paused. Use /resume to resume monitoring.'); } @@ -263,16 +249,7 @@ bot.onText(/\/switch/, (msg, match) => { if (isAuthenticated(chatId)) { bot.sendMessage(chatId, 'Attempting to update signing key...'); - logger.log('Received key update request.'); - Apis.instance(config.api_node, true).init_promise.then(() => { - return update_signing_key(chatId); - }).catch(() => { - notify(admin_id, 'Could not update signing key.'); - }).then(() => { - if (paused || !checking) { - return Apis.close(); - } - }); + witness_monitor.force_update_signing_key(); } }); @@ -281,123 +258,25 @@ bot.onText(/\/resume/, (msg, match) => { const chatId = msg.chat.id; if (isAuthenticated(chatId)) { - paused = false; - window_start = moment(); - try { - clearTimeout(check_witness_promise); - } finally { - check_witness_promise = setTimeout(checkWitness, config.checking_interval * 1000); - } + witness_monitor.resume(); bot.sendMessage(chatId, 'Witness monitoring resumed.'); } - }); -function notify(recipient_id, msg) { - logger.log(msg); - if (recipient_id != null) { - bot.setMessage(recipient_id, msg); - } -} -function find_last_publication_time(dynamic_assets_data, witness_account) { - return dynamic_assets_data.map(dynamic_assets_data => { - for (const feed of dynamic_assets_data['feeds']) { - if (feed[0] == witness_account) { - return feed[1][0]; - } - } - return null; - }); -} - -function check_publication_feeds() { - const has_no_feed_check_configured = !('feeds_to_check' in config) || config.feeds_to_check.length == 0; - const is_not_time_to_check_feeds = last_feed_check != null && moment().diff(last_feed_check, 'minutes') < 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', [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 => { - last_feed_check = moment(); - last_publication_times = find_last_publication_time(dynamic_assets_data, witness_account); - const formatted_result = config.feeds_to_check.map((x, i) => `${x} (${last_publication_times[i]})`) - logger.log(`Publication times: ${formatted_result.join(', ')}`); - for (let i = 0; i < config.feeds_to_check.length; ++i) { - if (last_publication_times[i] == null) { - notify(admin_id, `No publication found for ${config.feeds_to_check[i]}.`); - } else { - const minutes_since_last_publication = moment.utc().diff(moment.utc(last_publication_times[i]), 'minutes') - if (minutes_since_last_publication > config.feed_publication_threshold) { - notify(admin_id, `More than ${config.feed_publication_threshold} minutes elapsed since last publication of ${config.feeds_to_check[i]}.`); - notify(admin_id, `Last publication happened at ${moment.utc(last_publication_times[i]).local().format()}, ${minutes_since_last_publication} minutes ago.`); - } - } - } - }); - -} - -function check_missed_blocks() { - let missed = total_missed - start_missed; - logger.log('Total missed blocks: ' + total_missed); - logger.log('Missed since time window start: ' + missed); - if (missed > config.missed_block_threshold) { - notify(admin_id, `Missed blocks since start (${missed}) greater than threshold (${config.missed_block_threshold}).`); - notify(admin_id, 'Switching to backup witness server.'); - return update_signing_key(); - } else { - logger.log('Status: OK'); +const witness_monitor = new WitnessMonitor(config, logger); +witness_monitor.on('notify', (msg) => { + if (admin_id != null) { + bot.sendMessage(admin_id, msg); } - return Promise.resolve(); -} - - -logger.log('Starting witness health monitor'); -checkWitness(); - -function checkWitness() { - - if (!paused) { - checking = true; - Apis.instance(config.api_node, true).init_promise.then(() => { - node_retries = 0; - logger.log('Connected to API node: ' + config.api_node); - return Apis.instance().db_api().exec('get_objects', [[config.witness_id]]).then((witness) => { - witness_account = witness[0].witness_account; - total_missed = witness[0].total_missed; - - const should_reset_window = moment().diff(window_start, 'seconds') >= config.reset_period - if (start_missed === null || should_reset_window) { - reset_missed_block_window() - } - - if ((admin_id != null) && (config.recap_time > 0)) { - if (moment().diff(last_recap_send, 'minutes') >= config.recap_time) { - last_recap_send = moment(); - send_recap(admin_id); - } - } - - return Promise.all([check_missed_blocks(), check_publication_feeds()]); - }); - - }).catch((error) => { - console.log(JSON.stringify(error, null, 4)); - node_retries++; - logger.log('API node unavailable.'); - if (node_retries > config.retries_threshold) { - notify(admin_id, 'Unable to connect to API node for ' + node_retries + ' times.'); - } - }).then(() => { - check_witness_promise = setTimeout(checkWitness, config.checking_interval * 1000); - return Apis.close(); - }).then(() => checking = false); +}); +witness_monitor.on('checked', () => { + if ((admin_id != null) && (config.recap_time > 0)) { + if (moment().diff(last_recap_send, 'minutes') >= config.recap_time) { + last_recap_send = moment(); + send_recap(admin_id); + } } -} \ No newline at end of file +}); +witness_monitor.start_monitoring(); \ No newline at end of file diff --git a/lib/WitnessMonitor.js b/lib/WitnessMonitor.js new file mode 100644 index 0000000..aaa4073 --- /dev/null +++ b/lib/WitnessMonitor.js @@ -0,0 +1,218 @@ +const EventEmitter = require('events'); +const {Apis} = require('bitsharesjs-ws'); +const {PrivateKey,TransactionBuilder} = require('bitsharesjs'); +const moment = require('moment'); + +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._last_publication_times = null; + this._last_feed_check = null; + this._node_retries = 0; + } + + + format_recap() { + const stats = [ + `Checking interval: \`${this._config.checking_interval} sec\``, + `Node failed connection attempt notification threshold: \`${this._config.retries_threshold}\``, + `Missed block threshold: \`${this._config.missed_block_threshold}\``, + `Missed block reset time window: \`${this._config.reset_period} sec\``, + `API node: \`${this._config.api_node}\``, + `Backup signing key: \`${this._config.backup_key}\``, + `Recap time period: \`${this._config.recap_time} min\``, + `Total missed blocks: \`${this._total_missed}\``, + `Missed blocks in current time window: \`${this._total_missed - this._start_missed}\``, + `Feeds to check: \`${this._config.feeds_to_check}\``, + `Last publication times: \`${this._last_publication_times}\`` + ] + return stats.join('\n'); + } + + 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(); + } + }); + + } + + update_signing_key() { + const private_key = PrivateKey.fromWif(this._config.private_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: '', + new_signing_key: this._config.backup_key + }); + + return tr.set_required_fees().then(() => { + tr.add_signer(private_key, private_key.toPublicKey().toPublicKeyString()); + return tr.broadcast(); + }) + .then(() => { + this.reset_missed_block_window(); + this.notify('Signing key updated'); + }).catch(() => { + this.notify('Could not broadcast update_witness tx.'); + }); + + } + notify(msg) { + this._logger.log(msg); + this.emit('notify', msg) + } + + + find_last_publication_time(dynamic_assets_data, witness_account) { + return dynamic_assets_data.map(dynamic_assets_data => { + for (const feed of dynamic_assets_data['feeds']) { + if (feed[0] == witness_account) { + return feed[1][0]; + } + } + return null; + }); + } + + 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._last_publication_times = this.find_last_publication_time(dynamic_assets_data, this._witness_account); + const formatted_result = this._config.feeds_to_check.map((x, i) => `${x} (${this._last_publication_times[i]})`) + this._logger.log(`Publication times: ${formatted_result.join(', ')}`); + for (let i = 0; i < this._config.feeds_to_check.length; ++i) { + if (this._last_publication_times[i] == null) { + this.notify(`No publication found for ${this._config.feeds_to_check[i]}.`); + } else { + const minutes_since_last_publication = moment.utc().diff(moment.utc(this._last_publication_times[i]), 'minutes') + if (minutes_since_last_publication > this._config.feed_publication_threshold) { + const price_feed_alert = [ + `More than ${this._config.feed_publication_threshold} minutes elapsed since last publication of ${this._config.feeds_to_check[i]}.`, + `Last publication happened at ${moment.utc(this._last_publication_times[i]).local().format()}, ${minutes_since_last_publication} minutes ago.` + ]; + this.notify(price_feed_alert.join('\n')); + } + } + } + }); + } + + + 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(); + } + + start_monitoring() { + this._logger.log('Starting witness health monitor'); + 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._total_missed = witness[0].total_missed; + + 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_missed_blocks(), this.check_publication_feeds()]); + }); + + }).catch((error) => { + this._node_retries++; + this._logger.log('API node unavailable.'); + 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 From 0b1889f86015e82c1bee08fe9890688fa4151788 Mon Sep 17 00:00:00 2001 From: Zapata Date: Mon, 9 Jul 2018 16:40:50 +0200 Subject: [PATCH 05/15] Review information displayed. - Add more informations on feed publication (spread, elapsed time); - Do not send configuration information at each recap; - send configuration information at startup and on request (command /settings). - all formatting is done on the bot side and not in WitnessMonitor. --- index.js | 52 +++++++++++++++++++++++++++---- lib/FeedStat.js | 28 +++++++++++++++++ lib/WitnessMonitor.js | 71 ++++++++++++++++++++++--------------------- 3 files changed, 111 insertions(+), 40 deletions(-) create mode 100644 lib/FeedStat.js diff --git a/index.js b/index.js index 0a65b3f..7158444 100644 --- a/index.js +++ b/index.js @@ -19,8 +19,34 @@ function isAuthenticated(chatId) { return true; } -function send_recap(recipient_id) { - bot.sendMessage(recipient_id, witness_monitor.format_recap(), { parse_mode: 'Markdown' }); +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.current_missed}\``, + `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\``, + `Backup signing key: \`${config.backup_key}\``, + `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.setMessage(chatId, settings.join('\n'), { parse_mode: 'Markdown' }) } bot.on('polling_error', (error) => { @@ -183,10 +209,20 @@ bot.onText(/\/interval (.+)/, (msg, match) => { }); bot.onText(/\/stats/, (msg, match) => { + + const chatId = msg.chat.id; + + if (isAuthenticated(chatId)) { + send_stats(chatId); + } +}); + +bot.onText(/\/settings/, (msg, match) => { + const chatId = msg.chat.id; if (isAuthenticated(chatId)) { - send_recap(chatId); + send_settings(chatId); } }); @@ -230,8 +266,6 @@ bot.onText(/\/feeds (.+)/, (msg, match) => { }); - - bot.onText(/\/pause/, (msg, match) => { const chatId = msg.chat.id; @@ -266,6 +300,12 @@ bot.onText(/\/resume/, (msg, match) => { const witness_monitor = new WitnessMonitor(config, logger); +witness_monitor.on('started', () => { + if (admin_id != null) { + bot.sendMessage(admin_id, 'Bot (re)started.'); + send_settings(admin_id); + } +}); witness_monitor.on('notify', (msg) => { if (admin_id != null) { bot.sendMessage(admin_id, msg); @@ -275,7 +315,7 @@ witness_monitor.on('checked', () => { if ((admin_id != null) && (config.recap_time > 0)) { if (moment().diff(last_recap_send, 'minutes') >= config.recap_time) { last_recap_send = moment(); - send_recap(admin_id); + send_stats(admin_id); } } }); diff --git a/lib/FeedStat.js b/lib/FeedStat.js new file mode 100644 index 0000000..ba8be49 --- /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) { + 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/WitnessMonitor.js b/lib/WitnessMonitor.js index aaa4073..2c1a5c6 100644 --- a/lib/WitnessMonitor.js +++ b/lib/WitnessMonitor.js @@ -2,6 +2,7 @@ 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 { @@ -17,27 +18,18 @@ class WitnessMonitor extends EventEmitter { this._window_start = null; this._checking = false; this._witness_account = null; - this._last_publication_times = null; + this._feed_stats = null; this._last_feed_check = null; this._node_retries = 0; } - format_recap() { - const stats = [ - `Checking interval: \`${this._config.checking_interval} sec\``, - `Node failed connection attempt notification threshold: \`${this._config.retries_threshold}\``, - `Missed block threshold: \`${this._config.missed_block_threshold}\``, - `Missed block reset time window: \`${this._config.reset_period} sec\``, - `API node: \`${this._config.api_node}\``, - `Backup signing key: \`${this._config.backup_key}\``, - `Recap time period: \`${this._config.recap_time} min\``, - `Total missed blocks: \`${this._total_missed}\``, - `Missed blocks in current time window: \`${this._total_missed - this._start_missed}\``, - `Feeds to check: \`${this._config.feeds_to_check}\``, - `Last publication times: \`${this._last_publication_times}\`` - ] - return stats.join('\n'); + current_statistics() { + return { + total_missed : this._total_missed, + current_missed : this._total_missed - this._start_missed, + feed_publications : this._feed_stats + } } reset_missed_block_window() { @@ -95,15 +87,23 @@ class WitnessMonitor extends EventEmitter { } - find_last_publication_time(dynamic_assets_data, witness_account) { - return dynamic_assets_data.map(dynamic_assets_data => { - for (const feed of dynamic_assets_data['feeds']) { + 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) { - return feed[1][0]; + my_publication_time = feed[1][0]; + my_price = feed[1][1]['settlement_price']['base']['amount'] / feed[1][1]['settlement_price']['quote']['amount'] } } - return null; + + feeds_stats.set(symbol, new FeedStat(symbol,my_publication_time, my_price, average_price)); }); + return feeds_stats; } check_publication_feeds() { @@ -120,23 +120,25 @@ class WitnessMonitor extends EventEmitter { }) .then(dynamic_assets_data => { this._last_feed_check = moment(); - this._last_publication_times = this.find_last_publication_time(dynamic_assets_data, this._witness_account); - const formatted_result = this._config.feeds_to_check.map((x, i) => `${x} (${this._last_publication_times[i]})`) - this._logger.log(`Publication times: ${formatted_result.join(', ')}`); - for (let i = 0; i < this._config.feeds_to_check.length; ++i) { - if (this._last_publication_times[i] == null) { - this.notify(`No publication found for ${this._config.feeds_to_check[i]}.`); + 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 { - const minutes_since_last_publication = moment.utc().diff(moment.utc(this._last_publication_times[i]), 'minutes') - if (minutes_since_last_publication > this._config.feed_publication_threshold) { + 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 ${this._config.feeds_to_check[i]}.`, - `Last publication happened at ${moment.utc(this._last_publication_times[i]).local().format()}, ${minutes_since_last_publication} minutes ago.` + `More than ${this._config.feed_publication_threshold} minutes elapsed since last publication of ${feed_stat.name}.`, + `Last publication happened at ${moment.utc(feed_stat.publication_time).local().format()}, ${feed_stat.since().humanize()} ago.` ]; this.notify(price_feed_alert.join('\n')); } } - } + }); + }) + .catch(error => { + this._logger.log(`Unable to retrieve feed stats: ${error}`); + throw error; }); } @@ -160,6 +162,7 @@ class WitnessMonitor extends EventEmitter { start_monitoring() { this._logger.log('Starting witness health monitor'); + this.emit('started'); this.run_monitoring(); } @@ -182,9 +185,9 @@ class WitnessMonitor extends EventEmitter { return Promise.all([this.check_missed_blocks(), this.check_publication_feeds()]); }); - }).catch((error) => { + }).catch((error) => { this._node_retries++; - this._logger.log('API node unavailable.'); + this._logger.log(`API node unavailable: ${error}`); if (this._node_retries > this._config.retries_threshold) { this.notify('Unable to connect to API node for ' + this._node_retries + ' times.'); } From 8ffcbe3017cd76a161769450505e1239197c3292 Mon Sep 17 00:00:00 2001 From: Zapata Date: Tue, 10 Jul 2018 00:45:28 +0200 Subject: [PATCH 06/15] User authorized user list instead of password. - Avoid that someone found the password; - Handle multiple admin users; - Avoid stop of notifications after a restart of the monitoring script; - Avoid the need to reauthenticate after each restart. - Easier for development. --- README.md | 19 ++++---- config-sample.json | 7 ++- index.js | 115 +++++++++++++++++---------------------------- 3 files changed, 57 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 87b6184..5b7c97f 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,16 @@ 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, + "reset_period": 300, "backup_key": "BTSXXXXXXXXXXXXXXXXXX", "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 } ``` @@ -38,14 +41,14 @@ and then save as config.json | Key | Description | | --- | --- | | `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)) | +| `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. | | `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. Not currently used. | -| `telegram_token` | The telegram access token for your notifications bot. You can create one with [BotFather](https://telegram.me/BotFather) | -| `telegram_password` | Your chosen access password through telegram. | +| `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. | @@ -95,8 +98,6 @@ Open a chat to your bot and use the following: - `/start`: Introduction message. - `/help`: Get the list of available commands. -- `/pass ` : Required to authenticate, otherwise no command will work. -- `/changepass `: Update your telegram access password and requires you to authenticate again using `/pass` - `/stats`: Return the current configuration and statistics of the monitoring session. - `/switch`: IMMEDIATELY update your signing key to the currently configured backup key. - `/new_key `: Set a new backup key in place of the configured one. @@ -119,8 +120,6 @@ Send this to @BotFather `/setcommands` to get completion on commands: ``` start - Introduction help - List all commands -pass - Authenticate -changepass - Update authentication password stats - Gather statistics switch - Update signing key to backup new_key - Set a new backup key diff --git a/config-sample.json b/config-sample.json index 8426dca..16af49d 100644 --- a/config-sample.json +++ b/config-sample.json @@ -9,6 +9,9 @@ "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 7158444..047f3af 100644 --- a/index.js +++ b/index.js @@ -8,12 +8,9 @@ const WitnessMonitor = require('./lib/WitnessMonitor.js') const logger = new Logger(config.debug_level); const bot = new TelegramBot(config.telegram_token, {polling: true}); -var admin_id = null; -var last_recap_send = null; - -function isAuthenticated(chatId) { - if (admin_id != chatId) { - bot.sendMessage(chatId, "You need to authenticate first."); +function checkAuthorization(chatId) { + if (config.telegram_authorized_users.includes(chatId)) { + bot.sendMessage(chatId, `You (${chatId}) are not authorized.`); return false; } return true; @@ -46,7 +43,7 @@ function send_settings(recipient_id) { `Feed publication treshold: \`${config.feed_publication_threshold} min\``, `Feed check interval: \`${config.feed_checking_interval} min\``, ]; - bot.setMessage(chatId, settings.join('\n'), { parse_mode: 'Markdown' }) + bot.sendMessage(recipient_id, settings.join('\n'), { parse_mode: 'Markdown' }) } bot.on('polling_error', (error) => { @@ -55,14 +52,18 @@ bot.on('polling_error', (error) => { bot.onText(/\/start/, (msg) => { - bot.sendMessage(msg.from.id, 'Hello, please authentificate first with `/pass`.', - { parse_mode: "Markdown" }); + + const chatId = msg.from.id; + + 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, `Hello ${msg.from.first_name}, sorry but there is nothing for you here.`); + } }); bot.onText(/\/help/, (msg) => { const help = [ - `\`/pass \` : Required to authenticate, otherwise no command will work.`, - `\`/changepass \`: Update your telegram access password and requires you to authenticate again using \`/pass\``, `\`/stats\`: Return the current configuration and statistics of the monitoring session.`, `\`/switch\`: IMMEDIATELY update your signing key to the currently configured backup key.`, `\`/new_key \`: Set a new backup key in place of the configured one.`, @@ -83,38 +84,11 @@ bot.onText(/\/help/, (msg) => { bot.sendMessage(msg.from.id, help.join('\n'), { parse_mode: 'Markdown' }); }); -bot.onText(/\/pass (.+)/, (msg, match) => { - - const chatId = msg.from.id; - const pass = match[1]; - - if (pass == config.telegram_password) { - admin_id = chatId; - bot.sendMessage(chatId, `Password accepted. New admin is ${admin_id}`); - } else { - bot.sendMessage(chatId, 'Password incorrect.'); - } - -}); - -bot.onText(/\/changepass (.+)/, (msg, match) => { - - const chatId = msg.from.id; - const pass = match[1]; - - if (isAuthenticated(chatId)) { - config.telegram_password = pass; - bot.sendMessage(chatId, 'Password changed. Please authenticate again with /pass .'); - admin_id = null; - } - -}); - bot.onText(/\/reset/, (msg, match) => { const chatId = msg.chat.id; - if (isAuthenticated(chatId)) { + if (checkAuthorization(chatId)) { witness_monitor.reset_missed_block_window(); bot.sendMessage(chatId, 'Session missed block counter set to 0.'); } @@ -126,7 +100,7 @@ bot.onText(/\/new_key (.+)/, (msg, match) => { const chatId = msg.chat.id; const key = match[1]; - if (isAuthenticated(chatId)) { + if (checkAuthorization(chatId)) { config.backup_key = key; bot.sendMessage(chatId, `Backup signing key set to: ${config.backup_key}`); } @@ -138,7 +112,7 @@ bot.onText(/\/new_node (.+)/, (msg, match) => { const chatId = msg.chat.id; const node = match[1]; - if (isAuthenticated(chatId)) { + if (checkAuthorization(chatId)) { config.api_node = node; bot.sendMessage(chatId, `API node set to: ${config.api_node}`); } @@ -150,7 +124,7 @@ bot.onText(/\/threshold (.+)/, (msg, match) => { const chatId = msg.chat.id; const thresh = match[1]; - if (isAuthenticated(chatId)) { + if (checkAuthorization(chatId)) { config.missed_block_threshold = thresh; bot.sendMessage(chatId, `Missed block threshold set to: ${config.missed_block_threshold}`); } @@ -162,7 +136,7 @@ bot.onText(/\/recap (.+)/, (msg, match) => { const chatId = msg.chat.id; const recap = match[1]; - if (isAuthenticated(chatId)) { + if (checkAuthorization(chatId)) { config.recap_time = recap; if (config.recap_time > 0) { bot.sendMessage(chatId, `Recap time period set to: ${config.recap_time} minutes.`); @@ -177,7 +151,7 @@ bot.onText(/\/window (.+)/, (msg, match) => { const chatId = msg.chat.id; const wind = match[1]; - if (isAuthenticated(chatId)) { + if (checkAuthorization(chatId)) { config.reset_period = wind; bot.sendMessage(chatId, `Missed block reset time window set to: ${config.reset_period}s`); } @@ -189,7 +163,7 @@ bot.onText(/\/retries (.+)/, (msg, match) => { const chatId = msg.chat.id; const ret = match[1]; - if (isAuthenticated(chatId)) { + if (checkAuthorization(chatId)) { config.retries_threshold = ret; bot.sendMessage(chatId, `Failed node connection attempt notification threshold set to: ${config.retries_threshold}`); } @@ -201,7 +175,7 @@ bot.onText(/\/interval (.+)/, (msg, match) => { const chatId = msg.chat.id; const new_int = match[1]; - if (isAuthenticated(chatId)) { + if (checkAuthorization(chatId)) { config.checking_interval = new_int; bot.sendMessage(chatId, `Checking interval set to: ${config.checking_interval}s.`); } @@ -212,7 +186,7 @@ bot.onText(/\/stats/, (msg, match) => { const chatId = msg.chat.id; - if (isAuthenticated(chatId)) { + if (checkAuthorization(chatId)) { send_stats(chatId); } }); @@ -221,7 +195,7 @@ bot.onText(/\/settings/, (msg, match) => { const chatId = msg.chat.id; - if (isAuthenticated(chatId)) { + if (checkAuthorization(chatId)) { send_settings(chatId); } @@ -232,7 +206,7 @@ bot.onText(/\/feed_checking_interval (.+)/, (msg, match) => { const chatId = msg.chat.id; const new_int = match[1]; - if (isAuthenticated(chatId)) { + if (checkAuthorization(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.`); @@ -245,7 +219,7 @@ bot.onText(/\/feed_publication_threshold (.+)/, (msg, match) => { const chatId = msg.chat.id; const new_threshold = match[1]; - if (isAuthenticated(chatId)) { + if (checkAuthorization(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.`); @@ -258,7 +232,7 @@ bot.onText(/\/feeds (.+)/, (msg, match) => { const chatId = msg.chat.id; const new_feeds = match[1].split(' '); - if (isAuthenticated(chatId)) { + if (checkAuthorization(chatId)) { config.feeds_to_check = new_feeds; witness_monitor.reset_feed_check(); bot.sendMessage(chatId, `Feeds to check set to: ${config.feeds_to_check}.`); @@ -270,7 +244,7 @@ bot.onText(/\/pause/, (msg, match) => { const chatId = msg.chat.id; - if (isAuthenticated(chatId)) { + if (checkAuthorization(chatId)) { witness_monitor.pause(); bot.sendMessage(chatId, 'Witness monitoring paused. Use /resume to resume monitoring.'); } @@ -281,42 +255,39 @@ bot.onText(/\/switch/, (msg, match) => { const chatId = msg.chat.id; - if (isAuthenticated(chatId)) { + if (checkAuthorization(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 (isAuthenticated(chatId)) { + if (checkAuthorization(chatId)) { witness_monitor.resume(); bot.sendMessage(chatId, 'Witness monitoring resumed.'); } }); - const witness_monitor = new WitnessMonitor(config, logger); -witness_monitor.on('started', () => { - if (admin_id != null) { - bot.sendMessage(admin_id, 'Bot (re)started.'); - send_settings(admin_id); - } -}); -witness_monitor.on('notify', (msg) => { - if (admin_id != null) { - bot.sendMessage(admin_id, msg); - } -}); -witness_monitor.on('checked', () => { - if ((admin_id != null) && (config.recap_time > 0)) { - if (moment().diff(last_recap_send, 'minutes') >= config.recap_time) { +var last_recap_send = null; +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(admin_id); + send_stats(user_id); } - } -}); + }); +} witness_monitor.start_monitoring(); \ No newline at end of file From beca17c251440debddd8020b8c48d59e0f91131c Mon Sep 17 00:00:00 2001 From: Zapata Date: Tue, 10 Jul 2018 00:59:30 +0200 Subject: [PATCH 07/15] Do not reset witness url when updates signing keys. --- lib/WitnessMonitor.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/WitnessMonitor.js b/lib/WitnessMonitor.js index 2c1a5c6..216bbf8 100644 --- a/lib/WitnessMonitor.js +++ b/lib/WitnessMonitor.js @@ -18,6 +18,7 @@ class WitnessMonitor extends EventEmitter { this._window_start = null; this._checking = false; this._witness_account = null; + this._witness_url = null; this._feed_stats = null; this._last_feed_check = null; this._node_retries = 0; @@ -65,7 +66,7 @@ class WitnessMonitor extends EventEmitter { }, witness: this._config.witness_id, witness_account: this._witness_account, - new_url: '', + new_url: this._witness_url, new_signing_key: this._config.backup_key }); @@ -175,6 +176,7 @@ class WitnessMonitor extends EventEmitter { 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._total_missed = witness[0].total_missed; const should_reset_window = moment().diff(this._window_start, 'seconds') >= this._config.reset_period From 08eb281dd1b887daf4b6e585dfbf250047a93e6d Mon Sep 17 00:00:00 2001 From: Zapata Date: Tue, 10 Jul 2018 01:54:27 +0200 Subject: [PATCH 08/15] Automaticcaly roll between multiple signing keys. - Handle a list of signing keys. - Detect current key used, and switch to the next one if needed/requested. - Avoid to input a new key after a key switch; - Update documentation and commands accordingly. --- README.md | 12 +++++++----- config-sample.json | 2 +- index.js | 18 ++++++++++-------- lib/WitnessMonitor.js | 24 +++++++++++++++++------- 4 files changed, 35 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 5b7c97f..7b78a05 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Open config-sample.json in your favourite text editor and edit with your own set "missed_block_threshold": 3, "checking_interval": 10, "reset_period": 300, - "backup_key": "BTSXXXXXXXXXXXXXXXXXX", + "witness_signing_keys": [ "BTSXXXXXXXXXXXXXXXXXX", "BTSYYYYYYYYYYYYYYY"], "recap_time": 60, "debug_level": 3, "telegram_token": "", @@ -40,10 +40,12 @@ and then save as config.json | 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. | -| `backup_key` | The public signing key of your backup witness to be used when switching. | +| `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. | @@ -99,8 +101,8 @@ Open a chat to your bot and use the following: - `/start`: Introduction message. - `/help`: Get the list of available commands. - `/stats`: Return the current configuration and statistics of the monitoring session. -- `/switch`: IMMEDIATELY update your signing key to the currently configured backup key. -- `/new_key `: Set a new backup key in place of the configured one. +- `/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. @@ -122,7 +124,7 @@ start - Introduction help - List all commands stats - Gather statistics switch - Update signing key to backup -new_key - Set a new backup key +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 diff --git a/config-sample.json b/config-sample.json index 16af49d..2e70255 100644 --- a/config-sample.json +++ b/config-sample.json @@ -5,7 +5,7 @@ "missed_block_threshold": 3, "checking_interval": 10, "reset_period": 300, - "backup_key": "", + "witness_signing_keys": [], "recap_time": 60, "debug_level": 3, "telegram_token": "", diff --git a/index.js b/index.js index 047f3af..4020674 100644 --- a/index.js +++ b/index.js @@ -20,7 +20,8 @@ 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.current_missed}\``, + `Missed blocks in current time window: \`${current_stats.window_missed}\``, + `Current signing key: \`${current_stats.signing_key}\``, `Feed publications: ` ] current_stats.feed_publications.forEach(feed_stat => { @@ -37,7 +38,7 @@ function send_settings(recipient_id) { `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\``, - `Backup signing key: \`${config.backup_key}\``, + `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\``, @@ -65,8 +66,8 @@ bot.onText(/\/start/, (msg) => { 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 currently configured backup key.`, - `\`/new_key \`: Set a new backup key in place of the configured one.`, + `\`/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.`, @@ -95,14 +96,15 @@ bot.onText(/\/reset/, (msg, match) => { }); -bot.onText(/\/new_key (.+)/, (msg, match) => { +bot.onText(/\/signing_keys (.+)/, (msg, match) => { const chatId = msg.chat.id; - const key = match[1]; + const keys = match[1].split(' '); if (checkAuthorization(chatId)) { - config.backup_key = key; - bot.sendMessage(chatId, `Backup signing key set to: ${config.backup_key}`); + config.witness_signing_keys = keys; + bot.sendMessage(chatId, `Signing keys set to: ${config.witness_signing_keys.map(k => '`' + k + '`').join(', ')}`, + { parse_mode: 'Markdown' }); } }); diff --git a/lib/WitnessMonitor.js b/lib/WitnessMonitor.js index 216bbf8..b0a65b1 100644 --- a/lib/WitnessMonitor.js +++ b/lib/WitnessMonitor.js @@ -5,7 +5,6 @@ const moment = require('moment'); const FeedStat = require('./FeedStat.js'); class WitnessMonitor extends EventEmitter { - constructor(config, logger) { super(); @@ -19,16 +18,17 @@ class WitnessMonitor extends EventEmitter { this._checking = false; this._witness_account = null; this._witness_url = null; + this._witness_current_signing_key = null; this._feed_stats = null; this._last_feed_check = null; this._node_retries = 0; } - current_statistics() { return { total_missed : this._total_missed, - current_missed : this._total_missed - this._start_missed, + window_missed : this._total_missed - this._start_missed, + signing_key : this._witness_current_signing_key, feed_publications : this._feed_stats } } @@ -53,11 +53,18 @@ class WitnessMonitor extends EventEmitter { 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 private_key = PrivateKey.fromWif(this._config.private_key); + const backup_signing_key = this.find_next_signing_key(); const tr = new TransactionBuilder(); tr.add_type_operation('witness_update', { fee: { @@ -67,21 +74,23 @@ class WitnessMonitor extends EventEmitter { witness: this._config.witness_id, witness_account: this._witness_account, new_url: this._witness_url, - new_signing_key: this._config.backup_key + 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'); + 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) @@ -176,7 +185,8 @@ class WitnessMonitor extends EventEmitter { 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_url = witness[0].url; + this._witness_current_signing_key = witness[0].signing_key; this._total_missed = witness[0].total_missed; const should_reset_window = moment().diff(this._window_start, 'seconds') >= this._config.reset_period From 3d3c2ee47bcad6861bd5f0e9f6121b08c0199668 Mon Sep 17 00:00:00 2001 From: Zapata Date: Tue, 10 Jul 2018 12:54:46 +0200 Subject: [PATCH 09/15] Rename for consistency. --- index.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/index.js b/index.js index 4020674..35fb484 100644 --- a/index.js +++ b/index.js @@ -8,7 +8,7 @@ const WitnessMonitor = require('./lib/WitnessMonitor.js') const logger = new Logger(config.debug_level); const bot = new TelegramBot(config.telegram_token, {polling: true}); -function checkAuthorization(chatId) { +function check_authorization(chatId) { if (config.telegram_authorized_users.includes(chatId)) { bot.sendMessage(chatId, `You (${chatId}) are not authorized.`); return false; @@ -89,7 +89,7 @@ bot.onText(/\/reset/, (msg, match) => { const chatId = msg.chat.id; - if (checkAuthorization(chatId)) { + if (check_authorization(chatId)) { witness_monitor.reset_missed_block_window(); bot.sendMessage(chatId, 'Session missed block counter set to 0.'); } @@ -101,7 +101,7 @@ bot.onText(/\/signing_keys (.+)/, (msg, match) => { const chatId = msg.chat.id; const keys = match[1].split(' '); - if (checkAuthorization(chatId)) { + 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' }); @@ -114,7 +114,7 @@ bot.onText(/\/new_node (.+)/, (msg, match) => { const chatId = msg.chat.id; const node = match[1]; - if (checkAuthorization(chatId)) { + if (check_authorization(chatId)) { config.api_node = node; bot.sendMessage(chatId, `API node set to: ${config.api_node}`); } @@ -126,7 +126,7 @@ bot.onText(/\/threshold (.+)/, (msg, match) => { const chatId = msg.chat.id; const thresh = match[1]; - if (checkAuthorization(chatId)) { + if (check_authorization(chatId)) { config.missed_block_threshold = thresh; bot.sendMessage(chatId, `Missed block threshold set to: ${config.missed_block_threshold}`); } @@ -138,7 +138,7 @@ bot.onText(/\/recap (.+)/, (msg, match) => { const chatId = msg.chat.id; const recap = match[1]; - if (checkAuthorization(chatId)) { + 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.`); @@ -153,7 +153,7 @@ bot.onText(/\/window (.+)/, (msg, match) => { const chatId = msg.chat.id; const wind = match[1]; - if (checkAuthorization(chatId)) { + if (check_authorization(chatId)) { config.reset_period = wind; bot.sendMessage(chatId, `Missed block reset time window set to: ${config.reset_period}s`); } @@ -165,7 +165,7 @@ bot.onText(/\/retries (.+)/, (msg, match) => { const chatId = msg.chat.id; const ret = match[1]; - if (checkAuthorization(chatId)) { + if (check_authorization(chatId)) { config.retries_threshold = ret; bot.sendMessage(chatId, `Failed node connection attempt notification threshold set to: ${config.retries_threshold}`); } @@ -177,7 +177,7 @@ bot.onText(/\/interval (.+)/, (msg, match) => { const chatId = msg.chat.id; const new_int = match[1]; - if (checkAuthorization(chatId)) { + if (check_authorization(chatId)) { config.checking_interval = new_int; bot.sendMessage(chatId, `Checking interval set to: ${config.checking_interval}s.`); } @@ -188,7 +188,7 @@ bot.onText(/\/stats/, (msg, match) => { const chatId = msg.chat.id; - if (checkAuthorization(chatId)) { + if (check_authorization(chatId)) { send_stats(chatId); } }); @@ -197,7 +197,7 @@ bot.onText(/\/settings/, (msg, match) => { const chatId = msg.chat.id; - if (checkAuthorization(chatId)) { + if (check_authorization(chatId)) { send_settings(chatId); } @@ -208,7 +208,7 @@ bot.onText(/\/feed_checking_interval (.+)/, (msg, match) => { const chatId = msg.chat.id; const new_int = match[1]; - if (checkAuthorization(chatId)) { + 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.`); @@ -221,7 +221,7 @@ bot.onText(/\/feed_publication_threshold (.+)/, (msg, match) => { const chatId = msg.chat.id; const new_threshold = match[1]; - if (checkAuthorization(chatId)) { + 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.`); @@ -234,7 +234,7 @@ bot.onText(/\/feeds (.+)/, (msg, match) => { const chatId = msg.chat.id; const new_feeds = match[1].split(' '); - if (checkAuthorization(chatId)) { + 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}.`); @@ -246,7 +246,7 @@ bot.onText(/\/pause/, (msg, match) => { const chatId = msg.chat.id; - if (checkAuthorization(chatId)) { + if (check_authorization(chatId)) { witness_monitor.pause(); bot.sendMessage(chatId, 'Witness monitoring paused. Use /resume to resume monitoring.'); } @@ -257,7 +257,7 @@ bot.onText(/\/switch/, (msg, match) => { const chatId = msg.chat.id; - if (checkAuthorization(chatId)) { + if (check_authorization(chatId)) { bot.sendMessage(chatId, 'Attempting to update signing key...'); witness_monitor.force_update_signing_key(); } @@ -268,7 +268,7 @@ bot.onText(/\/resume/, (msg, match) => { const chatId = msg.chat.id; - if (checkAuthorization(chatId)) { + if (check_authorization(chatId)) { witness_monitor.resume(); bot.sendMessage(chatId, 'Witness monitoring resumed.'); } From 7f5af8c84677a95f89ef144b7d18d1f5a09db038 Mon Sep 17 00:00:00 2001 From: Zapata Date: Wed, 11 Jul 2018 01:22:24 +0200 Subject: [PATCH 10/15] Add configuration validation. --- index.js | 17 ++++ lib/ValidateConfig.js | 111 +++++++++++++++++++++++++ lib/WitnessMonitor.js | 2 +- package-lock.json | 146 +++++++++++++++++++++++++++++++-- package.json | 8 +- test/test_config_validation.js | 84 +++++++++++++++++++ 6 files changed, 357 insertions(+), 11 deletions(-) create mode 100644 lib/ValidateConfig.js create mode 100644 test/test_config_validation.js diff --git a/index.js b/index.js index 35fb484..ac16ce6 100644 --- a/index.js +++ b/index.js @@ -2,12 +2,27 @@ process.env["NTBA_FIX_319"] = 1; const TelegramBot = require('node-telegram-bot-api'); 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') const logger = new Logger(config.debug_level); const bot = new TelegramBot(config.telegram_token, {polling: true}); + +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.`); @@ -275,6 +290,8 @@ bot.onText(/\/resume/, (msg, match) => { }); +check_config(config); + const witness_monitor = new WitnessMonitor(config, logger); var last_recap_send = null; for (let user_id of config.telegram_authorized_users) { 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 index b0a65b1..4ec9fb4 100644 --- a/lib/WitnessMonitor.js +++ b/lib/WitnessMonitor.js @@ -199,7 +199,7 @@ class WitnessMonitor extends EventEmitter { }).catch((error) => { this._node_retries++; - this._logger.log(`API node unavailable: ${error}`); + 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.'); } diff --git a/package-lock.json b/package-lock.json index d55ea8b..6e2f6ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,6 +67,12 @@ "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", @@ -132,6 +138,22 @@ "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", @@ -198,6 +220,18 @@ "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", @@ -281,6 +315,12 @@ "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", @@ -422,6 +462,12 @@ "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", @@ -435,6 +481,26 @@ "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", @@ -471,6 +537,12 @@ "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", @@ -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", @@ -598,24 +680,61 @@ "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": { "version": "1.2.0", "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=" }, - "moment-timezone": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.21.tgz", - "integrity": "sha512-j96bAh4otsgj3lKydm3K7kdtA3iKf2m6MY2iSYCzCm5a1zmHo1g+aK3068dDEeocLZQIS9kU8bsdQHLqEvgW0A==", - "requires": { - "moment": ">= 2.9.0" - } - }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -662,6 +781,12 @@ "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", @@ -852,6 +977,11 @@ "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", diff --git a/package.json b/package.json index 49bf61a..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", @@ -14,6 +14,10 @@ "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 From ebddb4af67a87a24064a5f14f5a7ea8c508f2751 Mon Sep 17 00:00:00 2001 From: Zapata Date: Sat, 14 Jul 2018 09:58:56 +0200 Subject: [PATCH 11/15] Fix stats when there is no feed to monitor. --- lib/WitnessMonitor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/WitnessMonitor.js b/lib/WitnessMonitor.js index 4ec9fb4..aceac50 100644 --- a/lib/WitnessMonitor.js +++ b/lib/WitnessMonitor.js @@ -19,7 +19,7 @@ class WitnessMonitor extends EventEmitter { this._witness_account = null; this._witness_url = null; this._witness_current_signing_key = null; - this._feed_stats = null; + this._feed_stats = new Map(); this._last_feed_check = null; this._node_retries = 0; } From 7b330060ebba9cc787696b8f31eae64e91426bb6 Mon Sep 17 00:00:00 2001 From: Zapata Date: Sat, 14 Jul 2018 18:40:47 +0200 Subject: [PATCH 12/15] Fix regular recap notification. --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index ac16ce6..b6ca57f 100644 --- a/index.js +++ b/index.js @@ -293,7 +293,7 @@ bot.onText(/\/resume/, (msg, match) => { check_config(config); const witness_monitor = new WitnessMonitor(config, logger); -var last_recap_send = null; +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.'); From a159cfacf6d0b3ae3060986e76c13b5c3d06d495 Mon Sep 17 00:00:00 2001 From: Zapata Date: Tue, 24 Jul 2018 13:33:34 +0200 Subject: [PATCH 13/15] Improve feed stats messages. --- lib/FeedStat.js | 2 +- lib/WitnessMonitor.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/FeedStat.js b/lib/FeedStat.js index ba8be49..a5f4d6b 100644 --- a/lib/FeedStat.js +++ b/lib/FeedStat.js @@ -17,7 +17,7 @@ class FeedStat { } toString() { - if (this.publication_time == null) { + if (this.publication_time == null || moment.utc(this.publication_time).isBefore('2013-01-01')) { return `${this.name} not published yet.` } diff --git a/lib/WitnessMonitor.js b/lib/WitnessMonitor.js index aceac50..deb31f1 100644 --- a/lib/WitnessMonitor.js +++ b/lib/WitnessMonitor.js @@ -139,7 +139,7 @@ class WitnessMonitor extends EventEmitter { 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}.`, - `Last publication happened at ${moment.utc(feed_stat.publication_time).local().format()}, ${feed_stat.since().humanize()} ago.` + feed_stat.toString() ]; this.notify(price_feed_alert.join('\n')); } From 9a3227cfa03db2e6e567f697470bb8c5bf338378 Mon Sep 17 00:00:00 2001 From: Zapata Date: Tue, 24 Jul 2018 13:34:02 +0200 Subject: [PATCH 14/15] Add votes and (de)activation monitoring. --- index.js | 1 + lib/WitnessMonitor.js | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index b6ca57f..fe99a0d 100644 --- a/index.js +++ b/index.js @@ -36,6 +36,7 @@ function send_stats(recipient_id) { 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: ` ] diff --git a/lib/WitnessMonitor.js b/lib/WitnessMonitor.js index deb31f1..873d322 100644 --- a/lib/WitnessMonitor.js +++ b/lib/WitnessMonitor.js @@ -17,6 +17,8 @@ class WitnessMonitor extends EventEmitter { 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(); @@ -27,6 +29,8 @@ class WitnessMonitor extends EventEmitter { 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 @@ -170,6 +174,24 @@ class WitnessMonitor extends EventEmitter { 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'); @@ -188,13 +210,14 @@ class WitnessMonitor extends EventEmitter { 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_missed_blocks(), this.check_publication_feeds()]); + return Promise.all([this.check_activeness(), this.check_missed_blocks(), this.check_publication_feeds()]); }); }).catch((error) => { From 8a2c5ba7bd9216e3270693fecdf8f4c9883e5546 Mon Sep 17 00:00:00 2001 From: Zapata Date: Wed, 25 Jul 2018 00:15:00 +0200 Subject: [PATCH 15/15] Document /settings. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b78a05..caa4aa7 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,8 @@ Open a chat to your bot and use the following: - `/start`: Introduction message. - `/help`: Get the list of available commands. -- `/stats`: Return the current configuration and statistics of the monitoring session. +- `/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. @@ -123,6 +124,7 @@ Send this to @BotFather `/setcommands` to get completion on commands: 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