diff --git a/lib/notifier/codes.js b/lib/notifier/codes.js new file mode 100644 index 00000000..7211b802 --- /dev/null +++ b/lib/notifier/codes.js @@ -0,0 +1,34 @@ +const T = require('../time') + +const PING = 'PING' +const STALE = 'STALE' +const LOW_CRYPTO_BALANCE = 'LOW_CRYPTO_BALANCE' +const HIGH_CRYPTO_BALANCE = 'HIGH_CRYPTO_BALANCE' +const CASH_BOX_FULL = 'CASH_BOX_FULL' +const LOW_CASH_OUT = 'LOW_CASH_OUT' + +const CODES_DISPLAY = { + PING: 'Machine Down', + STALE: 'Machine Stuck', + LOW_CRYPTO_BALANCE: 'Low Crypto Balance', + HIGH_CRYPTO_BALANCE: 'High Crypto Balance', + CASH_BOX_FULL: 'Cash box full', + LOW_CASH_OUT: 'Low Cash-out' +} + +const NETWORK_DOWN_TIME = 1 * T.minute +const STALE_STATE = 7 * T.minute +const ALERT_SEND_INTERVAL = T.hour + +module.exports = { + PING, + STALE, + LOW_CRYPTO_BALANCE, + HIGH_CRYPTO_BALANCE, + CASH_BOX_FULL, + LOW_CASH_OUT, + CODES_DISPLAY, + NETWORK_DOWN_TIME, + STALE_STATE, + ALERT_SEND_INTERVAL +} diff --git a/lib/notifier/email.js b/lib/notifier/email.js new file mode 100644 index 00000000..01ad5504 --- /dev/null +++ b/lib/notifier/email.js @@ -0,0 +1,100 @@ +const _ = require('lodash/fp') +const prettyMs = require('pretty-ms') +const { + PING, + STALE, + LOW_CRYPTO_BALANCE, + HIGH_CRYPTO_BALANCE, + CASH_BOX_FULL, + LOW_CASH_OUT +} = require('./codes') + +function alertSubject(alertRec, config) { + let alerts = [] + + if (config.balance) { + alerts = _.concat(alerts, alertRec.general) + } + + _.keys(alertRec.devices).forEach(function (device) { + if (config.balance) { + alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts) + } + + if (config.errors) { + alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts) + } + }) + + if (alerts.length === 0) return null + + const alertTypes = _.map(codeDisplay, _.uniq(_.map('code', alerts))).sort() + return '[Lamassu] Errors reported: ' + alertTypes.join(', ') +} + +function printEmailAlerts(alertRec, config) { + let body = 'Errors were reported by your Lamassu Machines.\n' + + if (config.balance && alertRec.general.length !== 0) { + body = body + '\nGeneral errors:\n' + body = body + emailAlerts(alertRec.general) + '\n' + } + + _.keys(alertRec.devices).forEach(function (device) { + const deviceName = alertRec.deviceNames[device] + body = body + '\nErrors for ' + deviceName + ':\n' + + let alerts = [] + if (config.balance) { + alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts) + } + + if (config.errors) { + alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts) + } + + body = body + emailAlerts(alerts) + }) + + return body +} + +function emailAlerts(alerts) { + return alerts.map(emailAlert).join('\n') + '\n' +} + +function formatCurrency(num, code) { + return numeral(num).format('0,0.00') + ' ' + code +} + +function emailAlert(alert) { + switch (alert.code) { + case PING: + if (alert.age) { + const pingAge = prettyMs(alert.age, { compact: true, verbose: true }) + return `Machine down for ${pingAge}` + } + return 'Machine down for a while.' + case STALE: { + const stuckAge = prettyMs(alert.age, { compact: true, verbose: true }) + return `Machine is stuck on ${alert.state} screen for ${stuckAge}` + } + case LOW_CRYPTO_BALANCE: { + const balance = formatCurrency(alert.fiatBalance.balance, alert.fiatCode) + return `Low balance in ${alert.cryptoCode} [${balance}]` + } + case HIGH_CRYPTO_BALANCE: { + const highBalance = formatCurrency( + alert.fiatBalance.balance, + alert.fiatCode + ) + return `High balance in ${alert.cryptoCode} [${highBalance}]` + } + case CASH_BOX_FULL: + return `Cash box full on ${alert.machineName} [${alert.notes} banknotes]` + case LOW_CASH_OUT: + return `Cassette for ${alert.denomination} ${alert.fiatCode} low [${alert.notes} banknotes]` + } +} + +module.exports = { alertSubject, printEmailAlerts } diff --git a/lib/notifier/index.js b/lib/notifier/index.js new file mode 100644 index 00000000..1905a5d3 --- /dev/null +++ b/lib/notifier/index.js @@ -0,0 +1,139 @@ +const { STALE, STALE_STATE } = require('./codes') + +const _ = require('lodash/fp') +const queries = require('./queries') +const logger = require('../logger') + +const utils = require('./utils') +const emailFuncs = require('./email') +const smsFuncs = require('./sms') + +function buildMessage(alerts, notifications) { + const smsEnabled = utils.isActive(notifications.sms) + const emailEnabled = utils.isActive(notifications.email) + + let rec = {} + if (smsEnabled) { + rec = _.set(['sms', 'body'])( + smsFuncs.printSmsAlerts(alerts, notifications.sms) + )(rec) + } + if (emailEnabled) { + rec = _.set(['email', 'subject'])( + emailFuncs.alertSubject(alerts, notifications.email) + )(rec) + rec = _.set(['email', 'body'])( + emailFuncs.printEmailAlerts(alerts, notifications.email) + )(rec) + } + + return rec +} + +function checkNotification(plugins) { + const notifications = plugins.getNotificationConfig() + const smsEnabled = utils.isActive(notifications.sms) + const emailEnabled = utils.isActive(notifications.email) + + if (!smsEnabled && !emailEnabled) return Promise.resolve() + + return getAlerts(plugins) + .then(alerts => { + const currentAlertFingerprint = utils.buildAlertFingerprint( + alerts, + notifications + ) + if (!currentAlertFingerprint) { + const inAlert = !!utils.getAlertFingerprint() + // (fingerprint = null, lastAlertTime = null) + utils.setAlertFingerprint(null, null) + if (inAlert) { + return utils.sendNoAlerts(plugins, smsEnabled, emailEnabled) + } + } + if (utils.shouldNotAlert(currentAlertFingerprint)) { + return + } + + const message = buildMessage(alerts, notifications) + utils.setAlertFingerprint(currentAlertFingerprint, Date.now()) + return plugins.sendMessage(message) + }) + .then(results => { + if (results && results.length > 0) + logger.debug('Successfully sent alerts') + }) + .catch(logger.error) +} + +function getAlerts(plugins) { + return Promise.all([ + plugins.checkBalances(), + queries.machineEvents(), + plugins.getMachineNames() + ]).then(([balances, events, devices]) => + buildAlerts(checkPings(devices), balances, events, devices) + ) +} + +function buildAlerts(pings, balances, events, devices) { + const alerts = { devices: {}, deviceNames: {} } + alerts.general = _.filter(r => !r.deviceId, balances) + devices.forEach(function (device) { + const deviceId = device.deviceId + const deviceName = device.name + const deviceEvents = events.filter(function (eventRow) { + return eventRow.device_id === deviceId + }) + const ping = pings[deviceId] || [] + const stuckScreen = checkStuckScreen(deviceEvents, deviceName) + + if (!alerts.devices[deviceId]) alerts.devices[deviceId] = {} + alerts.devices[deviceId].balanceAlerts = _.filter( + ['deviceId', deviceId], + balances + ) + alerts.devices[deviceId].deviceAlerts = _.isEmpty(ping) ? stuckScreen : ping + + alerts.deviceNames[deviceId] = deviceName + }) + return alerts +} + +function checkPings(devices) { + const deviceIds = _.map('deviceId', devices) + const pings = _.map(utils.checkPing, devices) + return _.zipObject(deviceIds)(pings) +} + +function checkStuckScreen(deviceEvents, machineName) { + const sortedEvents = _.sortBy( + utils.getDeviceTime, + _.map(utils.parseEventNote, deviceEvents) + ) + const lastEvent = _.last(sortedEvents) + + if (!lastEvent) { + return [] + } + + const state = lastEvent.note.state + const isIdle = lastEvent.note.isIdle + + if (isIdle) { + return [] + } + + const age = Math.floor(lastEvent.age) + if (age > STALE_STATE) { + return [{ code: STALE, state, age, machineName }] + } + + return [] +} + +module.exports = { + checkNotification, + checkPings, + checkStuckScreen +} diff --git a/lib/notifier/queries.js b/lib/notifier/queries.js new file mode 100644 index 00000000..a41c3888 --- /dev/null +++ b/lib/notifier/queries.js @@ -0,0 +1,3 @@ +const dbm = require('../postgresql_interface') + +module.exports = { machineEvents: dbm.machineEvents } diff --git a/lib/notifier/sms.js b/lib/notifier/sms.js new file mode 100644 index 00000000..ea9a2c93 --- /dev/null +++ b/lib/notifier/sms.js @@ -0,0 +1,53 @@ +const _ = require('lodash/fp') +const utils = require('./utils') + +function printSmsAlerts(alertRec, config) { + let alerts = [] + + if (config.balance) { + alerts = _.concat(alerts, alertRec.general) + } + + _.keys(alertRec.devices).forEach(function (device) { + if (config.balance) { + alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts) + } + + if (config.errors) { + alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts) + } + }) + + if (alerts.length === 0) return null + + const alertsMap = _.groupBy('code', alerts) + + const alertTypes = _.map(entry => { + const code = entry[0] + const machineNames = _.filter( + _.negate(_.isEmpty), + _.map('machineName', entry[1]) + ) + + return { + codeDisplay: utils.codeDisplay(code), + machineNames + } + }, _.toPairs(alertsMap)) + + const mapByCodeDisplay = _.map(it => + _.isEmpty(it.machineNames) + ? it.codeDisplay + : `${it.codeDisplay} (${it.machineNames.join(', ')})` + ) + + const displayAlertTypes = _.compose( + _.uniq, + mapByCodeDisplay, + _.sortBy('codeDisplay') + )(alertTypes) + + return '[Lamassu] Errors reported: ' + displayAlertTypes.join(', ') +} + +module.exports = { printSmsAlerts } diff --git a/lib/notifier/test/email.test.js b/lib/notifier/test/email.test.js new file mode 100644 index 00000000..8d27b789 --- /dev/null +++ b/lib/notifier/test/email.test.js @@ -0,0 +1,28 @@ +const email = require('../email') + +const alertRec = { + devices: { + f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: { + balanceAlerts: [], + deviceAlerts: [ + { code: 'PING', age: 602784301.446, machineName: 'Abc123' } + ] + } + }, + deviceNames: { + f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: 'Abc123' + }, + general: [] +} + +const printEmailMsg = `Errors were reported by your Lamassu Machines. + +Errors for Abc123: +Machine down for ~6 days +` + +test('Print Email Alers', () => { + expect(email.printEmailAlerts(alertRec, { active: true, errors: true })).toBe( + printEmailMsg + ) +}) diff --git a/lib/notifier/test/notifier.test.js b/lib/notifier/test/notifier.test.js new file mode 100644 index 00000000..c7a6f4f8 --- /dev/null +++ b/lib/notifier/test/notifier.test.js @@ -0,0 +1,234 @@ +const notifier = require('..') + +// mock plugins object with mock data to test functions +const plugins = { + sendMessage: rec => { + return rec + }, + getNotificationConfig: () => ({ + email_active: false, + sms_active: true, + email_errors: false, + sms_errors: true, + sms: { active: true, errors: true }, + email: { active: false, errors: false } + }), + getMachineNames: () => [ + { + deviceId: + 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', + cashbox: 0, + cassette1: 444, + cassette2: 222, + version: '7.5.0-beta.0', + model: 'unknown', + pairedAt: '2020-11-13T16:20:31.624Z', + lastPing: '2020-11-16T13:11:03.169Z', + name: 'Abc123', + paired: true, + cashOut: true, + statuses: [{ label: 'Unresponsive', type: 'error' }] + } + ], + checkBalances: () => [] +} + +const devices = [ + { + deviceId: + '7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4', + lastPing: '2020-11-16T13:11:03.169Z', + name: 'Abc123' + }, + { + deviceId: + '9871e58aa2643ff9445cbc299b50397430ada75157d6c29b4c93548fff0f48f7', + lastPing: '2020-11-16T16:21:35.948Z', + name: 'Machine 2' + }, + { + deviceId: + '5ae0d02dedeb77b6521bd5eb7c9159bdc025873fa0bcb6f87aaddfbda0c50913', + lastPing: '2020-11-19T15:07:57.089Z', + name: 'Machine 3' + }, + { + deviceId: + 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', + lastPing: '2020-11-23T19:34:41.031Z', + name: 'New Machine 4 ' + } +] + +test('Exits checkNotifications with Promise.resolve() if SMS and Email are disabled', async () => { + expect.assertions(1) + await expect( + notifier.checkNotification({ + getNotificationConfig: () => ({ + sms: { active: false, errors: false }, + email: { active: false, errors: false } + }) + }) + ).resolves.toBe(undefined) +}) + +test('Exits checkNotifications with Promise.resolve() if SMS and Email are disabled even if errors or balance are defined to something', async () => { + expect.assertions(1) + await expect( + notifier.checkNotification({ + getNotificationConfig: () => ({ + sms: { active: false, errors: true, balance: true }, + email: { active: false, errors: true, balance: true } + }) + }) + ).resolves.toBe(undefined) +}) + +test("Check Pings should return code PING for devices that haven't been pinged recently", () => { + expect( + notifier.checkPings([ + { + deviceId: + '7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4', + lastPing: '2020-11-16T13:11:03.169Z', + name: 'Abc123' + } + ]) + ).toMatchObject({ + '7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4': [ + { code: 'PING', machineName: 'Abc123' } + ] + }) +}) + +test('Checkpings returns empty array as the value for the id prop, if the lastPing is more recent than 60 seconds', () => { + expect( + notifier.checkPings([ + { + deviceId: + '7a531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4', + lastPing: new Date(), + name: 'Abc123' + } + ]) + ).toMatchObject({ + '7a531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4': [] + }) +}) + +afterEach(() => { + // https://stackoverflow.com/questions/58151010/difference-between-resetallmocks-resetmodules-resetmoduleregistry-restoreallm + jest.restoreAllMocks() +}) + +const utils = require('../utils') + +test('Check notification resolves to undefined if shouldNotAlert is called and is true', async () => { + const mockShouldNotAlert = jest.spyOn(utils, 'shouldNotAlert') + mockShouldNotAlert.mockReturnValue(true) + + const result = await notifier.checkNotification(plugins) + expect(mockShouldNotAlert).toHaveBeenCalledTimes(1) + expect(result).toBe(undefined) +}) + +test('Sendmessage is called if shouldNotAlert is called and is false', async () => { + const mockShouldNotAlert = jest.spyOn(utils, 'shouldNotAlert') + const mockSendMessage = jest.spyOn(plugins, 'sendMessage') + + mockShouldNotAlert.mockReturnValue(false) + const result = await notifier.checkNotification(plugins) + + expect(mockShouldNotAlert).toHaveBeenCalledTimes(1) + expect(mockSendMessage).toHaveBeenCalledTimes(1) + expect(result).toBe(undefined) +}) + +test('If no alert fingerprint and inAlert is true, exits on call to sendNoAlerts', async () => { + // mock utils.buildAlertFingerprint to return null + // mock utils.getAlertFingerprint to be true which will make inAlert true + + const buildFp = jest.spyOn(utils, 'buildAlertFingerprint') + const mockGetFp = jest.spyOn(utils, 'getAlertFingerprint') + const mockSendNoAlerts = jest.spyOn(utils, 'sendNoAlerts') + + buildFp.mockReturnValue(null) + mockGetFp.mockReturnValue(true) + await notifier.checkNotification(plugins) + + expect(mockGetFp).toHaveBeenCalledTimes(1) + expect(mockSendNoAlerts).toHaveBeenCalledTimes(1) +}) + + +// vvv tests for checkstuckscreen... +test('checkStuckScreen returns [] when no events are found', () => { + expect(notifier.checkStuckScreen([], 'Abc123')).toEqual([]) +}) + +test('checkStuckScreen returns [] if most recent event is idle', () => { + // device_time is what matters for the sorting of the events by recency + expect(notifier.checkStuckScreen([{ + id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', + device_id: 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', + event_type: 'stateChange', + note: '{"state":"chooseCoin","isIdle":false}', + created: "2020-11-23T19:30:29.209Z", + device_time: "1999-11-23T19:30:29.177Z", + age: 157352628.123 + }, { + id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', + device_id: 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', + event_type: 'stateChange', + note: '{"state":"chooseCoin","isIdle":true}', + created: "2020-11-23T19:30:29.209Z", + device_time: "2020-11-23T19:30:29.177Z", + age: 157352628.123 + }])).toEqual([]) +}) + +test('checkStuckScreen returns object array of length 1 with prop code: "STALE" if age > STALE_STATE', () => { + // there is an age 0 and an isIdle true in the first object but it will be below the second one in the sorting order and thus ignored + const result = notifier.checkStuckScreen([{ + id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', + device_id: 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', + event_type: 'stateChange', + note: '{"state":"chooseCoin","isIdle":true}', + created: "2020-11-23T19:30:29.209Z", + device_time: "1999-11-23T19:30:29.177Z", + age: 0 + }, { + id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', + device_id: 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', + event_type: 'stateChange', + note: '{"state":"chooseCoin","isIdle":false}', + created: "2020-11-23T19:30:29.209Z", + device_time: "2020-11-23T19:30:29.177Z", + age: 157352628.123 + }]) + expect(result[0]).toMatchObject({code: "STALE"}) +}) + +test('checkStuckScreen returns empty array if age < STALE_STATE', () => { + const STALE_STATE = require("../codes").STALE_STATE + const result1 = notifier.checkStuckScreen([{ + id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', + device_id: 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', + event_type: 'stateChange', + note: '{"state":"chooseCoin","isIdle":false}', + created: "2020-11-23T19:30:29.209Z", + device_time: "2020-11-23T19:30:29.177Z", + age: 0 + }]) + const result2 = notifier.checkStuckScreen([{ + id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', + device_id: 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', + event_type: 'stateChange', + note: '{"state":"chooseCoin","isIdle":false}', + created: "2020-11-23T19:30:29.209Z", + device_time: "2020-11-23T19:30:29.177Z", + age: STALE_STATE + }]) + expect(result1).toEqual([]) + expect(result2).toEqual([]) +}) \ No newline at end of file diff --git a/lib/notifier/test/sms.test.js b/lib/notifier/test/sms.test.js new file mode 100644 index 00000000..d42ac7ac --- /dev/null +++ b/lib/notifier/test/sms.test.js @@ -0,0 +1,22 @@ +const sms = require('../sms') + +const alertRec = { + devices: { + f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: { + balanceAlerts: [], + deviceAlerts: [ + { code: 'PING', age: 602784301.446, machineName: 'Abc123' } + ] + } + }, + deviceNames: { + f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: 'Abc123' + }, + general: [] +} + +test('Print SMS alerts', () => { + expect(sms.printSmsAlerts(alertRec, { active: true, errors: true })).toBe( + '[Lamassu] Errors reported: Machine Down (Abc123)' + ) +}) diff --git a/lib/notifier/test/utils.test.js b/lib/notifier/test/utils.test.js new file mode 100644 index 00000000..785d76ef --- /dev/null +++ b/lib/notifier/test/utils.test.js @@ -0,0 +1,100 @@ +const utils = require('../utils') + +const plugins = { + sendMessage: rec => { + return rec + } +} + +const alertRec = { + devices: { + f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: { + balanceAlerts: [], + deviceAlerts: [ + { code: 'PING', age: 1605532263169, machineName: 'Abc123' } + ] + } + }, + deviceNames: { + f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: 'Abc123' + }, + general: [] +} + +const notifications = { + sms: { active: true, errors: true }, + email: { active: false, errors: false } +} + +test('Build alert fingerprint returns null if no sms or email alerts', () => { + expect( + utils.buildAlertFingerprint( + { + devices: {}, + deviceNames: {}, + general: [] + }, + notifications + ) + ).toBe(null) +}) + +test('Build alert fingerprint returns null if sms and email are disabled', () => { + expect( + utils.buildAlertFingerprint(alertRec, { + sms: { active: false, errors: true }, + email: { active: false, errors: false } + }) + ).toBe(null) +}) + +test('Build alert fingerprint returns hash if email or [sms] are enabled and there are alerts in alertrec', () => { + expect( + typeof utils.buildAlertFingerprint(alertRec, { + sms: { active: true, errors: true }, + email: { active: false, errors: false } + }) + ).toBe('string') +}) + +test('Build alert fingerprint returns hash if [email] or sms are enabled and there are alerts in alertrec', () => { + expect( + typeof utils.buildAlertFingerprint(alertRec, { + sms: { active: false, errors: false }, + email: { active: true, errors: true } + }) + ).toBe('string') +}) + +test('Send no alerts returns empty object with sms and email disabled', () => { + expect(utils.sendNoAlerts(plugins, false, false)).toEqual({}) +}) + +test('Send no alerts returns object with sms prop with sms only enabled', () => { + expect(utils.sendNoAlerts(plugins, true, false)).toEqual({ + sms: { + body: '[Lamassu] All clear' + } + }) +}) + +test('Send no alerts returns object with sms and email prop with both enabled', () => { + expect(utils.sendNoAlerts(plugins, true, true)).toEqual({ + email: { + body: 'No errors are reported for your machines.', + subject: '[Lamassu] All clear' + }, + sms: { + body: '[Lamassu] All clear' + } + }) +}) + +test('Send no alerts returns object with email prop if only email is enabled', () => { + expect(utils.sendNoAlerts(plugins, false, true)).toEqual({ + email: { + body: 'No errors are reported for your machines.', + subject: '[Lamassu] All clear' + } + }) +}) diff --git a/lib/notifier/utils.js b/lib/notifier/utils.js new file mode 100644 index 00000000..c3ef184f --- /dev/null +++ b/lib/notifier/utils.js @@ -0,0 +1,111 @@ +const _ = require('lodash/fp') +const crypto = require('crypto') +const { + CODES_DISPLAY, + NETWORK_DOWN_TIME, + PING, + ALERT_SEND_INTERVAL +} = require('./codes') + +function parseEventNote(event) { + return _.set('note', JSON.parse(event.note), event) +} + +function checkPing(device) { + const age = +Date.now() - +new Date(device.lastPing) + if (age > NETWORK_DOWN_TIME) + return [{ code: PING, age, machineName: device.name }] + return [] +} + +const getDeviceTime = _.flow(_.get('device_time'), Date.parse) + +const isActive = it => it.active && (it.balance || it.errors) + +const codeDisplay = code => CODES_DISPLAY[code] + +const alertFingerprint = { + fingerprint: null, + lastAlertTime: null +} + +const getAlertFingerprint = () => alertFingerprint.fingerprint + +const getLastAlertTime = () => alertFingerprint.lastAlertTime + +const setAlertFingerprint = (fp, time = Date.now()) => { + alertFingerprint.fingerprint = fp + alertFingerprint.lastAlertTime = time +} + +const shouldNotAlert = currentAlertFingerprint => { + return ( + currentAlertFingerprint === getAlertFingerprint() && + getLastAlertTime() - Date.now() < ALERT_SEND_INTERVAL + ) +} + +function getAlertTypes(alertRec, config) { + let alerts = [] + if (!isActive(config)) return alerts + + if (config.balance) { + alerts = _.concat(alerts, alertRec.general) + } + + _.keys(alertRec.devices).forEach(function (device) { + if (config.balance) { + alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts) + } + + if (config.errors) { + alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts) + } + }) + + return alerts +} + +function buildAlertFingerprint(alertRec, notifications) { + const sms = getAlertTypes(alertRec, notifications.sms) + const email = getAlertTypes(alertRec, notifications.email) + if (sms.length === 0 && email.length === 0) return null + + const smsTypes = _.map(codeDisplay, _.uniq(_.map('code', sms))).sort() + const emailTypes = _.map(codeDisplay, _.uniq(_.map('code', email))).sort() + + const subject = _.concat(smsTypes, emailTypes).join(', ') + return crypto.createHash('sha256').update(subject).digest('hex') +} + +function sendNoAlerts(plugins, smsEnabled, emailEnabled) { + const subject = '[Lamassu] All clear' + + let rec = {} + if (smsEnabled) { + rec = _.set(['sms', 'body'])(subject)(rec) + } + + if (emailEnabled) { + rec = _.set(['email', 'subject'])(subject)(rec) + rec = _.set(['email', 'body'])('No errors are reported for your machines.')( + rec + ) + } + + return plugins.sendMessage(rec) +} + +module.exports = { + codeDisplay, + parseEventNote, + getDeviceTime, + checkPing, + isActive, + getAlertFingerprint, + getLastAlertTime, + setAlertFingerprint, + shouldNotAlert, + buildAlertFingerprint, + sendNoAlerts +} diff --git a/lib/plugins/sms/mock-sms/mock-sms.js b/lib/plugins/sms/mock-sms/mock-sms.js index 952fca3c..19c43d07 100644 --- a/lib/plugins/sms/mock-sms/mock-sms.js +++ b/lib/plugins/sms/mock-sms/mock-sms.js @@ -2,7 +2,7 @@ const _ = require('lodash/fp') exports.NAME = 'MockSMS' -exports.sendMessage = function sendMessage (account, rec) { +exports.sendMessage = function sendMessage(account, rec) { console.log('Sending SMS: %j', rec) return new Promise((resolve, reject) => { if (_.endsWith('666', _.getOr(false, 'sms.toNumber', rec))) { diff --git a/package-lock.json b/package-lock.json index 9a9c9079..17836eb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,19 +57,19 @@ } }, "@babel/core": { - "version": "7.12.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.10.tgz", - "integrity": "sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w==", + "version": "7.12.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.3.tgz", + "integrity": "sha512-0qXcZYKZp3/6N2jKYVxZv0aNCsxTSVCiK72DTiTYZAu7sjg73W0/aynWjMbiGd87EQL4WyA8reiJVh92AVla9g==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.10", + "@babel/generator": "^7.12.1", "@babel/helper-module-transforms": "^7.12.1", - "@babel/helpers": "^7.12.5", - "@babel/parser": "^7.12.10", - "@babel/template": "^7.12.7", - "@babel/traverse": "^7.12.10", - "@babel/types": "^7.12.10", + "@babel/helpers": "^7.12.1", + "@babel/parser": "^7.12.3", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.1", + "@babel/types": "^7.12.1", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.1", @@ -119,6 +119,23 @@ "source-map": "^0.5.0" }, "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -136,6 +153,42 @@ "@babel/helper-get-function-arity": "^7.12.10", "@babel/template": "^7.12.7", "@babel/types": "^7.12.11" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/parser": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", + "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==", + "dev": true + }, + "@babel/template": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", + "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7" + } + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-get-function-arity": { @@ -145,15 +198,25 @@ "dev": true, "requires": { "@babel/types": "^7.12.10" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", - "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", - "dev": true, - "requires": { - "@babel/types": "^7.12.7" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-module-imports": { @@ -182,15 +245,6 @@ "lodash": "^4.17.19" } }, - "@babel/helper-optimise-call-expression": { - "version": "7.12.10", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz", - "integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==", - "dev": true, - "requires": { - "@babel/types": "^7.12.10" - } - }, "@babel/helper-plugin-utils": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", @@ -207,6 +261,90 @@ "@babel/helper-optimise-call-expression": "^7.12.10", "@babel/traverse": "^7.12.10", "@babel/types": "^7.12.11" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", + "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", + "dev": true, + "requires": { + "@babel/types": "^7.12.7" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz", + "integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==", + "dev": true, + "requires": { + "@babel/types": "^7.12.10" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/parser": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", + "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==", + "dev": true + }, + "@babel/traverse": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.12.tgz", + "integrity": "sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.11", + "@babel/generator": "^7.12.11", + "@babel/helper-function-name": "^7.12.11", + "@babel/helper-split-export-declaration": "^7.12.11", + "@babel/parser": "^7.12.11", + "@babel/types": "^7.12.12", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } } }, "@babel/helper-simple-access": { @@ -225,6 +363,25 @@ "dev": true, "requires": { "@babel/types": "^7.12.11" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-validator-identifier": { @@ -256,9 +413,9 @@ } }, "@babel/parser": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", - "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", + "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", "dev": true }, "@babel/plugin-syntax-async-generators": { @@ -385,28 +542,28 @@ } }, "@babel/template": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", - "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.12.7", - "@babel/types": "^7.12.7" + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/traverse": { - "version": "7.12.12", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.12.tgz", - "integrity": "sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.5.tgz", + "integrity": "sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA==", "dev": true, "requires": { - "@babel/code-frame": "^7.12.11", - "@babel/generator": "^7.12.11", - "@babel/helper-function-name": "^7.12.11", - "@babel/helper-split-export-declaration": "^7.12.11", - "@babel/parser": "^7.12.11", - "@babel/types": "^7.12.12", + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.5", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.12.5", + "@babel/types": "^7.12.5", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" @@ -439,9 +596,9 @@ } }, "@babel/types": { - "version": "7.12.12", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", - "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.12.11", @@ -1186,9 +1343,9 @@ } }, "@types/babel__traverse": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.11.0.tgz", - "integrity": "sha512-kSjgDMZONiIfSH1Nxcr5JIRMwUetDki63FSQfpTCz8ogF3Ulqm8+mr5f78dUYs6vMiB6gBusQqfQmBvHZj/lwg==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.15.tgz", + "integrity": "sha512-Pzh9O3sTK8V6I1olsXpCfj2k/ygO2q1X0vhhnDrEQyYLHZesWz+zMZMVcwXLCYf0U36EtmyYaFGPfXlTtDHe3A==", "dev": true, "requires": { "@babel/types": "^7.3.0" @@ -1470,9 +1627,9 @@ } }, "@types/yargs": { - "version": "15.0.12", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.12.tgz", - "integrity": "sha512-f+fD/fQAo3BCbCDlrUpznF1A5Zp9rB0noS5vnoormHSIPFKL0Z2DcUJ3Gxp5ytH4uLRNxy7AwYUC9exZzqGMAw==", + "version": "15.0.10", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.10.tgz", + "integrity": "sha512-z8PNtlhrj7eJNLmrAivM7rjBESG6JwC5xP3RVk12i/8HVP7Xnx/sEmERnRImyEuUaJfO942X0qMOYsoupaJbZQ==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -7649,9 +7806,9 @@ } }, "is-core-module": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", - "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz", + "integrity": "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==", "dev": true, "requires": { "has": "^1.0.3" @@ -8193,9 +8350,9 @@ } }, "y18n": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", "dev": true }, "yargs": { @@ -9174,9 +9331,9 @@ } }, "y18n": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", "dev": true }, "yargs": { @@ -9640,9 +9797,9 @@ } }, "ws": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz", - "integrity": "sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz", + "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==", "dev": true } }