UNPKG

@barchart/common-node-js

Version:

Common classes, utilities, and functions for building Node.js servers

213 lines (167 loc) 5.8 kB
const log4js = require('log4js'), twilio = require('twilio'); const assert = require('@barchart/common-js/lang/assert'), is = require('@barchart/common-js/lang/is'), Disposable = require('@barchart/common-js/lang/Disposable'), RateLimiter = require('@barchart/common-js/timing/RateLimiter'); module.exports = (() => { 'use strict'; const logger = log4js.getLogger('common-node/sms/TwilioProvider'); /** * A wrapper for the Twilio SDK. * * @public * @param {Object} configuration * @param {Object} configuration.accountSid * @param {Object} configuration.authToken * @param {Object} configuration.sourceNumber * @param {Array<String>|String=} recipientOverride * @param {Number=} rateLimitPerSecond */ class TwilioProvider extends Disposable { constructor(configuration) { super(); assert.argumentIsRequired(configuration, 'configuration'); assert.argumentIsRequired(configuration.accountSid, 'configuration.accountSid', String); assert.argumentIsRequired(configuration.authToken, 'configuration.authToken', String); assert.argumentIsRequired(configuration.sourceNumber, 'configuration.sourceNumber', String); if (is.array(configuration.recipientOverride)) { assert.argumentIsArray(configuration.recipientOverride, 'configuration.recipientOverride', String); } else { assert.argumentIsOptional(configuration.recipientOverride, 'configuration.recipientOverride', String); } assert.argumentIsOptional(configuration.rateLimitPerSecond, 'configuration.rateLimitPerSecond', Number); this._configuration = configuration; this._startPromise = null; this._started = false; this._client = null; this._rateLimiter = null; this._counter = 0; } start() { if (this._startPromise === null) { this._startPromise = Promise.resolve() .then(() => { logger.info('Twilio provider started'); this._client = twilio(this._configuration.accountSid, this._configuration.authToken); this._rateLimiter = new RateLimiter(this._configuration.rateLimitPerSecond || 10, 1000); this._started = true; return Promise.resolve(true); }).catch((e) => { logger.error('Twilio provider failed to start', e); return Promise.reject(false); }); } return this._startPromise; } /** * Sends an SMS message to one (or more) phone numbers. * * @public * @param {String|Array<String>} recipients * @param {String} content * @param {String=} source * @returns {Promise<String|Array<String>>} */ sendSms(recipients, content, source) { return Promise.resolve() .then(() => { if (is.array(recipients)) { assert.argumentIsArray(recipients, 'recipients', String); } else { assert.argumentIsRequired(recipients, 'recipients', String); } assert.argumentIsRequired(content, 'content', String); assert.argumentIsOptional(source, 'source', String); checkReady.call(this); let recipientsToUse; let recipientsPlural; if (this._configuration.recipientOverride) { recipientsToUse = [ this._configuration.recipientOverride ]; recipientsPlural = false; } else if (is.array(recipients)) { recipientsToUse = recipients; recipientsPlural = true; } else { recipientsToUse = [ recipients ]; recipientsPlural = false; } let sourceToUse; if (source) { sourceToUse = source; } else { sourceToUse = this._configuration.sourceNumber; } const sendAction = () => { return Promise.all(recipientsToUse.map((recipient) => { const id = this._counter++; const payload = { }; payload.from = sourceToUse; payload.to = recipient; payload.body = content; logger.debug('Sending SMS [', id, '] via Twilio to [', recipient, ']'); return this._client.messages.create(payload) .then((response) => { logger.debug('Sent SMS [', id, '] via Twilio to [', recipient, '] with SID [', response.sid, ']'); return response.sid; }).catch((e) => { logger.error('Failed to send SMS [', id, '] via Twilio to [', recipient, ']', e); return null; }); })).then((responses) => { if (recipientsPlural) { return responses; } else { return responses[0]; } }); }; return this._rateLimiter.enqueue(sendAction); }); } /** * Retrieve the E.164 formatted phone number, given a phone number * in another format. * * @public * @param {String} phone * @returns {Promise<String|null>} */ getPhoneNumberE164(phone) { return Promise.resolve() .then(() => { assert.argumentIsRequired(phone, 'phone', String); checkReady.call(this); logger.debug('Looking up phone number [', phone, '] via Twilio'); return this._client.lookups.phoneNumbers(phone).fetch({ type: [ 'carrier' ] }) .then((response) => { logger.debug('Lookup data retrieved for phone number [', phone, '] via Twilio with e164 [', response.phoneNumber, ']'); return response.phoneNumber; }).catch((e) => { logger.error('Lookup failed for phone number [', phone, '] via Twilio', e); return null; }); }); } _onDispose() { logger.debug('Twilio disposed'); if (this._rateLimiter !== null) { this._rateLimiter.dispose(); } this._rateLimiter = null; this._client = null; } toString() { return '[TwilioProvider]'; } } function checkReady() { if (this.getIsDisposed()) { throw new Error('The Twilio provider has been disposed.'); } if (!this._started) { throw new Error('The Twilio Provider has not been started.'); } } return TwilioProvider; })();