From 8d752ec876dd7a0c616e5064bd83acc6f9d5666b Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Thu, 20 Aug 2020 15:22:22 -0500 Subject: [PATCH 001/191] Contact User Number table --- .../20200820144200_contact_user_number.js | 27 ++++++ src/server/api/lib/message-sending.js | 6 +- src/server/api/lib/twilio.js | 48 +++++++++-- .../cacheable_queries/campaign-contact.js | 82 ++++++++++++------- .../cacheable_queries/contact-user-number.js | 65 +++++++++++++++ src/server/models/cacheable_queries/index.js | 2 + .../models/cacheable_queries/message.js | 42 ++++++++-- src/server/models/contact-user-number.js | 19 +++++ src/server/models/index.js | 3 + src/server/models/message.js | 5 ++ src/workers/jobs.js | 3 +- 11 files changed, 256 insertions(+), 46 deletions(-) create mode 100644 migrations/20200820144200_contact_user_number.js create mode 100644 src/server/models/cacheable_queries/contact-user-number.js create mode 100644 src/server/models/contact-user-number.js diff --git a/migrations/20200820144200_contact_user_number.js b/migrations/20200820144200_contact_user_number.js new file mode 100644 index 000000000..3bb4376a5 --- /dev/null +++ b/migrations/20200820144200_contact_user_number.js @@ -0,0 +1,27 @@ +exports.up = async function up(knex, Promise) { + if (!(await knex.schema.hasTable("contact_user_number"))) { + return knex.schema.createTable("contact_user_number", t => { + t.increments("id"); + t.text("organization_id"); + t.text("contact_number"); + t.text("user_number"); + + t.index( + ["organization_id", "contact_number"], + "contact_user_number_organization_contact_number" + ); + + t.unique(["organization_id", "contact_number"]); + + knex + .table("message") + .index(["contact_number", "user_number"], "cell_user_number_idx"); + }); + } + + return Promise.resolve(); +}; + +exports.down = function down(knex, Promise) { + return knex.schema.dropTableIfExists("contact_user_number"); +}; diff --git a/src/server/api/lib/message-sending.js b/src/server/api/lib/message-sending.js index 71befbb0f..ce8dd4e01 100644 --- a/src/server/api/lib/message-sending.js +++ b/src/server/api/lib/message-sending.js @@ -3,12 +3,14 @@ import { r, cacheableData } from "../../models"; export async function getLastMessage({ contactNumber, service, - messageServiceSid + messageServiceSid, + userNumber }) { const lookup = await cacheableData.campaignContact.lookupByCell( contactNumber, service, - messageServiceSid + messageServiceSid, + userNumber ); return lookup; } diff --git a/src/server/api/lib/twilio.js b/src/server/api/lib/twilio.js index 9988cb3cb..d1f906b32 100644 --- a/src/server/api/lib/twilio.js +++ b/src/server/api/lib/twilio.js @@ -153,6 +153,13 @@ async function getMessagingServiceSid( ); } +async function getContactUserNumber(organizationId, contactNumber) { + return await cacheableData.contactUserNumber.query({ + organizationId, + contactNumber + }); +} + async function sendMessage(message, contact, trx, organization, campaign) { const twilio = await getTwilio(organization); const APITEST = /twilioapitest/.test(message.text); @@ -182,6 +189,20 @@ async function sendMessage(message, contact, trx, organization, campaign) { campaign ); + let userNumber; + if (process.env.EXPERIMENTAL_STICKY_SENDER) { + console.log("getting user number", organization.id, contact.cell); + const contactUserNumber = await getContactUserNumber( + organization.id, + contact.cell + ); + console.log("got user number", userNumber); + + if (contactUserNumber) { + userNumber = contactUserNumber.user_number; + } + } + return new Promise((resolve, reject) => { if (message.service !== "twilio") { log.warn("Message not marked as a twilio message", message.id); @@ -219,17 +240,23 @@ async function sendMessage(message, contact, trx, organization, campaign) { } const changes = {}; - changes.messageservice_sid = messagingServiceSid; + if (userNumber) { + changes.user_number = userNumber; + } else { + changes.messageservice_sid = messagingServiceSid; + } const messageParams = Object.assign( { to: message.contact_number, body: message.text, - messagingServiceSid: messagingServiceSid, statusCallback: process.env.TWILIO_STATUS_CALLBACK_URL }, twilioValidityPeriod ? { validityPeriod: twilioValidityPeriod } : {}, - parseMessageText(message) + parseMessageText(message), + userNumber + ? { from: userNumber } + : { messagingServiceSid: messagingServiceSid } ); console.log("twilioMessage", messageParams); @@ -352,10 +379,11 @@ export function postMessageSend( }; Promise.all([ updateQuery.update(changesToSave), - cacheableData.campaignContact.updateStatus({ - ...contact, - messageservice_sid: changesToSave.messageservice_sid - }) + cacheableData.campaignContact.updateStatus( + contact, + null, + changesToSave.messageservice_sid || changesToSave.user_number + ) ]) .then((newMessage, cacheResult) => { resolve({ @@ -535,15 +563,21 @@ async function addNumberToMessagingService( * Buy a phone number and add it to the owned_phone_number table */ async function buyNumber(organization, twilioInstance, phoneNumber, opts = {}) { + const twilioBaseUrl = getConfig("TWILIO_BASE_CALLBACK_URL", organization); + const response = await twilioInstance.incomingPhoneNumbers.create({ phoneNumber, friendlyName: `Managed by Spoke [${process.env.BASE_URL}]: ${phoneNumber}`, + smsUrl: urlJoin(twilioBaseUrl, "twilio", organization.id.toString()), voiceUrl: getConfig("TWILIO_VOICE_URL", organization) // will use default twilio recording if undefined }); + if (response.error) { throw new Error(`Error buying twilio number: ${response.error}`); } + log.debug(`Bought number ${phoneNumber} [${response.sid}]`); + let allocationFields = {}; const messagingServiceSid = opts && opts.messagingServiceSid; if (messagingServiceSid) { diff --git a/src/server/models/cacheable_queries/campaign-contact.js b/src/server/models/cacheable_queries/campaign-contact.js index ef1ace151..f07ab2e0c 100644 --- a/src/server/models/cacheable_queries/campaign-contact.js +++ b/src/server/models/cacheable_queries/campaign-contact.js @@ -335,7 +335,13 @@ const campaignContactCache = { orgId: async contact => contact.organization_id || ((await campaignCache.load(contact.campaign_id)) || {}).organization_id, - lookupByCell: async (cell, service, messageServiceSid, bailWithoutCache) => { + lookupByCell: async ( + cell, + service, + messageServiceSid, + userNumber, + bailWithoutCache + ) => { // Used to lookup contact/campaign information by cell number for incoming messages // in order to map it to the existing campaign, since Twilio, etc "doesn't know" // what campaign or other objects this is. @@ -346,7 +352,7 @@ const campaignContactCache = { // which is called for incoming AND outgoing messages. if (r.redis && CONTACT_CACHE_ENABLED) { const cellData = await r.redis.getAsync( - cellTargetKey(cell, messageServiceSid) + cellTargetKey(cell, messageServiceSid || userNumber) ); // console.log('lookupByCell cache', cell, service, messageServiceSid, cellData) if (cellData) { @@ -367,15 +373,23 @@ const campaignContactCache = { return false; } } + + const fromFilter = messageServiceSid + ? { messageservice_sid: messageServiceSid } + : { user_number: userNumber }; let messageQuery = r .knex("message") .select("campaign_contact_id") - .where({ - is_from_contact: false, - contact_number: cell, - messageservice_sid: messageServiceSid, - service - }) + .where( + Object.assign( + { + is_from_contact: false, + contact_number: cell, + service + }, + fromFilter + ) + ) .orderBy("message.created_at", "desc") .limit(1); if (r.redis) { @@ -436,7 +450,7 @@ const campaignContactCache = { console.log("updateCampaignAssignmentCache", data[0], data.length); } }, - updateStatus: async (contact, newStatus) => { + updateStatus: async (contact, newStatus, messageServiceOrUserNumber) => { // console.log('updateSTATUS', newStatus, contact) try { await r @@ -447,33 +461,41 @@ const campaignContactCache = { if (r.redis && CONTACT_CACHE_ENABLED) { const contactKey = cacheKey(contact.id); const statusKey = messageStatusKey(contact.id); - // NOTE: contact.messageservice_sid is not a field, but will have been - // added on to the contact object from message.save - // Other contexts don't really need to update the cell key -- just the status - const cellKey = cellTargetKey(contact.cell, contact.messageservice_sid); - // console.log('contact updateStatus', cellKey, newStatus, contact) + let redisQuery = r.redis - .multi() - // We update the cell info on status updates, because this happens - // during message sending -- this is exactly the moment we want to - // 'steal' a cell from one (presumably older) campaign into another - // delay expiration for contacts we continue to update - .set( - cellKey, - [ - contact.id, - "", - contact.timezone_offset || "", - contact.campaign_id || "" - ].join(":") - ) + .mulit() // delay expiration for contacts we continue to update .expire(contactKey, 43200) - .expire(statusKey, 43200) - .expire(cellKey, 43200); + .expire(statusKey, 43200); + + if (messageServiceOrUserNumber) { + // Other contexts don't really need to update the cell key -- just the status + const cellKey = cellTargetKey( + contact.cell, + messageServiceOrUserNumber + ); + + redisQuery = redisQuery + // We update the cell info on status updates, because this happens + // during message sending -- this is exactly the moment we want to + // 'steal' a cell from one (presumably older) campaign into another + // delay expiration for contacts we continue to update + .set( + cellKey, + [ + contact.id, + "", + contact.timezone_offset || "", + contact.campaign_id || "" + ].join(":") + ) + .expire(cellKey, 43200); + } + if (newStatus) { redisQuery = redisQuery.set(statusKey, newStatus); } + await redisQuery.execAsync(); //await updateAssignmentContact(contact, newStatus); } diff --git a/src/server/models/cacheable_queries/contact-user-number.js b/src/server/models/cacheable_queries/contact-user-number.js new file mode 100644 index 000000000..4b6157790 --- /dev/null +++ b/src/server/models/cacheable_queries/contact-user-number.js @@ -0,0 +1,65 @@ +import { r } from "../../models"; + +// Datastructure: +// * regular GET/SET with JSON ordered list of the objects {id,title,text} +// * keyed by campaignId-userId pairs -- userId is '' for global campaign records +// Requirements: +// * needs an order +// * needs to get by campaignId-userId pairs + +const getCacheKey = (organizationId, contactNumber) => + `${process.env.CACHE_PREFIX || + ""}-contact-user-number-${organizationId}-${contactNumber}`; + +const contactUserNumberCache = { + query: async ({ organizationId, contactNumber }) => { + const cacheKey = getCacheKey(organizationId, contactNumber); + + if (r.redis) { + const contactUserNumber = await r.redis.getAsync(cacheKey); + + if (contactUserNumber) { + return JSON.parse(contactUserNumber); + } + } + + const [contactUserNumber] = await r + .knex("contact_user_number") + .where({ organization_id: organizationId, contact_number: contactNumber }) + .select("organization_id", "contact_number", "user_number") + .limit(1); + + if (r.redis) { + await r.redis + .multi() + .set(cacheKey, JSON.stringify(contactUserNumber)) + .expire(cacheKey, 43200) // 12 hours + .execAsync(); + } + + return contactUserNumber; + }, + save: async ({ organizationId, contactNumber, userNumber }) => { + const contactUserNumber = { + organization_id: organizationId, + contact_number: contactNumber, + user_number: userNumber + }; + + await r.knex("contact_phone_number").insert(contactUserNumber); + + if (r.redis) { + const cacheKey = getCacheKey(organizationId, contactNumber); + + await r.redis + .multi() + .set(cacheKey, JSON.stringify(contactUserNumber)) + .expire(cacheKey, 43200) // 12 hours + .execAsync(); + } + + return await knex.raw(query); + } +}; + +export default contactUserNumberCache; diff --git a/src/server/models/cacheable_queries/index.js b/src/server/models/cacheable_queries/index.js index 61dafcd80..a1f8f2f7f 100644 --- a/src/server/models/cacheable_queries/index.js +++ b/src/server/models/cacheable_queries/index.js @@ -2,6 +2,7 @@ import assignment from "./assignment"; import campaign from "./campaign"; import campaignContact from "./campaign-contact"; import cannedResponse from "./canned-response"; +import contactUserNumber from "./contact-user-number"; import message from "./message"; import optOut from "./opt-out"; import organization from "./organization"; @@ -14,6 +15,7 @@ const cacheableData = { campaign, campaignContact, cannedResponse, + contactUserNumber, message, optOut, organization, diff --git a/src/server/models/cacheable_queries/message.js b/src/server/models/cacheable_queries/message.js index ae612b920..dd5f65739 100644 --- a/src/server/models/cacheable_queries/message.js +++ b/src/server/models/cacheable_queries/message.js @@ -1,6 +1,7 @@ import { r, Message } from "../../models"; import campaignCache from "./campaign"; import campaignContactCache from "./campaign-contact"; +import contactUserNumberCache from "./contact-user-number"; import { getMessageHandlers } from "../../../extensions/message-handlers"; // QUEUE // messages- @@ -51,6 +52,7 @@ const contactIdFromOther = async ({ cell, service || "", messageServiceSid, + null, // is this even used? /* bailWithoutCache*/ true ); if (cellLookup) { @@ -172,6 +174,7 @@ const incomingMessageMatching = async (messageInstance, activeCellFound) => { const deliveryReport = async ({ contactNumber, + userNumber, messageSid, service, messageServiceSid, @@ -188,7 +191,8 @@ const deliveryReport = async ({ const lookup = await campaignContactCache.lookupByCell( contactNumber, service || "", - messageServiceSid + messageServiceSid, + userNumber ); if (lookup && lookup.campaign_contact_id) { await r @@ -201,11 +205,33 @@ const deliveryReport = async ({ campaignCache.incrCount(lookup.campaign_id, "errorCount"); } } - await r + + const message = await r .knex("message") .where("service_id", messageSid) .limit(1) - .update(changes); + .update(changes) + .returning("*"); + + if (process.env.EXPERIMENTAL_STICKY_SENDER && newStatus === "DELIVERED") { + const campaignContact = await campaignContactCache.load( + message.campaign_contact_id + ); + const organizationId = await campaignContactCache.orgId(campaignContact); + + const contactUserNumber = await contactUserNumberCache.query({ + organizationId, + contactNumber + }); + + if (!contactUserNumber) { + await contactUserNumberCache.save({ + organizationId: organizationId, + contactNumber: contactNumber, + userNumber: userNumber + }); + } + } }; const messageCache = { @@ -241,7 +267,8 @@ const messageCache = { activeCellFound = await campaignContactCache.lookupByCell( messageInstance.contact_number, messageInstance.service, - messageInstance.messageservice_sid + messageInstance.messageservice_sid, + messageInstance.user_number ); // console.log("messageCache activeCellFound", activeCellFound); const matchError = await incomingMessageMatching( @@ -318,11 +345,14 @@ const messageCache = { const contactData = { id: messageToSave.campaign_contact_id, cell: messageToSave.contact_number, - messageservice_sid: messageToSave.messageservice_sid, campaign_id: campaignId }; // console.log("messageCache hi saveMsg3", messageToSave.id, newStatus, contactData); - await campaignContactCache.updateStatus(contactData, newStatus); + await campaignContactCache.updateStatus( + contactData, + newStatus, + messageToSave.messageservice_sid || messageToSave.user_number + ); // console.log("messageCache saveMsg4", newStatus); // update campaign counts diff --git a/src/server/models/contact-user-number.js b/src/server/models/contact-user-number.js new file mode 100644 index 000000000..2d1f622c6 --- /dev/null +++ b/src/server/models/contact-user-number.js @@ -0,0 +1,19 @@ +import thinky from "./thinky"; +const type = thinky.type; +import { requiredString } from "./custom-types"; + +// For documentation purposes only. Use knex queries instead of this model. +const ContactUserNumber = thinky.createModel( + "contact_user_number", + type + .object() + .schema({ + id: type.string(), + organization_id: requiredString(), + contact_number: requiredString(), + user_number: requiredString() + }) + .allowExtra(false) +); + +export default ContactUserNumber; diff --git a/src/server/models/index.js b/src/server/models/index.js index 24f061b75..ddc38f97a 100644 --- a/src/server/models/index.js +++ b/src/server/models/index.js @@ -21,6 +21,7 @@ import ZipCode from "./zip-code"; import Log from "./log"; import Tag from "./tag"; import TagCampaignContact from "./tag-campaign-contact"; +import ContactUserNumber from "./contact-user-number"; import thinky from "./thinky"; import datawarehouse from "./datawarehouse"; @@ -51,6 +52,7 @@ const tableList = [ // the rest are alphabetical "campaign_contact", // ?good candidate (or by cell) "canned_response", // good candidate + "contact_user_number", "interaction_step", "invite", "job_request", @@ -134,6 +136,7 @@ export { Campaign, CampaignAdmin, CampaignContact, + ContactUserNumber, InteractionStep, Invite, JobRequest, diff --git a/src/server/models/message.js b/src/server/models/message.js index 915f1ccd8..6d7e10c2f 100644 --- a/src/server/models/message.js +++ b/src/server/models/message.js @@ -68,4 +68,9 @@ Message.ensureIndex("cell_messageservice_sid_idx", doc => [ doc("messageservice_sid") ]); +Message.ensureIndex("cell_user_number_idx", doc => [ + doc("contact_number"), + doc("user_number") +]); + export default Message; diff --git a/src/workers/jobs.js b/src/workers/jobs.js index 6df68224b..2b08acab0 100644 --- a/src/workers/jobs.js +++ b/src/workers/jobs.js @@ -959,7 +959,8 @@ export async function handleIncomingMessageParts() { .count(); const lastMessage = await getLastMessage({ contactNumber: part.contact_number, - service: serviceKey + service: serviceKey, + userNumber: part.user_number }); const duplicateMessageToSaveExists = !!messagesToSave.find( message => message.service_id === serviceMessageId From de6e54a0dc41fe276da5106833cb46413c7c26c4 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Thu, 20 Aug 2020 16:01:34 -0500 Subject: [PATCH 002/191] Fix not null --- .../models/cacheable_queries/campaign-contact.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/server/models/cacheable_queries/campaign-contact.js b/src/server/models/cacheable_queries/campaign-contact.js index f07ab2e0c..1b1a431aa 100644 --- a/src/server/models/cacheable_queries/campaign-contact.js +++ b/src/server/models/cacheable_queries/campaign-contact.js @@ -451,12 +451,13 @@ const campaignContactCache = { } }, updateStatus: async (contact, newStatus, messageServiceOrUserNumber) => { - // console.log('updateSTATUS', newStatus, contact) try { - await r - .knex("campaign_contact") - .where("id", contact.id) - .update({ message_status: newStatus, updated_at: new Date() }); + if (newStatus) { + await r + .knex("campaign_contact") + .where("id", contact.id) + .update({ message_status: newStatus, updated_at: new Date() }); + } if (r.redis && CONTACT_CACHE_ENABLED) { const contactKey = cacheKey(contact.id); From bb85576183bb6be27331f16d7422fa61f5506191 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Thu, 20 Aug 2020 16:41:54 -0500 Subject: [PATCH 003/191] Fixes --- src/server/api/lib/twilio.js | 2 +- .../cacheable_queries/campaign-contact.js | 12 +++++------- .../cacheable_queries/contact-user-number.js | 17 +++++++++++------ 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/server/api/lib/twilio.js b/src/server/api/lib/twilio.js index d1f906b32..add642fd5 100644 --- a/src/server/api/lib/twilio.js +++ b/src/server/api/lib/twilio.js @@ -381,7 +381,7 @@ export function postMessageSend( updateQuery.update(changesToSave), cacheableData.campaignContact.updateStatus( contact, - null, + undefined, changesToSave.messageservice_sid || changesToSave.user_number ) ]) diff --git a/src/server/models/cacheable_queries/campaign-contact.js b/src/server/models/cacheable_queries/campaign-contact.js index 1b1a431aa..ced6a5b52 100644 --- a/src/server/models/cacheable_queries/campaign-contact.js +++ b/src/server/models/cacheable_queries/campaign-contact.js @@ -452,19 +452,17 @@ const campaignContactCache = { }, updateStatus: async (contact, newStatus, messageServiceOrUserNumber) => { try { - if (newStatus) { - await r - .knex("campaign_contact") - .where("id", contact.id) - .update({ message_status: newStatus, updated_at: new Date() }); - } + await r + .knex("campaign_contact") + .where("id", contact.id) + .update({ message_status: newStatus, updated_at: new Date() }); if (r.redis && CONTACT_CACHE_ENABLED) { const contactKey = cacheKey(contact.id); const statusKey = messageStatusKey(contact.id); let redisQuery = r.redis - .mulit() + .multi() // delay expiration for contacts we continue to update .expire(contactKey, 43200) .expire(statusKey, 43200); diff --git a/src/server/models/cacheable_queries/contact-user-number.js b/src/server/models/cacheable_queries/contact-user-number.js index 4b6157790..75b846acd 100644 --- a/src/server/models/cacheable_queries/contact-user-number.js +++ b/src/server/models/cacheable_queries/contact-user-number.js @@ -9,7 +9,7 @@ import { r } from "../../models"; const getCacheKey = (organizationId, contactNumber) => `${process.env.CACHE_PREFIX || - ""}-contact-user-number-${organizationId}-${contactNumber}`; + ""}contact-user-number-${organizationId}-${contactNumber}`; const contactUserNumberCache = { query: async ({ organizationId, contactNumber }) => { @@ -23,11 +23,16 @@ const contactUserNumberCache = { } } - const [contactUserNumber] = await r - .knex("contact_user_number") - .where({ organization_id: organizationId, contact_number: contactNumber }) - .select("organization_id", "contact_number", "user_number") - .limit(1); + const contactUserNumber = await r + .table("contact_user_number") + .filter({ + organization_id: organizationId, + contact_number: contactNumber + }) + .limit(1)(0) + .default(null); + + console.log(contactUserNumber); if (r.redis) { await r.redis From 5662dbdff783b058dbfe3acefb777af13285a439 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Thu, 20 Aug 2020 17:04:09 -0500 Subject: [PATCH 004/191] Multi line responses --- src/server/models/cacheable_queries/message.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/models/cacheable_queries/message.js b/src/server/models/cacheable_queries/message.js index dd5f65739..4d8f2a7ff 100644 --- a/src/server/models/cacheable_queries/message.js +++ b/src/server/models/cacheable_queries/message.js @@ -206,12 +206,12 @@ const deliveryReport = async ({ } } - const message = await r + const [message] = await r .knex("message") .where("service_id", messageSid) .limit(1) - .update(changes) - .returning("*"); + .returning("*") + .update(changes); if (process.env.EXPERIMENTAL_STICKY_SENDER && newStatus === "DELIVERED") { const campaignContact = await campaignContactCache.load( From 548ceb150b34e0b3e98196709ccd8c77674d569c Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Thu, 20 Aug 2020 18:08:32 -0500 Subject: [PATCH 005/191] Cleanup --- .../20200820144200_contact_user_number.js | 20 ++++++++++--------- src/server/api/lib/fakeservice.js | 3 ++- src/server/api/lib/nexmo.js | 3 ++- src/server/api/lib/twilio.js | 5 ----- .../cacheable_queries/campaign-contact.js | 4 ++-- .../models/cacheable_queries/message.js | 1 + 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/migrations/20200820144200_contact_user_number.js b/migrations/20200820144200_contact_user_number.js index 3bb4376a5..160109513 100644 --- a/migrations/20200820144200_contact_user_number.js +++ b/migrations/20200820144200_contact_user_number.js @@ -1,6 +1,10 @@ exports.up = async function up(knex, Promise) { + await knex.schema.alterTable("message", t => { + t.index(["contact_number", "user_number"], "cell_user_number_idx"); + }); + if (!(await knex.schema.hasTable("contact_user_number"))) { - return knex.schema.createTable("contact_user_number", t => { + await knex.schema.createTable("contact_user_number", t => { t.increments("id"); t.text("organization_id"); t.text("contact_number"); @@ -12,16 +16,14 @@ exports.up = async function up(knex, Promise) { ); t.unique(["organization_id", "contact_number"]); - - knex - .table("message") - .index(["contact_number", "user_number"], "cell_user_number_idx"); }); } - - return Promise.resolve(); }; -exports.down = function down(knex, Promise) { - return knex.schema.dropTableIfExists("contact_user_number"); +exports.down = async function down(knex, Promise) { + await knex.schema.alterTable("message", t => { + t.dropIndex("cell_user_number_idx"); + }); + + await knex.schema.dropTableIfExists("contact_user_number"); }; diff --git a/src/server/api/lib/fakeservice.js b/src/server/api/lib/fakeservice.js index 809288c9a..ede13f633 100644 --- a/src/server/api/lib/fakeservice.js +++ b/src/server/api/lib/fakeservice.js @@ -67,7 +67,8 @@ async function convertMessagePartsToMessage(messageParts) { const lastMessage = await getLastMessage({ contactNumber, service: "fakeservice", - messageServiceSid: "fakeservice" + messageServiceSid: "fakeservice", + userNumber }); const service_id = diff --git a/src/server/api/lib/nexmo.js b/src/server/api/lib/nexmo.js index 23fa26f91..0405372a1 100644 --- a/src/server/api/lib/nexmo.js +++ b/src/server/api/lib/nexmo.js @@ -36,7 +36,8 @@ async function convertMessagePartsToMessage(messageParts) { contactNumber, service: "nexmo", // Nexmo has nothing better that is both from sent and received message repsonses: - messageServiceSid: "nexmo" + messageServiceSid: "nexmo", + userNumber }); return new Message({ diff --git a/src/server/api/lib/twilio.js b/src/server/api/lib/twilio.js index add642fd5..9e14519c3 100644 --- a/src/server/api/lib/twilio.js +++ b/src/server/api/lib/twilio.js @@ -191,12 +191,10 @@ async function sendMessage(message, contact, trx, organization, campaign) { let userNumber; if (process.env.EXPERIMENTAL_STICKY_SENDER) { - console.log("getting user number", organization.id, contact.cell); const contactUserNumber = await getContactUserNumber( organization.id, contact.cell ); - console.log("got user number", userNumber); if (contactUserNumber) { userNumber = contactUserNumber.user_number; @@ -563,12 +561,9 @@ async function addNumberToMessagingService( * Buy a phone number and add it to the owned_phone_number table */ async function buyNumber(organization, twilioInstance, phoneNumber, opts = {}) { - const twilioBaseUrl = getConfig("TWILIO_BASE_CALLBACK_URL", organization); - const response = await twilioInstance.incomingPhoneNumbers.create({ phoneNumber, friendlyName: `Managed by Spoke [${process.env.BASE_URL}]: ${phoneNumber}`, - smsUrl: urlJoin(twilioBaseUrl, "twilio", organization.id.toString()), voiceUrl: getConfig("TWILIO_VOICE_URL", organization) // will use default twilio recording if undefined }); diff --git a/src/server/models/cacheable_queries/campaign-contact.js b/src/server/models/cacheable_queries/campaign-contact.js index ced6a5b52..3518f6ad7 100644 --- a/src/server/models/cacheable_queries/campaign-contact.js +++ b/src/server/models/cacheable_queries/campaign-contact.js @@ -374,7 +374,7 @@ const campaignContactCache = { } } - const fromFilter = messageServiceSid + const assignmentFilter = messageServiceSid ? { messageservice_sid: messageServiceSid } : { user_number: userNumber }; let messageQuery = r @@ -387,7 +387,7 @@ const campaignContactCache = { contact_number: cell, service }, - fromFilter + assignmentFilter ) ) .orderBy("message.created_at", "desc") diff --git a/src/server/models/cacheable_queries/message.js b/src/server/models/cacheable_queries/message.js index 4d8f2a7ff..35cf388ce 100644 --- a/src/server/models/cacheable_queries/message.js +++ b/src/server/models/cacheable_queries/message.js @@ -214,6 +214,7 @@ const deliveryReport = async ({ .update(changes); if (process.env.EXPERIMENTAL_STICKY_SENDER && newStatus === "DELIVERED") { + // Assign user number to contact/organization const campaignContact = await campaignContactCache.load( message.campaign_contact_id ); From d14bd753f49e0bdaa980baf89d469b8640ad7e45 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Thu, 20 Aug 2020 20:39:00 -0500 Subject: [PATCH 006/191] Updating variables --- src/server/models/cacheable_queries/campaign-contact.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/server/models/cacheable_queries/campaign-contact.js b/src/server/models/cacheable_queries/campaign-contact.js index 3518f6ad7..fcecf000d 100644 --- a/src/server/models/cacheable_queries/campaign-contact.js +++ b/src/server/models/cacheable_queries/campaign-contact.js @@ -35,8 +35,9 @@ const messageStatusKey = id => `${process.env.CACHE_PREFIX || ""}contactstatus-${id}`; // allows a lookup of contact_id, assignment_id, and timezone_offset by cell+messageservice_sid -const cellTargetKey = (cell, messageServiceSid) => - `${process.env.CACHE_PREFIX || ""}cell-${cell}-${messageServiceSid || "x"}`; +const cellTargetKey = (cell, messageServiceOrUserNumber) => + `${process.env.CACHE_PREFIX || ""}cell-${cell}-${messageServiceOrUserNumber || + "x"}`; // HASH assignment_id and user_id (sometimes) of assignment // This allows us to clear assignment cache all at once for a campaign From 6f3aadc43bac00486da8264ad76b1fe7dacc8bb3 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Fri, 21 Aug 2020 12:24:12 -0500 Subject: [PATCH 007/191] Phone Inventory config parity --- src/server/api/organization.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/server/api/organization.js b/src/server/api/organization.js index 1c77b9a49..28a481c03 100644 --- a/src/server/api/organization.js +++ b/src/server/api/organization.js @@ -337,6 +337,9 @@ export const resolvers = { if ( !getConfig("EXPERIMENTAL_PHONE_INVENTORY", organization, { truthy: true + }) && + !getConfig("PHONE_INVENTORY", organization, { + truthy: true }) ) { return []; From 37f3dc34defd84df1d92caa89006e854675f981d Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Fri, 21 Aug 2020 17:38:13 -0500 Subject: [PATCH 008/191] Cleaning up console log --- src/server/models/cacheable_queries/contact-user-number.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/server/models/cacheable_queries/contact-user-number.js b/src/server/models/cacheable_queries/contact-user-number.js index 75b846acd..25e5b3acf 100644 --- a/src/server/models/cacheable_queries/contact-user-number.js +++ b/src/server/models/cacheable_queries/contact-user-number.js @@ -32,8 +32,6 @@ const contactUserNumberCache = { .limit(1)(0) .default(null); - console.log(contactUserNumber); - if (r.redis) { await r.redis .multi() From ef8486b29845570647200e6252aaa7c0cad3f3f7 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Fri, 21 Aug 2020 17:44:21 -0500 Subject: [PATCH 009/191] EXPERIMENTAL_STICKY_SENDER documentation --- docs/REFERENCE-environment_variables.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/REFERENCE-environment_variables.md b/docs/REFERENCE-environment_variables.md index 7707ac99c..c4257ba45 100644 --- a/docs/REFERENCE-environment_variables.md +++ b/docs/REFERENCE-environment_variables.md @@ -40,6 +40,7 @@ | EMAIL_HOST_SECURE | Email server security -- set to `1` if connection is made with smtps, otherwise set to the _default_ empty string to connect without TLS. | | EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE | Allow campaigns to have their own messaging service. This allows Spoke to send more than default Twilio limits of 400 numbers or 80,000 texts per day | | EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS | Allows campaigns to select numbers from the inventory by area code. Currently only implemented for Twilio and fakeservice. When using Twilio, this creates a messaging service for each campaign on the fly. Requires PHONE_INVENTORY to be turned on. | +| EXPERIMENTAL_STICKY_SENDER | Creates permanent mappings between contact numbers and user numbers per organization when a recipient is texted. Once the mapping exists, that contact number will always be texted by that user number. This also means that messaging services will be skipped in favor of the direct user number. | | PHONE_INVENTORY | Enable the Twilio phone number inventory feature while it's under active development | | FIX_ORGLESS | Set to any truthy value only if you want to run the job that automatically assigns the default org (see DEFAULT_ORG) to new users who have no assigned org. | | GRAPHQL_URL | Optional URL for pointing GraphQL API requests. Should end with `/graphql`, e.g. `https://example.org/graphql`. _Default_: "/graphql" | From 4dcf0a0f871f7830b666cb8e5c5200ca69b3854d Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Fri, 21 Aug 2020 21:27:46 -0500 Subject: [PATCH 010/191] wrong table name --- src/server/models/cacheable_queries/contact-user-number.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/models/cacheable_queries/contact-user-number.js b/src/server/models/cacheable_queries/contact-user-number.js index 25e5b3acf..b72dc4f49 100644 --- a/src/server/models/cacheable_queries/contact-user-number.js +++ b/src/server/models/cacheable_queries/contact-user-number.js @@ -49,7 +49,7 @@ const contactUserNumberCache = { user_number: userNumber }; - await r.knex("contact_phone_number").insert(contactUserNumber); + await r.knex("contact_user_number").insert(contactUserNumber); if (r.redis) { const cacheKey = getCacheKey(organizationId, contactNumber); From f02ec404380658003e2717239f2ea9f4a442f88f Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Thu, 21 Jan 2021 16:46:25 -0600 Subject: [PATCH 011/191] Contact User Number to Organization Contact --- ...> 20200820144200_organization_contacts.js} | 8 ++--- src/server/api/lib/twilio.js | 36 +++++++++++++++---- src/server/models/cacheable_queries/index.js | 4 +-- ...user-number.js => organization-contact.js} | 28 +++++++-------- src/server/models/index.js | 6 ++-- ...user-number.js => organization-contact.js} | 6 ++-- 6 files changed, 55 insertions(+), 33 deletions(-) rename migrations/{20200820144200_contact_user_number.js => 20200820144200_organization_contacts.js} (70%) rename src/server/models/cacheable_queries/{contact-user-number.js => organization-contact.js} (65%) rename src/server/models/{contact-user-number.js => organization-contact.js} (78%) diff --git a/migrations/20200820144200_contact_user_number.js b/migrations/20200820144200_organization_contacts.js similarity index 70% rename from migrations/20200820144200_contact_user_number.js rename to migrations/20200820144200_organization_contacts.js index 160109513..57a76dd7d 100644 --- a/migrations/20200820144200_contact_user_number.js +++ b/migrations/20200820144200_organization_contacts.js @@ -3,8 +3,8 @@ exports.up = async function up(knex, Promise) { t.index(["contact_number", "user_number"], "cell_user_number_idx"); }); - if (!(await knex.schema.hasTable("contact_user_number"))) { - await knex.schema.createTable("contact_user_number", t => { + if (!(await knex.schema.hasTable("organization_contact"))) { + await knex.schema.createTable("organization_contact", t => { t.increments("id"); t.text("organization_id"); t.text("contact_number"); @@ -12,7 +12,7 @@ exports.up = async function up(knex, Promise) { t.index( ["organization_id", "contact_number"], - "contact_user_number_organization_contact_number" + "organization_contact_organization_contact_number" ); t.unique(["organization_id", "contact_number"]); @@ -25,5 +25,5 @@ exports.down = async function down(knex, Promise) { t.dropIndex("cell_user_number_idx"); }); - await knex.schema.dropTableIfExists("contact_user_number"); + await knex.schema.dropTableIfExists("organization_contact"); }; diff --git a/src/server/api/lib/twilio.js b/src/server/api/lib/twilio.js index e748e1c00..b2cf5bf87 100644 --- a/src/server/api/lib/twilio.js +++ b/src/server/api/lib/twilio.js @@ -217,11 +217,33 @@ async function getMessagingServiceSid( ); } -async function getContactUserNumber(organizationId, contactNumber) { - return await cacheableData.contactUserNumber.query({ - organizationId, +async function getOrganizationContact(organization, contactNumber) { + const organizationContact = await cacheableData.organizationContact.query({ + organizationId: organization.id, contactNumber }); + + if (organizationContact && organizationContact.user_number) { + return organizationContact.user_number; + } + + if ( + (getConfig("EXPERIMENTAL_PHONE_INVENTORY", organization, { + truthy: true + }) || + getConfig("PHONE_INVENTORY", organization, { truthy: true })) && + getConfig("SKIP_TWILIO_MESSAGING_SERVICE", organization, { truthy: true }) + ) { + const phoneNumber = await ownedPhoneNumber.getOwnedPhoneNumberForStickySender( + organization.id, + contactNumber + ); + + console.log({ phoneNumber }); + return phoneNumber && phoneNumber.phone_number; + } + + return null; } async function sendMessage(message, contact, trx, organization, campaign) { @@ -255,13 +277,13 @@ async function sendMessage(message, contact, trx, organization, campaign) { let userNumber; if (process.env.EXPERIMENTAL_STICKY_SENDER) { - const contactUserNumber = await getContactUserNumber( - organization.id, + const organizationContact = await getOrganizationContact( + organization, contact.cell ); - if (contactUserNumber) { - userNumber = contactUserNumber.user_number; + if (organizationContact) { + userNumber = organizationContact.user_number; } } diff --git a/src/server/models/cacheable_queries/index.js b/src/server/models/cacheable_queries/index.js index 964babfad..5a6ce98b6 100644 --- a/src/server/models/cacheable_queries/index.js +++ b/src/server/models/cacheable_queries/index.js @@ -2,7 +2,7 @@ import assignment from "./assignment"; import campaign from "./campaign"; import campaignContact from "./campaign-contact"; import cannedResponse from "./canned-response"; -import contactUserNumber from "./contact-user-number"; +import organizationContact from "./organization-contact"; import message from "./message"; import optOut from "./opt-out"; import organization from "./organization"; @@ -15,7 +15,7 @@ const cacheableData = { campaign, campaignContact, cannedResponse, - contactUserNumber, + organizationContact, message, optOut, organization, diff --git a/src/server/models/cacheable_queries/contact-user-number.js b/src/server/models/cacheable_queries/organization-contact.js similarity index 65% rename from src/server/models/cacheable_queries/contact-user-number.js rename to src/server/models/cacheable_queries/organization-contact.js index b72dc4f49..2b2583de3 100644 --- a/src/server/models/cacheable_queries/contact-user-number.js +++ b/src/server/models/cacheable_queries/organization-contact.js @@ -1,4 +1,4 @@ -import { r } from "../../models"; +import { r } from ".."; // Datastructure: // * regular GET/SET with JSON ordered list of the objects {id,title,text} @@ -9,22 +9,22 @@ import { r } from "../../models"; const getCacheKey = (organizationId, contactNumber) => `${process.env.CACHE_PREFIX || - ""}contact-user-number-${organizationId}-${contactNumber}`; + ""}organization-contact-${organizationId}-${contactNumber}`; -const contactUserNumberCache = { +const organizationContactCache = { query: async ({ organizationId, contactNumber }) => { const cacheKey = getCacheKey(organizationId, contactNumber); if (r.redis) { - const contactUserNumber = await r.redis.getAsync(cacheKey); + const organizationContact = await r.redis.getAsync(cacheKey); - if (contactUserNumber) { - return JSON.parse(contactUserNumber); + if (organizationContact) { + return JSON.parse(organizationContact); } } - const contactUserNumber = await r - .table("contact_user_number") + const organizationContact = await r + .table("organization_contact") .filter({ organization_id: organizationId, contact_number: contactNumber @@ -35,28 +35,28 @@ const contactUserNumberCache = { if (r.redis) { await r.redis .multi() - .set(cacheKey, JSON.stringify(contactUserNumber)) + .set(cacheKey, JSON.stringify(organizationContact)) .expire(cacheKey, 43200) // 12 hours .execAsync(); } - return contactUserNumber; + return organizationContact; }, save: async ({ organizationId, contactNumber, userNumber }) => { - const contactUserNumber = { + const organizationContact = { organization_id: organizationId, contact_number: contactNumber, user_number: userNumber }; - await r.knex("contact_user_number").insert(contactUserNumber); + await r.knex("organization_contact").insert(organizationContact); if (r.redis) { const cacheKey = getCacheKey(organizationId, contactNumber); await r.redis .multi() - .set(cacheKey, JSON.stringify(contactUserNumber)) + .set(cacheKey, JSON.stringify(organizationContact)) .expire(cacheKey, 43200) // 12 hours .execAsync(); } @@ -65,4 +65,4 @@ const contactUserNumberCache = { } }; -export default contactUserNumberCache; +export default organizationContactCache; diff --git a/src/server/models/index.js b/src/server/models/index.js index 2cac1ec32..11bc9ddb5 100644 --- a/src/server/models/index.js +++ b/src/server/models/index.js @@ -21,7 +21,7 @@ import ZipCode from "./zip-code"; import Log from "./log"; import Tag from "./tag"; import TagCampaignContact from "./tag-campaign-contact"; -import ContactUserNumber from "./contact-user-number"; +import OrganizationContact from "./organization-contact"; import thinky from "./thinky"; import datawarehouse from "./datawarehouse"; @@ -55,7 +55,7 @@ const tableList = [ // the rest are alphabetical "campaign_contact", // ?good candidate (or by cell) "canned_response", // good candidate - "contact_user_number", + "organization_contact", "interaction_step", "invite", "job_request", @@ -155,7 +155,7 @@ export { Campaign, CampaignAdmin, CampaignContact, - ContactUserNumber, + OrganizationContact, InteractionStep, Invite, JobRequest, diff --git a/src/server/models/contact-user-number.js b/src/server/models/organization-contact.js similarity index 78% rename from src/server/models/contact-user-number.js rename to src/server/models/organization-contact.js index 2d1f622c6..48fc3fb21 100644 --- a/src/server/models/contact-user-number.js +++ b/src/server/models/organization-contact.js @@ -3,8 +3,8 @@ const type = thinky.type; import { requiredString } from "./custom-types"; // For documentation purposes only. Use knex queries instead of this model. -const ContactUserNumber = thinky.createModel( - "contact_user_number", +const OrganizationContact = thinky.createModel( + "organization_contact", type .object() .schema({ @@ -16,4 +16,4 @@ const ContactUserNumber = thinky.createModel( .allowExtra(false) ); -export default ContactUserNumber; +export default OrganizationContact; From b600fef7caa0bbf873d440e36a9ef26fa4563a5e Mon Sep 17 00:00:00 2001 From: Larry Person Date: Fri, 22 Jan 2021 13:12:00 -0500 Subject: [PATCH 012/191] move message-service-related files to extensions --- .../message-handlers/profanity-tagger.test.js | 2 +- __test__/lib.test.js | 4 ++-- __test__/server/api/campaign/campaign.test.js | 3 +-- __test__/server/api/lib/twilio.test.js | 4 ++-- docs/HOWTO-scale-spoke-plan.md | 2 +- src/extensions/message-handlers/auto-optout/index.js | 3 +-- .../message-handlers/initaltext-guard/index.js | 4 +--- src/extensions/message-handlers/to-ascii/index.js | 4 +--- .../messaging_services}/__mocks__/twilio.js | 0 .../messaging_services}/fakeservice.js | 9 +++++++-- .../messaging_services}/message-sending.js | 2 +- .../api/lib => extensions/messaging_services}/nexmo.js | 8 ++++---- .../lib => extensions/messaging_services}/services.js | 0 .../lib => extensions/messaging_services}/twilio.js | 10 +++++----- src/server/api/campaign.js | 5 +++-- src/server/api/mutations/buyPhoneNumbers.js | 2 +- src/server/api/mutations/releaseCampaignNumbers.js | 2 +- src/server/api/mutations/startCampaign.js | 2 +- src/server/api/schema.js | 3 +-- src/server/index.js | 6 +++--- src/workers/jobs.js | 6 +++--- src/workers/tasks.js | 2 +- 22 files changed, 41 insertions(+), 42 deletions(-) rename src/{server/api/lib => extensions/messaging_services}/__mocks__/twilio.js (100%) rename src/{server/api/lib => extensions/messaging_services}/fakeservice.js (97%) rename src/{server/api/lib => extensions/messaging_services}/message-sending.js (87%) rename src/{server/api/lib => extensions/messaging_services}/nexmo.js (97%) rename src/{server/api/lib => extensions/messaging_services}/services.js (100%) rename src/{server/api/lib => extensions/messaging_services}/twilio.js (99%) diff --git a/__test__/extensions/message-handlers/profanity-tagger.test.js b/__test__/extensions/message-handlers/profanity-tagger.test.js index cbb67aefa..ad3215958 100644 --- a/__test__/extensions/message-handlers/profanity-tagger.test.js +++ b/__test__/extensions/message-handlers/profanity-tagger.test.js @@ -1,5 +1,5 @@ import { r, cacheableData } from "../../../src/server/models"; -import serviceMap from "../../../src/server/api/lib/services"; +import serviceMap from "../../../src/extensions/messaging_services/services"; import { available, DEFAULT_PROFANITY_REGEX_BASE64 diff --git a/__test__/lib.test.js b/__test__/lib.test.js index a526319ec..174f00867 100644 --- a/__test__/lib.test.js +++ b/__test__/lib.test.js @@ -1,12 +1,12 @@ import { resolvers } from "../src/server/api/schema"; import { schema } from "../src/api/schema"; -import twilio from "../src/server/api/lib/twilio"; +import twilio from "../src/extensions/messaging_services/twilio"; import { getConfig, hasConfig } from "../src/server/api/lib/config"; import { makeExecutableSchema } from "graphql-tools"; const mySchema = makeExecutableSchema({ typeDefs: schema, - resolvers: resolvers, + resolvers, allowUndefinedInResolve: true }); diff --git a/__test__/server/api/campaign/campaign.test.js b/__test__/server/api/campaign/campaign.test.js index f67407a52..7b73ac8df 100644 --- a/__test__/server/api/campaign/campaign.test.js +++ b/__test__/server/api/campaign/campaign.test.js @@ -15,7 +15,6 @@ import { } from "../../../../src/containers/AdminIncomingMessageList"; import { makeTree } from "../../../../src/lib"; -import twilio from "../../../../src/server/api/lib/twilio"; import { setupTest, @@ -39,7 +38,7 @@ import { sleep } from "../../../test_helpers"; -jest.mock("../../../../src/server/api/lib/twilio"); +jest.mock("../../../../src/extensions/messaging_services/twilio"); let testAdminUser; let testInvite; diff --git a/__test__/server/api/lib/twilio.test.js b/__test__/server/api/lib/twilio.test.js index eafc13781..7647f50ff 100644 --- a/__test__/server/api/lib/twilio.test.js +++ b/__test__/server/api/lib/twilio.test.js @@ -4,8 +4,8 @@ import { getConfig } from "../../../../src/server/api/lib/config"; import twilio, { postMessageSend, handleDeliveryReport -} from "../../../../src/server/api/lib/twilio"; -import { getLastMessage } from "../../../../src/server/api/lib/message-sending"; +} from "../../../../src/extensions/messaging_services/twilio"; +import { getLastMessage } from "../../../../src/extensions/messaging_services/message-sending"; import { erroredMessageSender } from "../../../../src/workers/job-processes"; import { setupTest, diff --git a/docs/HOWTO-scale-spoke-plan.md b/docs/HOWTO-scale-spoke-plan.md index 7d5855516..c43492ada 100644 --- a/docs/HOWTO-scale-spoke-plan.md +++ b/docs/HOWTO-scale-spoke-plan.md @@ -155,7 +155,7 @@ Here is the (proposed) structure of data in Redis to support the above data need 4. HSET `replies-` (using lookup) * Code points: - * [twilio backend codepoint](https://github.com/MoveOnOrg/Spoke/blob/main/src/server/api/lib/twilio.js#L203) + * [twilio backend codepoint](https://github.com/MoveOnOrg/Spoke/blob/main/src/server/extensions/messaging_services/twilio.js#L203) * note that is called both from server/index.js and workers/jobs.js * In theory we can/should do this generically over services, but pending_message_part complicates this a bit much. A 'middle road' approach would also implement this in server/api/lib/fakeservice.js diff --git a/src/extensions/message-handlers/auto-optout/index.js b/src/extensions/message-handlers/auto-optout/index.js index 3a4cd5a14..53ea956b2 100644 --- a/src/extensions/message-handlers/auto-optout/index.js +++ b/src/extensions/message-handlers/auto-optout/index.js @@ -1,6 +1,5 @@ import { getConfig } from "../../../server/api/lib/config"; -import { cacheableData, Message } from "../../../server/models"; -import serviceMap from "../../../server/api/lib/services"; +import { cacheableData } from "../../../server/models"; const DEFAULT_AUTO_OPTOUT_REGEX_LIST_BASE64 = "W3sicmVnZXgiOiAiXlxccypzdG9wXFxifFxcYnJlbW92ZSBtZVxccyokfHJlbW92ZSBteSBuYW1lfFxcYnRha2UgbWUgb2ZmIHRoXFx3KyBsaXN0fFxcYmxvc2UgbXkgbnVtYmVyfGRvblxcVz90IGNvbnRhY3QgbWV8ZGVsZXRlIG15IG51bWJlcnxJIG9wdCBvdXR8c3RvcDJxdWl0fHN0b3BhbGx8Xlxccyp1bnN1YnNjcmliZVxccyokfF5cXHMqY2FuY2VsXFxzKiR8XlxccyplbmRcXHMqJHxeXFxzKnF1aXRcXHMqJCIsICJyZWFzb24iOiAic3RvcCJ9XQ=="; diff --git a/src/extensions/message-handlers/initaltext-guard/index.js b/src/extensions/message-handlers/initaltext-guard/index.js index 0589fbfb5..996d46846 100644 --- a/src/extensions/message-handlers/initaltext-guard/index.js +++ b/src/extensions/message-handlers/initaltext-guard/index.js @@ -1,6 +1,4 @@ -import { getConfig } from "../../../server/api/lib/config"; -import { r, cacheableData, Message } from "../../../server/models"; -import serviceMap from "../../../server/api/lib/services"; +import { r } from "../../../server/models"; export const serverAdministratorInstructions = () => { return { diff --git a/src/extensions/message-handlers/to-ascii/index.js b/src/extensions/message-handlers/to-ascii/index.js index 71fe6bd05..9b2c4f817 100644 --- a/src/extensions/message-handlers/to-ascii/index.js +++ b/src/extensions/message-handlers/to-ascii/index.js @@ -1,6 +1,4 @@ -import { getConfig } from "../../../server/api/lib/config"; -import { r, cacheableData, Message } from "../../../server/models"; -import serviceMap from "../../../server/api/lib/services"; +import { r } from "../../../server/models"; export const serverAdministratorInstructions = () => { return { diff --git a/src/server/api/lib/__mocks__/twilio.js b/src/extensions/messaging_services/__mocks__/twilio.js similarity index 100% rename from src/server/api/lib/__mocks__/twilio.js rename to src/extensions/messaging_services/__mocks__/twilio.js diff --git a/src/server/api/lib/fakeservice.js b/src/extensions/messaging_services/fakeservice.js similarity index 97% rename from src/server/api/lib/fakeservice.js rename to src/extensions/messaging_services/fakeservice.js index 3a321c9b4..27cb4118c 100644 --- a/src/server/api/lib/fakeservice.js +++ b/src/extensions/messaging_services/fakeservice.js @@ -1,5 +1,10 @@ import { getLastMessage } from "./message-sending"; -import { Message, PendingMessagePart, r, cacheableData } from "../../models"; +import { + Message, + PendingMessagePart, + r, + cacheableData +} from "../../server/models"; import uuid from "uuid"; // This 'fakeservice' allows for fake-sending messages @@ -48,7 +53,7 @@ async function sendMessage(message, contact, trx, organization, campaign) { ] : null; await cacheableData.message.save({ - contact: contact, + contact, messageInstance: new Message({ ...message, ...changes, diff --git a/src/server/api/lib/message-sending.js b/src/extensions/messaging_services/message-sending.js similarity index 87% rename from src/server/api/lib/message-sending.js rename to src/extensions/messaging_services/message-sending.js index 71befbb0f..9b87a7314 100644 --- a/src/server/api/lib/message-sending.js +++ b/src/extensions/messaging_services/message-sending.js @@ -1,4 +1,4 @@ -import { r, cacheableData } from "../../models"; +import { cacheableData } from "../../server/models"; export async function getLastMessage({ contactNumber, diff --git a/src/server/api/lib/nexmo.js b/src/extensions/messaging_services/nexmo.js similarity index 97% rename from src/server/api/lib/nexmo.js rename to src/extensions/messaging_services/nexmo.js index 23fa26f91..7a0bdae2a 100644 --- a/src/server/api/lib/nexmo.js +++ b/src/extensions/messaging_services/nexmo.js @@ -1,8 +1,8 @@ import Nexmo from "nexmo"; -import { getFormattedPhoneNumber } from "../../../lib/phone-format"; -import { Message, PendingMessagePart } from "../../models"; +import { getFormattedPhoneNumber } from "../../lib/phone-format"; +import { Message, PendingMessagePart } from "../../server/models"; import { getLastMessage } from "./message-sending"; -import { log } from "../../../lib"; +import { log } from "../../lib"; // NEXMO error_codes: // If status is a number, then it will be the number @@ -148,7 +148,7 @@ async function sendMessage(message, contact, trx, organization, campaign) { } messageToSave.service = "nexmo"; - //userNum is required so can be tracked as messageservice_sid + // userNum is required so can be tracked as messageservice_sid messageToSave.messageservice_sid = getFormattedPhoneNumber(userNumber); messageToSave.campaign_contact_id = contact.id; diff --git a/src/server/api/lib/services.js b/src/extensions/messaging_services/services.js similarity index 100% rename from src/server/api/lib/services.js rename to src/extensions/messaging_services/services.js diff --git a/src/server/api/lib/twilio.js b/src/extensions/messaging_services/twilio.js similarity index 99% rename from src/server/api/lib/twilio.js rename to src/extensions/messaging_services/twilio.js index d9455c986..5a05498a6 100644 --- a/src/server/api/lib/twilio.js +++ b/src/extensions/messaging_services/twilio.js @@ -2,17 +2,17 @@ import _ from "lodash"; import Twilio, { twiml } from "twilio"; import urlJoin from "url-join"; -import { log } from "../../../lib"; -import { getFormattedPhoneNumber } from "../../../lib/phone-format"; +import { log } from "../../lib"; +import { getFormattedPhoneNumber } from "../../lib/phone-format"; import { cacheableData, Log, Message, PendingMessagePart, r -} from "../../models"; -import wrap from "../../wrap"; -import { getConfig } from "./config"; +} from "../../server/models"; +import wrap from "../../server/wrap"; +import { getConfig } from "../../server/api/lib/config"; import { saveNewIncomingMessage } from "./message-sending"; // TWILIO error_codes: diff --git a/src/server/api/campaign.js b/src/server/api/campaign.js index 5e3d6918e..133b7b987 100644 --- a/src/server/api/campaign.js +++ b/src/server/api/campaign.js @@ -1,6 +1,8 @@ import { accessRequired } from "./errors"; import { mapFieldsToModel, mapFieldsOrNull } from "./lib/utils"; -import { errorDescriptions } from "./lib/twilio"; +import twilio, { + errorDescriptions +} from "../../extensions/messaging_services/twilio"; import { Campaign, JobRequest, r, cacheableData } from "../models"; import { getUsers } from "./user"; import { getSideboxChoices } from "./organization"; @@ -8,7 +10,6 @@ import { getAvailableIngestMethods, getMethodChoiceData } from "../../extensions/contact-loaders"; -import twilio from "./lib/twilio"; import { getConfig, getFeatures } from "./lib/config"; import ownedPhoneNumber from "./lib/owned-phone-number"; const title = 'lower("campaign"."title")'; diff --git a/src/server/api/mutations/buyPhoneNumbers.js b/src/server/api/mutations/buyPhoneNumbers.js index ed519e12d..2e7a63411 100644 --- a/src/server/api/mutations/buyPhoneNumbers.js +++ b/src/server/api/mutations/buyPhoneNumbers.js @@ -1,4 +1,4 @@ -import serviceMap from "../lib/services"; +import serviceMap from "../../../extensions/messaging_services/services"; import { accessRequired } from "../errors"; import { getConfig } from "../lib/config"; import { cacheableData } from "../../models"; diff --git a/src/server/api/mutations/releaseCampaignNumbers.js b/src/server/api/mutations/releaseCampaignNumbers.js index 9be5b8644..ecf53231f 100644 --- a/src/server/api/mutations/releaseCampaignNumbers.js +++ b/src/server/api/mutations/releaseCampaignNumbers.js @@ -2,7 +2,7 @@ import { r } from "../../models"; import cacheableData from "../../models/cacheable_queries"; import { accessRequired } from "../errors"; import { getConfig } from "../lib/config"; -import twilio from "../lib/twilio"; +import twilio from "../../../extensions/messaging_services/twilio"; import ownedPhoneNumber from "../lib/owned-phone-number"; export const releaseCampaignNumbers = async (_, { campaignId }, { user }) => { diff --git a/src/server/api/mutations/startCampaign.js b/src/server/api/mutations/startCampaign.js index 4ed74f3fa..a4bbb8593 100644 --- a/src/server/api/mutations/startCampaign.js +++ b/src/server/api/mutations/startCampaign.js @@ -2,7 +2,7 @@ import cacheableData from "../../models/cacheable_queries"; import { r } from "../../models"; import { accessRequired } from "../errors"; import { Notifications, sendUserNotification } from "../../notifications"; -import twilio from "../lib/twilio"; +import twilio from "../../../extensions/messaging_services/twilio"; import { getConfig } from "../lib/config"; import { jobRunner } from "../../../extensions/job-runners"; import { Tasks } from "../../../workers/tasks"; diff --git a/src/server/api/schema.js b/src/server/api/schema.js index 9500279d5..0096a691c 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -5,7 +5,6 @@ import isUrl from "is-url"; import _ from "lodash"; import { gzip, makeTree, getHighestRole } from "../../lib"; import { capitalizeWord, groupCannedResponses } from "./lib/utils"; -import twilio from "./lib/twilio"; import ownedPhoneNumber from "./lib/owned-phone-number"; import { getIngestMethod } from "../../extensions/contact-loaders"; @@ -38,7 +37,7 @@ import { } from "./errors"; import { resolvers as interactionStepResolvers } from "./interaction-step"; import { resolvers as inviteResolvers } from "./invite"; -import { saveNewIncomingMessage } from "./lib/message-sending"; +import { saveNewIncomingMessage } from "../../extensions/messaging_services/message-sending"; import { getConfig, getFeatures } from "./lib/config"; import { resolvers as messageResolvers } from "./message"; import { resolvers as optOutResolvers } from "./opt-out"; diff --git a/src/server/index.js b/src/server/index.js index d57bde1ff..605d85f4a 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -3,7 +3,7 @@ import bodyParser from "body-parser"; import express from "express"; import appRenderer from "./middleware/app-renderer"; import { graphqlExpress, graphiqlExpress } from "apollo-server-express"; -import { makeExecutableSchema, addMockFunctionsToSchema } from "graphql-tools"; +import { makeExecutableSchema } from "graphql-tools"; // ORDERING: ./models import must be imported above ./api to help circular imports import { createLoaders, createTablesIfNecessary, r } from "./models"; import { resolvers } from "./api/schema"; @@ -14,8 +14,8 @@ import passportSetup from "./auth-passport"; import wrap from "./wrap"; import { log } from "../lib"; import telemetry from "./telemetry"; -import nexmo from "./api/lib/nexmo"; -import twilio from "./api/lib/twilio"; +import nexmo from "../extensions/messaging_services/nexmo"; +import twilio from "../extensions/messaging_services/twilio"; import { getConfig } from "./api/lib/config"; import { seedZipCodes } from "./seeds/seed-zip-codes"; import { setupUserNotificationObservers } from "./notifications"; diff --git a/src/workers/jobs.js b/src/workers/jobs.js index 5a8ebba66..50f626ece 100644 --- a/src/workers/jobs.js +++ b/src/workers/jobs.js @@ -10,12 +10,12 @@ import { import telemetry from "../server/telemetry"; import { log, gunzip, zipToTimeZone, convertOffsetsToStrings } from "../lib"; import { sleep, updateJob } from "./lib"; -import serviceMap from "../server/api/lib/services"; -import twilio from "../server/api/lib/twilio"; +import serviceMap from "../extensions/messaging_services/services"; +import twilio from "../extensions/messaging_services/twilio"; import { getLastMessage, saveNewIncomingMessage -} from "../server/api/lib/message-sending"; +} from "../extensions/messaging_services/message-sending"; import importScriptFromDocument from "../server/api/lib/import-script"; import { rawIngestMethod } from "../extensions/contact-loaders"; diff --git a/src/workers/tasks.js b/src/workers/tasks.js index a02097b5f..da46e2ed1 100644 --- a/src/workers/tasks.js +++ b/src/workers/tasks.js @@ -1,7 +1,7 @@ // Tasks are lightweight, fire-and-forget functions run in the background. // Unlike Jobs, tasks are not tracked in the database. // See src/extensions/job-runners/README.md for more details -import serviceMap from "../server/api/lib/services"; +import serviceMap from "../extensions/messaging_services/services"; import * as ActionHandlers from "../extensions/action-handlers"; import { cacheableData } from "../server/models"; From 595a10fc3a4c4b0cb87f3feba6b7ece8ac776d4f Mon Sep 17 00:00:00 2001 From: Larry Person Date: Fri, 22 Jan 2021 13:12:00 -0500 Subject: [PATCH 013/191] organize imports --- src/extensions/messaging_services/twilio.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/messaging_services/twilio.js b/src/extensions/messaging_services/twilio.js index 5a05498a6..de5b6233e 100644 --- a/src/extensions/messaging_services/twilio.js +++ b/src/extensions/messaging_services/twilio.js @@ -4,6 +4,7 @@ import Twilio, { twiml } from "twilio"; import urlJoin from "url-join"; import { log } from "../../lib"; import { getFormattedPhoneNumber } from "../../lib/phone-format"; +import { getConfig } from "../../server/api/lib/config"; import { cacheableData, Log, @@ -12,7 +13,6 @@ import { r } from "../../server/models"; import wrap from "../../server/wrap"; -import { getConfig } from "../../server/api/lib/config"; import { saveNewIncomingMessage } from "./message-sending"; // TWILIO error_codes: From 7b3c70051bf845ef6c1ffa440817dbae26abc2b2 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Wed, 27 Jan 2021 16:02:40 -0600 Subject: [PATCH 014/191] Cleanup --- src/server/api/lib/twilio.js | 16 ---------------- .../cacheable_queries/organization-contact.js | 13 ++----------- src/server/models/organization-contact.js | 4 ++-- 3 files changed, 4 insertions(+), 29 deletions(-) diff --git a/src/server/api/lib/twilio.js b/src/server/api/lib/twilio.js index b2cf5bf87..7644d8b75 100644 --- a/src/server/api/lib/twilio.js +++ b/src/server/api/lib/twilio.js @@ -227,22 +227,6 @@ async function getOrganizationContact(organization, contactNumber) { return organizationContact.user_number; } - if ( - (getConfig("EXPERIMENTAL_PHONE_INVENTORY", organization, { - truthy: true - }) || - getConfig("PHONE_INVENTORY", organization, { truthy: true })) && - getConfig("SKIP_TWILIO_MESSAGING_SERVICE", organization, { truthy: true }) - ) { - const phoneNumber = await ownedPhoneNumber.getOwnedPhoneNumberForStickySender( - organization.id, - contactNumber - ); - - console.log({ phoneNumber }); - return phoneNumber && phoneNumber.phone_number; - } - return null; } diff --git a/src/server/models/cacheable_queries/organization-contact.js b/src/server/models/cacheable_queries/organization-contact.js index 2b2583de3..24609d2b3 100644 --- a/src/server/models/cacheable_queries/organization-contact.js +++ b/src/server/models/cacheable_queries/organization-contact.js @@ -1,11 +1,4 @@ -import { r } from ".."; - -// Datastructure: -// * regular GET/SET with JSON ordered list of the objects {id,title,text} -// * keyed by campaignId-userId pairs -- userId is '' for global campaign records -// Requirements: -// * needs an order -// * needs to get by campaignId-userId pairs +import { r } from "../../models"; const getCacheKey = (organizationId, contactNumber) => `${process.env.CACHE_PREFIX || @@ -60,9 +53,7 @@ const organizationContactCache = { .expire(cacheKey, 43200) // 12 hours .execAsync(); } - - return await knex.raw(query); } }; -export default organizationContactCache; +export default organizationContactCache; \ No newline at end of file diff --git a/src/server/models/organization-contact.js b/src/server/models/organization-contact.js index 48fc3fb21..92290413e 100644 --- a/src/server/models/organization-contact.js +++ b/src/server/models/organization-contact.js @@ -1,6 +1,6 @@ import thinky from "./thinky"; const type = thinky.type; -import { requiredString } from "./custom-types"; +import { optionalString, requiredString } from "./custom-types"; // For documentation purposes only. Use knex queries instead of this model. const OrganizationContact = thinky.createModel( @@ -11,7 +11,7 @@ const OrganizationContact = thinky.createModel( id: type.string(), organization_id: requiredString(), contact_number: requiredString(), - user_number: requiredString() + user_number: optionalString() }) .allowExtra(false) ); From 56f94ffcf105bf2d4bb84b7786b942943db3de7e Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Wed, 27 Jan 2021 19:45:08 -0600 Subject: [PATCH 015/191] Get Org Contact User Number --- src/server/api/lib/twilio.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/server/api/lib/twilio.js b/src/server/api/lib/twilio.js index b732c2cfe..bd079dff9 100644 --- a/src/server/api/lib/twilio.js +++ b/src/server/api/lib/twilio.js @@ -233,7 +233,7 @@ async function getMessagingServiceSid( ); } -async function getOrganizationContact(organization, contactNumber) { +async function getOrganizationContactUserNumber(organization, contactNumber) { const organizationContact = await cacheableData.organizationContact.query({ organizationId: organization.id, contactNumber @@ -277,14 +277,10 @@ async function sendMessage(message, contact, trx, organization, campaign) { let userNumber; if (process.env.EXPERIMENTAL_STICKY_SENDER) { - const organizationContact = await getOrganizationContact( + userNumber = await getOrganizationContactUserNumber( organization, contact.cell ); - - if (organizationContact) { - userNumber = organizationContact.user_number; - } } return new Promise((resolve, reject) => { From eed6cb7251a2ac66597cfdc0b1f876f2465806f2 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Wed, 27 Jan 2021 20:01:16 -0600 Subject: [PATCH 016/191] fixing message service sid logic --- src/server/api/lib/twilio.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/server/api/lib/twilio.js b/src/server/api/lib/twilio.js index bd079dff9..c52599bdb 100644 --- a/src/server/api/lib/twilio.js +++ b/src/server/api/lib/twilio.js @@ -332,12 +332,13 @@ async function sendMessage(message, contact, trx, organization, campaign) { body: message.text, statusCallback: process.env.TWILIO_STATUS_CALLBACK_URL }, - messagingServiceSid ? { messagingServiceSid } : {}, - twilioValidityPeriod ? { validityPeriod: twilioValidityPeriod } : {}, - parseMessageText(message), userNumber ? { from: userNumber } - : { messagingServiceSid: messagingServiceSid } + : messagingServiceSid + ? { messagingServiceSid } + : {}, + twilioValidityPeriod ? { validityPeriod: twilioValidityPeriod } : {}, + parseMessageText(message) ); console.log("twilioMessage", messageParams); From 571b7a1f30c1008b90cf60e925f747bc28ec33c8 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Wed, 27 Jan 2021 20:01:56 -0600 Subject: [PATCH 017/191] fixing message service sid logic 2 From b0fff210154fcb34e085e545f719892c40eec4b6 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Wed, 27 Jan 2021 20:09:35 -0600 Subject: [PATCH 018/191] orgContactCache --- src/server/models/cacheable_queries/message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/models/cacheable_queries/message.js b/src/server/models/cacheable_queries/message.js index 34aa35138..70202cb9d 100644 --- a/src/server/models/cacheable_queries/message.js +++ b/src/server/models/cacheable_queries/message.js @@ -232,7 +232,7 @@ const deliveryReport = async ({ }); if (!organizationContact) { - await organizationContact.save({ + await organizationContactCache.save({ organizationId: organizationId, contactNumber: contactNumber, userNumber: userNumber From d4533ef72d311ad21f7c4e2c77436a092616cb9e Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Wed, 27 Jan 2021 23:14:29 -0600 Subject: [PATCH 019/191] Returning() doesn't work on sqlite --- src/server/models/cacheable_queries/message.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/server/models/cacheable_queries/message.js b/src/server/models/cacheable_queries/message.js index 70202cb9d..8ba518ae1 100644 --- a/src/server/models/cacheable_queries/message.js +++ b/src/server/models/cacheable_queries/message.js @@ -212,20 +212,24 @@ const deliveryReport = async ({ } } - const [message] = await r + await r .knex("message") .where("service_id", messageSid) .limit(1) - .returning("*") .update(changes); if (process.env.EXPERIMENTAL_STICKY_SENDER && newStatus === "DELIVERED") { + const [message] = await r + .knex("message") + .where("service_id", messageSid) + .limit(1); + // Assign user number to contact/organization const campaignContact = await campaignContactCache.load( message.campaign_contact_id ); - const organizationId = await campaignContactCache.orgId(campaignContact); + const organizationId = await campaignContactCache.orgId(campaignContact); const organizationContact = await organizationContactCache.query({ organizationId, contactNumber From 995363a90a98eba010a8c02828019f95647c55b0 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sat, 30 Jan 2021 18:13:45 -0500 Subject: [PATCH 020/191] move twilio test --- .../api/lib => extensions/messaging_services}/twilio.test.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename __test__/{server/api/lib => extensions/messaging_services}/twilio.test.js (100%) diff --git a/__test__/server/api/lib/twilio.test.js b/__test__/extensions/messaging_services/twilio.test.js similarity index 100% rename from __test__/server/api/lib/twilio.test.js rename to __test__/extensions/messaging_services/twilio.test.js From 0b68663f5ffa83fdba929647cfc84ba70ffbe395 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Fri, 22 Jan 2021 13:12:00 -0500 Subject: [PATCH 021/191] refactor twilio mock --- __test__/server/api/campaign/campaign.test.js | 99 ++++++++++--------- 1 file changed, 52 insertions(+), 47 deletions(-) diff --git a/__test__/server/api/campaign/campaign.test.js b/__test__/server/api/campaign/campaign.test.js index 7b73ac8df..54977f323 100644 --- a/__test__/server/api/campaign/campaign.test.js +++ b/__test__/server/api/campaign/campaign.test.js @@ -1,41 +1,39 @@ import gql from "graphql-tag"; -import { r } from "../../../../src/server/models"; -import { getConfig } from "../../../../src/server/api/lib/config"; -import { dataQuery as TexterTodoListQuery } from "../../../../src/containers/TexterTodoList"; -import { - dataQuery as TexterTodoQuery, - campaignQuery -} from "../../../../src/containers/TexterTodo"; import { campaignDataQuery as AdminCampaignEditQuery } from "../../../../src/containers/AdminCampaignEdit"; -import { campaignsQuery } from "../../../../src/containers/PaginatedCampaignsRetriever"; - import { bulkReassignCampaignContactsMutation, reassignCampaignContactsMutation } from "../../../../src/containers/AdminIncomingMessageList"; - +import { campaignsQuery } from "../../../../src/containers/PaginatedCampaignsRetriever"; +import { + campaignQuery, + dataQuery as TexterTodoQuery +} from "../../../../src/containers/TexterTodo"; +import { dataQuery as TexterTodoListQuery } from "../../../../src/containers/TexterTodoList"; +import * as twilio from "../../../../src/extensions/messaging_services/twilio"; import { makeTree } from "../../../../src/lib"; - +import { getConfig } from "../../../../src/server/api/lib/config"; +import { r } from "../../../../src/server/models"; import { - setupTest, + assignTexter, + bulkSendMessages, cleanupTest, - createUser, - createInvite, - createOrganization, - createCampaign, - saveCampaign, copyCampaign, + createCampaign, + createCannedResponses, createContacts, - createTexter, - assignTexter, + createInvite, + createOrganization, createScript, - createCannedResponses, - startCampaign, + createTexter, + createUser, getCampaignContact, - sendMessage, - bulkSendMessages, runGql, - sleep + saveCampaign, + sendMessage, + setupTest, + sleep, + startCampaign } from "../../../test_helpers"; jest.mock("../../../../src/extensions/messaging_services/twilio"); @@ -1231,15 +1229,21 @@ describe("useOwnMessagingService", async () => { global.TWILIO_MESSAGE_SERVICE_SID ); }); - it("creates new messaging service when true", async () => { - await saveCampaign( - testAdminUser, - { id: testCampaign.id, organizationId }, - "test campaign new title", - true - ); + describe("when true", () => { + beforeEach(() => { + jest.spyOn(twilio, "createMessagingService").mockReturnValue({ + sid: "testTWILIOsid" + }); + }); + it("creates new messaging service when true", async () => { + await saveCampaign( + testAdminUser, + { id: testCampaign.id, organizationId }, + "test campaign new title", + true + ); - const getCampaignsQuery = ` + const getCampaignsQuery = ` query getCampaign($campaignId: String!) { campaign(id: $campaignId) { id @@ -1249,24 +1253,25 @@ describe("useOwnMessagingService", async () => { } `; - const variables = { - campaignId: testCampaign.id - }; + const variables = { + campaignId: testCampaign.id + }; - await startCampaign(testAdminUser, testCampaign); + await startCampaign(testAdminUser, testCampaign); - const campaignDataResults = await runGql( - getCampaignsQuery, - variables, - testAdminUser - ); + const campaignDataResults = await runGql( + getCampaignsQuery, + variables, + testAdminUser + ); - expect(campaignDataResults.data.campaign.useOwnMessagingService).toEqual( - true - ); - expect(campaignDataResults.data.campaign.messageserviceSid).toEqual( - "testTWILIOsid" - ); + expect(campaignDataResults.data.campaign.useOwnMessagingService).toEqual( + true + ); + expect(campaignDataResults.data.campaign.messageserviceSid).toEqual( + "testTWILIOsid" + ); + }); }); }); From 7c3f8e53cb7a54f20b7148041d96f679399955d1 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Fri, 22 Jan 2021 13:12:00 -0500 Subject: [PATCH 022/191] refactor twilio mock --- .../messaging_services/twilio.test.js | 825 +++++++++--------- 1 file changed, 417 insertions(+), 408 deletions(-) diff --git a/__test__/extensions/messaging_services/twilio.test.js b/__test__/extensions/messaging_services/twilio.test.js index 7647f50ff..73aff7c17 100644 --- a/__test__/extensions/messaging_services/twilio.test.js +++ b/__test__/extensions/messaging_services/twilio.test.js @@ -1,210 +1,251 @@ /* eslint-disable no-unused-expressions, consistent-return */ -import { r, Message, cacheableData } from "../../../../src/server/models/"; -import { getConfig } from "../../../../src/server/api/lib/config"; -import twilio, { - postMessageSend, - handleDeliveryReport -} from "../../../../src/extensions/messaging_services/twilio"; import { getLastMessage } from "../../../../src/extensions/messaging_services/message-sending"; +import * as twilio from "../../../../src/extensions/messaging_services/twilio"; // } // MAX_SEND_ATTEMPTS // handleDeliveryReport, // postMessageSend, // , { +import { getConfig } from "../../../../src/server/api/lib/config"; +import { cacheableData, Message, r } from "../../../../src/server/models/"; import { erroredMessageSender } from "../../../../src/workers/job-processes"; import { - setupTest, + assignTexter, cleanupTest, - createUser, - createInvite, - createOrganization, - setTwilioAuth, createCampaign, createContacts, - createTexter, - assignTexter, + createInvite, + createOrganization, createScript, - startCampaign, - getCampaignContact + createTexter, + createUser, + getCampaignContact, + setTwilioAuth, + setupTest, + startCampaign } from "../../../test_helpers"; -let testAdminUser; -let testInvite; -let testInvite2; -let testOrganization; -let testOrganization2; -let testCampaign; -let testTexterUser; -let testContacts; -let organizationId; -let organizationId2; -let assignmentId; -let dbCampaignContact; -let queryLog; - -function spokeDbListener(data) { - if (queryLog) { - queryLog.push(data); +describe("twilio", () => { + let testAdminUser; + let testInvite; + let testInvite2; + let testOrganization; + let testOrganization2; + let testCampaign; + let testTexterUser; + let testContacts; + let organizationId; + let organizationId2; + let dbCampaignContact; + let queryLog; + let mockAddNumberToMessagingService; + let mockMessageCreate; + + function spokeDbListener(data) { + if (queryLog) { + queryLog.push(data); + } } -} - -const mockAddNumberToMessagingService = jest.fn(); -const mockMessageCreate = jest.fn(); - -jest.mock("twilio", () => { - const uuid = require("uuid"); - return jest.fn().mockImplementation(() => ({ - availablePhoneNumbers: _ => ({ - local: { - list: ({ areaCode, limit }) => { - const response = []; - for (let i = 0; i < limit; i++) { - const last4 = limit.toString().padStart(4, "0"); - response.push({ - phoneNumber: `+1${areaCode}XYZ${last4}` - }); + + beforeEach(async () => { + mockAddNumberToMessagingService = jest.fn(); + mockMessageCreate = jest.fn(); + + jest.spyOn(twilio, "getTwilio").mockImplementation(() => { + // eslint-disable-next-line global-require + const uuid = require("uuid"); + return { + availablePhoneNumbers: () => ({ + local: { + list: ({ areaCode, limit }) => { + const response = []; + for (let i = 0; i < limit; i++) { + const last4 = limit.toString().padStart(4, "0"); + response.push({ + phoneNumber: `+1${areaCode}XYZ${last4}` + }); + } + return response; + } } - return response; - } - } - }), - incomingPhoneNumbers: { - create: () => ({ - sid: `PNTEST${uuid.v4()}` - }) - }, - messaging: { - services: () => ({ - phoneNumbers: { - create: mockAddNumberToMessagingService + }), + incomingPhoneNumbers: { + create: () => ({ + sid: `PNTEST${uuid.v4()}` + }) + }, + messaging: { + services: () => ({ + phoneNumbers: { + create: mockAddNumberToMessagingService + } + }) + }, + messages: { + create: mockMessageCreate } - }) - }, - messages: { - create: mockMessageCreate - } - })); -}); + }; + }); -beforeEach(async () => { - // Set up an entire working campaign - await setupTest(); - testAdminUser = await createUser(); - testInvite = await createInvite(); - testOrganization = await createOrganization(testAdminUser, testInvite); - organizationId = testOrganization.data.createOrganization.id; - testCampaign = await createCampaign(testAdminUser, testOrganization); - testContacts = await createContacts(testCampaign, 100); - testTexterUser = await createTexter(testOrganization); - await assignTexter(testAdminUser, testTexterUser, testCampaign); - dbCampaignContact = await getCampaignContact(testContacts[0].id); - assignmentId = dbCampaignContact.assignment_id; - await createScript(testAdminUser, testCampaign); - await startCampaign(testAdminUser, testCampaign); - testInvite2 = await createInvite(); - testOrganization2 = await createOrganization(testAdminUser, testInvite2); - organizationId2 = testOrganization2.data.createOrganization.id; - await setTwilioAuth(testAdminUser, testOrganization2); - - // use caching - await cacheableData.organization.load(organizationId); - await cacheableData.campaign.load(testCampaign.id, { forceLoad: true }); - - queryLog = []; - r.knex.on("query", spokeDbListener); -}, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); - -afterEach(async () => { - queryLog = null; - r.knex.removeListener("query", spokeDbListener); - await cleanupTest(); - if (r.redis) r.redis.flushdb(); -}, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); - -it("should send messages", async () => { - let message = await Message.save({ - campaign_contact_id: dbCampaignContact.id, - messageservice_sid: "test_message_service", - contact_number: dbCampaignContact.cell, - is_from_contact: false, - send_status: "SENDING", - service: "twilio", - text: "blah blah blah", - user_id: testTexterUser.id - }); + // Set up an entire working campaign + await setupTest(); + testAdminUser = await createUser(); + testInvite = await createInvite(); + testOrganization = await createOrganization(testAdminUser, testInvite); + organizationId = testOrganization.data.createOrganization.id; + testCampaign = await createCampaign(testAdminUser, testOrganization); + testContacts = await createContacts(testCampaign, 100); + testTexterUser = await createTexter(testOrganization); + await assignTexter(testAdminUser, testTexterUser, testCampaign); + dbCampaignContact = await getCampaignContact(testContacts[0].id); + await createScript(testAdminUser, testCampaign); + await startCampaign(testAdminUser, testCampaign); + testInvite2 = await createInvite(); + testOrganization2 = await createOrganization(testAdminUser, testInvite2); + organizationId2 = testOrganization2.data.createOrganization.id; + await setTwilioAuth(testAdminUser, testOrganization2); + + // use caching + await cacheableData.organization.load(organizationId); + await cacheableData.campaign.load(testCampaign.id, { forceLoad: true }); + + queryLog = []; + r.knex.on("query", spokeDbListener); + }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); + + afterEach(async () => { + queryLog = null; + r.knex.removeListener("query", spokeDbListener); + await cleanupTest(); + if (r.redis) r.redis.flushdb(); + }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); + + it("should send messages", async () => { + let message = await Message.save({ + campaign_contact_id: dbCampaignContact.id, + messageservice_sid: "test_message_service", + contact_number: dbCampaignContact.cell, + is_from_contact: false, + send_status: "SENDING", + service: "twilio", + text: "blah blah blah", + user_id: testTexterUser.id + }); - await setTwilioAuth(testAdminUser, testOrganization); - const org = await cacheableData.organization.load(organizationId); + await setTwilioAuth(testAdminUser, testOrganization); + const org = await cacheableData.organization.load(organizationId); - mockMessageCreate.mockImplementation((payload, cb) => { - cb(null, { sid: "SM12345", error_code: null }); - }); + mockMessageCreate.mockImplementation((payload, cb) => { + cb(null, { sid: "SM12345", error_code: null }); + }); - await twilio.sendMessage(message, dbCampaignContact, null, org); - expect(mockMessageCreate).toHaveBeenCalledTimes(1); - const arg = mockMessageCreate.mock.calls[0][0]; - expect(arg).toMatchObject({ - to: dbCampaignContact.cell, - body: "blah blah blah", - messagingServiceSid: "test_message_service" - }); + await twilio.sendMessage(message, dbCampaignContact, null, org); + expect(mockMessageCreate).toHaveBeenCalledTimes(1); + const arg = mockMessageCreate.mock.calls[0][0]; + expect(arg).toMatchObject({ + to: dbCampaignContact.cell, + body: "blah blah blah", + messagingServiceSid: "test_message_service" + }); - message = await Message.get(message.id); - expect(message).toMatchObject({ - service_id: "SM12345", - send_status: "SENT" + message = await Message.get(message.id); + expect(message).toMatchObject({ + service_id: "SM12345", + send_status: "SENT" + }); }); -}); -it("postMessageSend success should save message and update contact state", async () => { - const message = await Message.save({ - campaign_contact_id: dbCampaignContact.id, - messageservice_sid: "fakeSid_MK123", - contact_number: dbCampaignContact.cell, - is_from_contact: false, - send_status: "SENDING", - service: "twilio", - text: "some message", - user_id: testTexterUser.id - }); - const updatedMessage = await new Promise((resolve, reject) => { - postMessageSend( - message, - dbCampaignContact, - null, - resolve, - reject, - // err, resposne - null, - { sid: "1234", error_code: null } + it("postMessageSend success should save message and update contact state", async () => { + const message = await Message.save({ + campaign_contact_id: dbCampaignContact.id, + messageservice_sid: "fakeSid_MK123", + contact_number: dbCampaignContact.cell, + is_from_contact: false, + send_status: "SENDING", + service: "twilio", + text: "some message", + user_id: testTexterUser.id + }); + const updatedMessage = await new Promise((resolve, reject) => { + twilio.postMessageSend( + message, + dbCampaignContact, + null, + resolve, + reject, + // err, resposne + null, + { sid: "1234", error_code: null } + ); + }); + expect(updatedMessage.send_status).toEqual("SENT"); + expect(Number(updatedMessage.sent_at) > Number(new Date()) - 1000).toEqual( + true ); }); - expect(updatedMessage.send_status).toEqual("SENT"); - expect(Number(updatedMessage.sent_at) > Number(new Date()) - 1000).toEqual( - true - ); -}); -it("postMessageSend network error should decrement on err/failure ", async () => { - let message = await Message.save({ - campaign_contact_id: dbCampaignContact.id, - messageservice_sid: "fakeSid_MK123", - contact_number: dbCampaignContact.cell, - is_from_contact: false, - send_status: "SENDING", - service: "twilio", - text: "some message", - user_id: testTexterUser.id + it("postMessageSend network error should decrement on err/failure ", async () => { + let message = await Message.save({ + campaign_contact_id: dbCampaignContact.id, + messageservice_sid: "fakeSid_MK123", + contact_number: dbCampaignContact.cell, + is_from_contact: false, + send_status: "SENDING", + service: "twilio", + text: "some message", + user_id: testTexterUser.id + }); + for (let i = 1; i < 7; i++) { + // We loop MAX_SEND_ATTEMPTS + 1 times (starting at i=1! + // The last time, the send_status should update to "ERROR" (so we don't keep trying) + try { + await new Promise((resolve, reject) => { + twilio.postMessageSend( + message, + dbCampaignContact, + null, + resolve, + reject, + // err, resposne + { status: "ETIMEDOUT" }, + null, + null, + null, + { + maxSendAttempts: twilio.MAX_SEND_ATTEMPTS, + serviceName: "twilio" + } + ); + }); + expect("above statement to throw error w/ reject").toEqual(true); + } catch (err) { + dbCampaignContact = await getCampaignContact(dbCampaignContact.id); + message = await Message.get(message.id); + + expect(dbCampaignContact.error_code).toEqual(-i); + expect(message.error_code).toEqual(-i); + expect(message.send_status).toEqual(i < 6 ? "SENDING" : "ERROR"); + } + } }); - for (let i = 1; i < 7; i++) { - // We loop MAX_SEND_ATTEMPTS + 1 times (starting at i=1! - // The last time, the send_status should update to "ERROR" (so we don't keep trying) + + it("postMessageSend error from twilio response should fail immediately", async () => { + let message = await Message.save({ + campaign_contact_id: dbCampaignContact.id, + messageservice_sid: "fakeSid_MK123", + contact_number: dbCampaignContact.cell, + is_from_contact: false, + send_status: "SENDING", + service: "twilio", + text: "some message", + user_id: testTexterUser.id + }); try { await new Promise((resolve, reject) => { - postMessageSend( + twilio.postMessageSend( message, dbCampaignContact, null, resolve, reject, // err, resposne - { status: "ETIMEDOUT" }, - null + null, + { sid: "1234", error_code: 11200 } ); }); expect("above statement to throw error w/ reject").toEqual(true); @@ -212,272 +253,240 @@ it("postMessageSend network error should decrement on err/failure ", async () => dbCampaignContact = await getCampaignContact(dbCampaignContact.id); message = await Message.get(message.id); - expect(dbCampaignContact.error_code).toEqual(-i); - expect(message.error_code).toEqual(-i); - expect(message.send_status).toEqual(i < 6 ? "SENDING" : "ERROR"); + expect(dbCampaignContact.error_code).toEqual(11200); + expect(message.error_code).toEqual(11200); + expect(message.send_status).toEqual("ERROR"); } - } -}); - -it("postMessageSend error from twilio response should fail immediately", async () => { - let message = await Message.save({ - campaign_contact_id: dbCampaignContact.id, - messageservice_sid: "fakeSid_MK123", - contact_number: dbCampaignContact.cell, - is_from_contact: false, - send_status: "SENDING", - service: "twilio", - text: "some message", - user_id: testTexterUser.id }); - try { - await new Promise((resolve, reject) => { - postMessageSend( - message, - dbCampaignContact, - null, - resolve, - reject, - // err, resposne - null, - { sid: "1234", error_code: 11200 } - ); - }); - expect("above statement to throw error w/ reject").toEqual(true); - } catch (err) { - dbCampaignContact = await getCampaignContact(dbCampaignContact.id); - message = await Message.get(message.id); - expect(dbCampaignContact.error_code).toEqual(11200); - expect(message.error_code).toEqual(11200); - expect(message.send_status).toEqual("ERROR"); - } -}); + it("handleIncomingMessage should save message and update contact state", async () => { + // use caching + const org = await cacheableData.organization.load(organizationId); + const campaign = await cacheableData.campaign.load(testCampaign.id, { + forceLoad: true + }); + await cacheableData.campaignContact.loadMany(campaign, org, {}); + queryLog = []; + await cacheableData.message.save({ + contact: dbCampaignContact, + messageInstance: new Message({ + campaign_contact_id: dbCampaignContact.id, + contact_number: dbCampaignContact.cell, + messageservice_sid: "fakeSid_MK123", + is_from_contact: false, + send_status: "SENT", + service: "twilio", + text: "some message", + user_id: testTexterUser.id, + service_id: "123123123" + }) + }); -it("handleIncomingMessage should save message and update contact state", async () => { - // use caching - const org = await cacheableData.organization.load(organizationId); - const campaign = await cacheableData.campaign.load(testCampaign.id, { - forceLoad: true - }); - await cacheableData.campaignContact.loadMany(campaign, org, {}); - queryLog = []; - await cacheableData.message.save({ - contact: dbCampaignContact, - messageInstance: new Message({ - campaign_contact_id: dbCampaignContact.id, - contact_number: dbCampaignContact.cell, - messageservice_sid: "fakeSid_MK123", - is_from_contact: false, - send_status: "SENT", + const lastMessage = await getLastMessage({ + contactNumber: dbCampaignContact.cell, service: "twilio", - text: "some message", - user_id: testTexterUser.id, - service_id: "123123123" - }) - }); - - const lastMessage = await getLastMessage({ - contactNumber: dbCampaignContact.cell, - service: "twilio", - messageServiceSid: "fakeSid_MK123" - }); - - expect(lastMessage.campaign_contact_id).toEqual(dbCampaignContact.id); - await twilio.handleIncomingMessage({ - From: dbCampaignContact.cell, - To: "+16465559999", - MessageSid: "TestMessageId", - Body: "Fake reply", - MessagingServiceSid: "fakeSid_MK123" - }); - - if (r.redis && getConfig("REDIS_CONTACT_CACHE")) { - // IMPORTANT: this should be tested before we do SELECT statements below - // in the test itself to check the database - const selectMethods = { select: 1, first: 1 }; - const selectCalls = queryLog.filter(q => q.method in selectMethods); - // NO select statements should have fired! - expect(selectCalls).toEqual([]); - } - - const [reply] = await r.knex("message").where("service_id", "TestMessageId"); - dbCampaignContact = await getCampaignContact(dbCampaignContact.id); - - expect(reply.send_status).toEqual("DELIVERED"); - expect(reply.campaign_contact_id).toEqual(dbCampaignContact.id); - expect(reply.contact_number).toEqual(dbCampaignContact.cell); - expect(reply.user_number).toEqual("+16465559999"); - expect(reply.messageservice_sid).toEqual("fakeSid_MK123"); - expect(dbCampaignContact.message_status).toEqual("needsResponse"); -}); + messageServiceSid: "fakeSid_MK123" + }); -it("postMessageSend+erroredMessageSender network error should decrement on err/failure ", async () => { - let message = await Message.save({ - campaign_contact_id: dbCampaignContact.id, - messageservice_sid: "fakeSid_MK123", - contact_number: dbCampaignContact.cell, - is_from_contact: false, - send_status: "SENDING", - service: "twilio", // important since message.service is used in erroredMessageSender - text: "twilioapitesterrortimeout <= IMPORTANT text to make this test work!", - user_id: testTexterUser.id, - error_code: -1 // important to start with an error - }); - for (let i = 1; i < 7; i++) { - const errorSendResult = await erroredMessageSender({ - delay: 1, - maxCount: 2 + expect(lastMessage.campaign_contact_id).toEqual(dbCampaignContact.id); + await twilio.handleIncomingMessage({ + From: dbCampaignContact.cell, + To: "+16465559999", + MessageSid: "TestMessageId", + Body: "Fake reply", + MessagingServiceSid: "fakeSid_MK123" }); - if (i < 6) { - expect(errorSendResult).toBe(1); - } else { - expect(errorSendResult).toBe(0); + + if (r.redis && getConfig("REDIS_CONTACT_CACHE")) { + // IMPORTANT: this should be tested before we do SELECT statements below + // in the test itself to check the database + const selectMethods = { select: 1, first: 1 }; + const selectCalls = queryLog.filter(q => q.method in selectMethods); + // NO select statements should have fired! + expect(selectCalls).toEqual([]); } - } -}); -it("handleDeliveryReport delivered", async () => { - const org = await cacheableData.organization.load(organizationId); - let campaign = await cacheableData.campaign.load(testCampaign.id, { - forceLoad: true + const [reply] = await r + .knex("message") + .where("service_id", "TestMessageId"); + dbCampaignContact = await getCampaignContact(dbCampaignContact.id); + + expect(reply.send_status).toEqual("DELIVERED"); + expect(reply.campaign_contact_id).toEqual(dbCampaignContact.id); + expect(reply.contact_number).toEqual(dbCampaignContact.cell); + expect(reply.user_number).toEqual("+16465559999"); + expect(reply.messageservice_sid).toEqual("fakeSid_MK123"); + expect(dbCampaignContact.message_status).toEqual("needsResponse"); }); - await cacheableData.campaignContact.loadMany(campaign, org, {}); - queryLog = []; - const messageSid = "123123123"; - await cacheableData.message.save({ - contact: dbCampaignContact, - messageInstance: new Message({ + it("postMessageSend+erroredMessageSender network error should decrement on err/failure ", async () => { + await Message.save({ campaign_contact_id: dbCampaignContact.id, - contact_number: dbCampaignContact.cell, messageservice_sid: "fakeSid_MK123", + contact_number: dbCampaignContact.cell, is_from_contact: false, - send_status: "SENT", - service: "twilio", - text: "some message", + send_status: "SENDING", + service: "twilio", // important since message.service is used in erroredMessageSender + text: + "twilioapitesterrortimeout <= IMPORTANT text to make this test work!", user_id: testTexterUser.id, - service_id: messageSid - }) - }); - await handleDeliveryReport({ - MessageSid: messageSid, - MessagingServiceSid: "fakeSid_MK123", - To: dbCampaignContact.cell, - MessageStatus: "delivered", - From: "+14145551010" + error_code: -1 // important to start with an error + }); + for (let i = 1; i < 7; i++) { + const errorSendResult = await erroredMessageSender({ + delay: 1, + maxCount: 2 + }); + if (i < 6) { + expect(errorSendResult).toBe(1); + } else { + expect(errorSendResult).toBe(0); + } + } }); - const messages = await r.knex("message").where("service_id", messageSid); - expect(messages.length).toBe(1); - expect(messages[0].error_code).toBe(null); - expect(messages[0].send_status).toBe("DELIVERED"); - - const contacts = await r - .knex("campaign_contact") - .where("id", dbCampaignContact.id); + it("handleDeliveryReport delivered", async () => { + const org = await cacheableData.organization.load(organizationId); + let campaign = await cacheableData.campaign.load(testCampaign.id, { + forceLoad: true + }); + await cacheableData.campaignContact.loadMany(campaign, org, {}); + queryLog = []; + + const messageSid = "123123123"; + await cacheableData.message.save({ + contact: dbCampaignContact, + messageInstance: new Message({ + campaign_contact_id: dbCampaignContact.id, + contact_number: dbCampaignContact.cell, + messageservice_sid: "fakeSid_MK123", + is_from_contact: false, + send_status: "SENT", + service: "twilio", + text: "some message", + user_id: testTexterUser.id, + service_id: messageSid + }) + }); + await twilio.handleDeliveryReport({ + MessageSid: messageSid, + MessagingServiceSid: "fakeSid_MK123", + To: dbCampaignContact.cell, + MessageStatus: "delivered", + From: "+14145551010" + }); - expect(contacts.length).toBe(1); - expect(contacts[0].error_code).toBe(null); + const messages = await r.knex("message").where("service_id", messageSid); + expect(messages.length).toBe(1); + expect(messages[0].error_code).toBe(null); + expect(messages[0].send_status).toBe("DELIVERED"); - if (r.redis) { - campaign = await cacheableData.campaign.load(testCampaign.id); - expect(campaign.errorCount).toBe(null); - } -}); + const contacts = await r + .knex("campaign_contact") + .where("id", dbCampaignContact.id); -it("handleDeliveryReport error", async () => { - const org = await cacheableData.organization.load(organizationId); - let campaign = await cacheableData.campaign.load(testCampaign.id, { - forceLoad: true - }); - await cacheableData.campaignContact.loadMany(campaign, org, {}); - queryLog = []; + expect(contacts.length).toBe(1); + expect(contacts[0].error_code).toBe(null); - const messageSid = "123123123"; - await cacheableData.message.save({ - contact: dbCampaignContact, - messageInstance: new Message({ - campaign_contact_id: dbCampaignContact.id, - contact_number: dbCampaignContact.cell, - messageservice_sid: "fakeSid_MK123", - is_from_contact: false, - send_status: "SENT", - service: "twilio", - text: "some message", - user_id: testTexterUser.id, - service_id: messageSid - }) - }); - await handleDeliveryReport({ - MessageSid: messageSid, - MessagingServiceSid: "fakeSid_MK123", - To: dbCampaignContact.cell, - MessageStatus: "failed", - From: "+14145551010", - ErrorCode: "98989" + if (r.redis) { + campaign = await cacheableData.campaign.load(testCampaign.id); + expect(campaign.errorCount).toBe(null); + } }); - const messages = await r.knex("message").where("service_id", messageSid); - expect(messages.length).toBe(1); - expect(messages[0].error_code).toBe(98989); - expect(messages[0].send_status).toBe("ERROR"); + it("handleDeliveryReport error", async () => { + const org = await cacheableData.organization.load(organizationId); + let campaign = await cacheableData.campaign.load(testCampaign.id, { + forceLoad: true + }); + await cacheableData.campaignContact.loadMany(campaign, org, {}); + queryLog = []; + + const messageSid = "123123123"; + await cacheableData.message.save({ + contact: dbCampaignContact, + messageInstance: new Message({ + campaign_contact_id: dbCampaignContact.id, + contact_number: dbCampaignContact.cell, + messageservice_sid: "fakeSid_MK123", + is_from_contact: false, + send_status: "SENT", + service: "twilio", + text: "some message", + user_id: testTexterUser.id, + service_id: messageSid + }) + }); + await twilio.handleDeliveryReport({ + MessageSid: messageSid, + MessagingServiceSid: "fakeSid_MK123", + To: dbCampaignContact.cell, + MessageStatus: "failed", + From: "+14145551010", + ErrorCode: "98989" + }); - const contacts = await r - .knex("campaign_contact") - .where("id", dbCampaignContact.id); + const messages = await r.knex("message").where("service_id", messageSid); + expect(messages.length).toBe(1); + expect(messages[0].error_code).toBe(98989); + expect(messages[0].send_status).toBe("ERROR"); - expect(contacts.length).toBe(1); - expect(contacts[0].error_code).toBe(98989); + const contacts = await r + .knex("campaign_contact") + .where("id", dbCampaignContact.id); - if (r.redis) { - campaign = await cacheableData.campaign.load(testCampaign.id); - expect(campaign.errorCount).toBe("1"); - } -}); + expect(contacts.length).toBe(1); + expect(contacts[0].error_code).toBe(98989); -it("orgs should have separate twilio credentials", async () => { - const org1 = await cacheableData.organization.load(organizationId); - const org1Auth = await cacheableData.organization.getTwilioAuth(org1); - expect(org1Auth.authToken).toBeUndefined(); - expect(org1Auth.accountSid).toBeUndefined(); + if (r.redis) { + campaign = await cacheableData.campaign.load(testCampaign.id); + expect(campaign.errorCount).toBe("1"); + } + }); - const org2 = await cacheableData.organization.load(organizationId2); - const org2Auth = await cacheableData.organization.getTwilioAuth(org2); - expect(org2Auth.authToken).toBe("test_twlio_auth_token"); - expect(org2Auth.accountSid).toBe("test_twilio_account_sid"); -}); + it("orgs should have separate twilio credentials", async () => { + const org1 = await cacheableData.organization.load(organizationId); + const org1Auth = await cacheableData.organization.getTwilioAuth(org1); + expect(org1Auth.authToken).toBeUndefined(); + expect(org1Auth.accountSid).toBeUndefined(); -describe("Number buying", () => { - it("buys numbers in batches from twilio", async () => { const org2 = await cacheableData.organization.load(organizationId2); - await twilio.buyNumbersInAreaCode(org2, "212", 35); - const inventoryCount = await r.getCount( - r.knex("owned_phone_number").where({ - area_code: "212", - organization_id: organizationId2, - allocated_to: null - }) - ); - - expect(inventoryCount).toEqual(35); + const org2Auth = await cacheableData.organization.getTwilioAuth(org2); + expect(org2Auth.authToken).toBe("test_twlio_auth_token"); + expect(org2Auth.accountSid).toBe("test_twilio_account_sid"); }); - it("optionally adds them to a messaging service", async () => { - const org2 = await cacheableData.organization.load(organizationId2); - await twilio.buyNumbersInAreaCode(org2, "917", 12, { - messagingServiceSid: "MG123FAKE" + describe("Number buying", () => { + it("buys numbers in batches from twilio", async () => { + const org2 = await cacheableData.organization.load(organizationId2); + await twilio.buyNumbersInAreaCode(org2, "212", 35); + const inventoryCount = await r.getCount( + r.knex("owned_phone_number").where({ + area_code: "212", + organization_id: organizationId2, + allocated_to: null + }) + ); + + expect(inventoryCount).toEqual(35); + }); + + it("optionally adds them to a messaging service", async () => { + const org2 = await cacheableData.organization.load(organizationId2); + await twilio.buyNumbersInAreaCode(org2, "917", 12, { + messagingServiceSid: "MG123FAKE" + }); + const inventoryCount = await r.getCount( + r.knex("owned_phone_number").where({ + area_code: "917", + organization_id: organizationId2, + allocated_to: "messaging_service", + allocated_to_id: "MG123FAKE" + }) + ); + expect(mockAddNumberToMessagingService).toHaveBeenCalledTimes(12); + expect(inventoryCount).toEqual(12); }); - const inventoryCount = await r.getCount( - r.knex("owned_phone_number").where({ - area_code: "917", - organization_id: organizationId2, - allocated_to: "messaging_service", - allocated_to_id: "MG123FAKE" - }) - ); - expect(mockAddNumberToMessagingService).toHaveBeenCalledTimes(12); - expect(inventoryCount).toEqual(12); }); }); From bc6deb66495e4e023ea2817a05ec0cd57931e212 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Fri, 22 Jan 2021 13:12:00 -0500 Subject: [PATCH 023/191] fix imports --- .../extensions/messaging_services/twilio.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/__test__/extensions/messaging_services/twilio.test.js b/__test__/extensions/messaging_services/twilio.test.js index 7647f50ff..4a9290fb6 100644 --- a/__test__/extensions/messaging_services/twilio.test.js +++ b/__test__/extensions/messaging_services/twilio.test.js @@ -1,12 +1,12 @@ /* eslint-disable no-unused-expressions, consistent-return */ -import { r, Message, cacheableData } from "../../../../src/server/models/"; -import { getConfig } from "../../../../src/server/api/lib/config"; +import { r, Message, cacheableData } from "../../../src/server/models/"; +import { getConfig } from "../../../src/server/api/lib/config"; import twilio, { postMessageSend, handleDeliveryReport -} from "../../../../src/extensions/messaging_services/twilio"; -import { getLastMessage } from "../../../../src/extensions/messaging_services/message-sending"; -import { erroredMessageSender } from "../../../../src/workers/job-processes"; +} from "../../../src/extensions/messaging_services/twilio"; +import { getLastMessage } from "../../../src/extensions/messaging_services/message-sending"; +import { erroredMessageSender } from "../../../src/workers/job-processes"; import { setupTest, cleanupTest, @@ -21,7 +21,7 @@ import { createScript, startCampaign, getCampaignContact -} from "../../../test_helpers"; +} from "../../test_helpers"; let testAdminUser; let testInvite; From 5e3cab38c906b4cf6883f13f0994b020577ef615 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Fri, 22 Jan 2021 13:12:00 -0500 Subject: [PATCH 024/191] remove .only so we don't skip tests --- __test__/extensions/message-handlers/ngpvan.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__test__/extensions/message-handlers/ngpvan.test.js b/__test__/extensions/message-handlers/ngpvan.test.js index 67af0357b..254eabc1f 100644 --- a/__test__/extensions/message-handlers/ngpvan.test.js +++ b/__test__/extensions/message-handlers/ngpvan.test.js @@ -66,7 +66,7 @@ describe("extensions.message-handlers.ngpvan", () => { ]); }); - describe.only("when NGP_VAN_INITIAL_TEXT_CANVASS_RESULT is not found in the actions", () => { + describe("when NGP_VAN_INITIAL_TEXT_CANVASS_RESULT is not found in the actions", () => { beforeEach(async () => { ActionHandlers.getActionChoiceData.mockRestore(); jest.spyOn(ActionHandlers, "getActionChoiceData").mockResolvedValue([]); From 004978f11ae71f1ff9b28e26d4594bfd988d0657 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Fri, 22 Jan 2021 13:12:00 -0500 Subject: [PATCH 025/191] refactor the way we mock twilio --- .../messaging_services/__mocks__/twilio.js | 11 ----- src/extensions/messaging_services/twilio.js | 40 ++++++++++++------- src/server/api/mutations/startCampaign.js | 2 +- 3 files changed, 27 insertions(+), 26 deletions(-) delete mode 100644 src/extensions/messaging_services/__mocks__/twilio.js diff --git a/src/extensions/messaging_services/__mocks__/twilio.js b/src/extensions/messaging_services/__mocks__/twilio.js deleted file mode 100644 index 38c4af023..000000000 --- a/src/extensions/messaging_services/__mocks__/twilio.js +++ /dev/null @@ -1,11 +0,0 @@ -const twilio = jest.genMockFromModule("twilio"); - -async function createMessagingService(friendlyName) { - return { - sid: "testTWILIOsid" - }; -} - -twilio.createMessagingService = createMessagingService; - -module.exports = twilio; diff --git a/src/extensions/messaging_services/twilio.js b/src/extensions/messaging_services/twilio.js index de5b6233e..c093eea43 100644 --- a/src/extensions/messaging_services/twilio.js +++ b/src/extensions/messaging_services/twilio.js @@ -30,7 +30,7 @@ const TWILIO_SKIP_VALIDATION = getConfig("TWILIO_SKIP_VALIDATION"); const BULK_REQUEST_CONCURRENCY = 5; const MAX_NUMBERS_PER_BUY_JOB = getConfig("MAX_NUMBERS_PER_BUY_JOB") || 100; -async function getTwilio(organization) { +export async function getTwilio(organization) { const { authToken, accountSid @@ -233,8 +233,14 @@ async function getMessagingServiceSid( ); } -async function sendMessage(message, contact, trx, organization, campaign) { - const twilio = await getTwilio(organization); +export async function sendMessage( + message, + contact, + trx, + organization, + campaign +) { + const twilio = await exports.getTwilio(organization); const APITEST = /twilioapitest/.test(message.text); if (!twilio && !APITEST) { log.warn( @@ -496,7 +502,7 @@ export async function handleDeliveryReport(report) { } } -async function handleIncomingMessage(message) { +export async function handleIncomingMessage(message) { if ( !message.hasOwnProperty("From") || !message.hasOwnProperty("To") || @@ -550,8 +556,8 @@ async function handleIncomingMessage(message) { /** * Create a new Twilio messaging service */ -async function createMessagingService(organization, friendlyName) { - const twilio = await getTwilio(organization); +export async function createMessagingService(organization, friendlyName) { + const twilio = await exports.getTwilio(organization); const twilioBaseUrl = getConfig("TWILIO_BASE_CALLBACK_URL", organization) || getConfig("BASE_URL"); @@ -599,7 +605,7 @@ async function searchForAvailableNumbers( * Fetch Phone Numbers assigned to Messaging Service */ async function getPhoneNumbersForService(organization, messagingServiceSid) { - const twilio = await getTwilio(organization); + const twilio = await exports.getTwilio(organization); return await twilio.messaging .services(messagingServiceSid) .phoneNumbers.list({ limit: 400 }); @@ -671,8 +677,13 @@ async function bulkRequest(array, fn) { /** * Buy up to numbers in */ -async function buyNumbersInAreaCode(organization, areaCode, limit, opts = {}) { - const twilioInstance = await getTwilio(organization); +export async function buyNumbersInAreaCode( + organization, + areaCode, + limit, + opts = {} +) { + const twilioInstance = await exports.getTwilio(organization); const countryCode = getConfig("PHONE_NUMBER_COUNTRY ", organization) || "US"; async function buyBatch(size) { let successCount = 0; @@ -713,7 +724,7 @@ async function addNumbersToMessagingService( phoneSids, messagingServiceSid ) { - const twilioInstance = await getTwilio(organization); + const twilioInstance = await exports.getTwilio(organization); return await bulkRequest(phoneSids, async phoneNumberSid => twilioInstance.messaging .services(messagingServiceSid) @@ -751,7 +762,7 @@ async function deleteNumber(twilioInstance, phoneSid, phoneNumber) { * Delete all non-allocted phone numbers in an area code */ async function deleteNumbersInAreaCode(organization, areaCode) { - const twilioInstance = await getTwilio(organization); + const twilioInstance = await exports.getTwilio(organization); const numbersToDelete = await r .knex("owned_phone_number") .select("service_id", "phone_number") @@ -771,13 +782,13 @@ async function deleteNumbersInAreaCode(organization, areaCode) { } async function deleteMessagingService(organization, messagingServiceSid) { - const twilioInstance = await getTwilio(organization); + const twilioInstance = await exports.getTwilio(organization); console.log("Deleting messaging service", messagingServiceSid); return twilioInstance.messaging.services(messagingServiceSid).remove(); } async function clearMessagingServicePhones(organization, messagingServiceSid) { - const twilioInstance = await getTwilio(organization); + const twilioInstance = await exports.getTwilio(organization); console.log("Deleting phones from messaging service", messagingServiceSid); const phones = await twilioInstance.messaging @@ -820,5 +831,6 @@ export default { deleteNumbersInAreaCode, addNumbersToMessagingService, deleteMessagingService, - clearMessagingServicePhones + clearMessagingServicePhones, + getTwilio }; diff --git a/src/server/api/mutations/startCampaign.js b/src/server/api/mutations/startCampaign.js index a4bbb8593..87bbeee9b 100644 --- a/src/server/api/mutations/startCampaign.js +++ b/src/server/api/mutations/startCampaign.js @@ -2,7 +2,7 @@ import cacheableData from "../../models/cacheable_queries"; import { r } from "../../models"; import { accessRequired } from "../errors"; import { Notifications, sendUserNotification } from "../../notifications"; -import twilio from "../../../extensions/messaging_services/twilio"; +import * as twilio from "../../../extensions/messaging_services/twilio"; import { getConfig } from "../lib/config"; import { jobRunner } from "../../../extensions/job-runners"; import { Tasks } from "../../../workers/tasks"; From 2e19fe8692c2eb98fc17c8dc589e0660af2cc416 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sat, 13 Feb 2021 09:41:23 -0500 Subject: [PATCH 026/191] move messaging services to folders --- .../message-handlers/profanity-tagger.test.js | 2 +- src/api/organization.js | 11 +++++++++++ .../{fakeservice.js => fakeservice/index.js} | 4 ++-- .../messaging_services/{services.js => index.js} | 0 .../messaging_services/{nexmo.js => nexmo/index.js} | 8 ++++---- .../{twilio.js => twilio/index.js} | 12 ++++++------ src/server/api/mutations/buyPhoneNumbers.js | 2 +- src/server/api/organization.js | 2 +- src/workers/jobs.js | 2 +- src/workers/tasks.js | 2 +- 10 files changed, 28 insertions(+), 17 deletions(-) rename src/extensions/messaging_services/{fakeservice.js => fakeservice/index.js} (98%) rename src/extensions/messaging_services/{services.js => index.js} (100%) rename src/extensions/messaging_services/{nexmo.js => nexmo/index.js} (97%) rename src/extensions/messaging_services/{twilio.js => twilio/index.js} (98%) diff --git a/__test__/extensions/message-handlers/profanity-tagger.test.js b/__test__/extensions/message-handlers/profanity-tagger.test.js index ad3215958..e46c80ce9 100644 --- a/__test__/extensions/message-handlers/profanity-tagger.test.js +++ b/__test__/extensions/message-handlers/profanity-tagger.test.js @@ -1,5 +1,5 @@ import { r, cacheableData } from "../../../src/server/models"; -import serviceMap from "../../../src/extensions/messaging_services/services"; +import serviceMap from "../../../src/extensions/messaging_services"; import { available, DEFAULT_PROFANITY_REGEX_BASE64 diff --git a/src/api/organization.js b/src/api/organization.js index 225d00c75..08659baf7 100644 --- a/src/api/organization.js +++ b/src/api/organization.js @@ -41,6 +41,16 @@ export const schema = gql` unsetFeatures: [String] } + enum MessagingServiceType { + SMS + } + + type MessagingService { + name: String! + type: MessagingServiceType + config: JSON + } + input OrgSettingsInput { messageHandlers: [String] actionHandlers: [String] @@ -76,6 +86,7 @@ export const schema = gql` twilioAccountSid: String twilioAuthToken: String twilioMessageServiceSid: String + messagingService: MessagingService fullyConfigured: Boolean emailEnabled: Boolean phoneInventoryEnabled: Boolean! diff --git a/src/extensions/messaging_services/fakeservice.js b/src/extensions/messaging_services/fakeservice/index.js similarity index 98% rename from src/extensions/messaging_services/fakeservice.js rename to src/extensions/messaging_services/fakeservice/index.js index 27cb4118c..926e2bd15 100644 --- a/src/extensions/messaging_services/fakeservice.js +++ b/src/extensions/messaging_services/fakeservice/index.js @@ -1,10 +1,10 @@ -import { getLastMessage } from "./message-sending"; +import { getLastMessage } from "../message-sending"; import { Message, PendingMessagePart, r, cacheableData -} from "../../server/models"; +} from "../../../server/models"; import uuid from "uuid"; // This 'fakeservice' allows for fake-sending messages diff --git a/src/extensions/messaging_services/services.js b/src/extensions/messaging_services/index.js similarity index 100% rename from src/extensions/messaging_services/services.js rename to src/extensions/messaging_services/index.js diff --git a/src/extensions/messaging_services/nexmo.js b/src/extensions/messaging_services/nexmo/index.js similarity index 97% rename from src/extensions/messaging_services/nexmo.js rename to src/extensions/messaging_services/nexmo/index.js index 7a0bdae2a..64598e3c3 100644 --- a/src/extensions/messaging_services/nexmo.js +++ b/src/extensions/messaging_services/nexmo/index.js @@ -1,8 +1,8 @@ import Nexmo from "nexmo"; -import { getFormattedPhoneNumber } from "../../lib/phone-format"; -import { Message, PendingMessagePart } from "../../server/models"; -import { getLastMessage } from "./message-sending"; -import { log } from "../../lib"; +import { getFormattedPhoneNumber } from "../../../lib/phone-format"; +import { Message, PendingMessagePart } from "../../../server/models"; +import { getLastMessage } from "../message-sending"; +import { log } from "../../../lib"; // NEXMO error_codes: // If status is a number, then it will be the number diff --git a/src/extensions/messaging_services/twilio.js b/src/extensions/messaging_services/twilio/index.js similarity index 98% rename from src/extensions/messaging_services/twilio.js rename to src/extensions/messaging_services/twilio/index.js index c093eea43..297854bd6 100644 --- a/src/extensions/messaging_services/twilio.js +++ b/src/extensions/messaging_services/twilio/index.js @@ -2,18 +2,18 @@ import _ from "lodash"; import Twilio, { twiml } from "twilio"; import urlJoin from "url-join"; -import { log } from "../../lib"; -import { getFormattedPhoneNumber } from "../../lib/phone-format"; -import { getConfig } from "../../server/api/lib/config"; +import { log } from "../../../lib"; +import { getFormattedPhoneNumber } from "../../../lib/phone-format"; +import { getConfig } from "../../../server/api/lib/config"; import { cacheableData, Log, Message, PendingMessagePart, r -} from "../../server/models"; -import wrap from "../../server/wrap"; -import { saveNewIncomingMessage } from "./message-sending"; +} from "../../../server/models"; +import wrap from "../../../server/wrap"; +import { saveNewIncomingMessage } from "../message-sending"; // TWILIO error_codes: // > 1 (i.e. positive) error_codes are reserved for Twilio error codes diff --git a/src/server/api/mutations/buyPhoneNumbers.js b/src/server/api/mutations/buyPhoneNumbers.js index 2e7a63411..103161f15 100644 --- a/src/server/api/mutations/buyPhoneNumbers.js +++ b/src/server/api/mutations/buyPhoneNumbers.js @@ -1,4 +1,4 @@ -import serviceMap from "../../../extensions/messaging_services/services"; +import serviceMap from "../../../extensions/messaging_services"; import { accessRequired } from "../errors"; import { getConfig } from "../lib/config"; import { cacheableData } from "../../models"; diff --git a/src/server/api/organization.js b/src/server/api/organization.js index 7d5ecc618..8abd03df3 100644 --- a/src/server/api/organization.js +++ b/src/server/api/organization.js @@ -229,7 +229,7 @@ export const resolvers = { }; }, cacheable: (org, _, { user }) => - //quanery logic. levels are 0, 1, 2 + // quanery logic. levels are 0, 1, 2 r.redis ? (getConfig("REDIS_CONTACT_CACHE", org) ? 2 : 1) : 0, twilioAccountSid: async (organization, _, { user }) => { try { diff --git a/src/workers/jobs.js b/src/workers/jobs.js index 50f626ece..d7ca2c1ce 100644 --- a/src/workers/jobs.js +++ b/src/workers/jobs.js @@ -10,7 +10,7 @@ import { import telemetry from "../server/telemetry"; import { log, gunzip, zipToTimeZone, convertOffsetsToStrings } from "../lib"; import { sleep, updateJob } from "./lib"; -import serviceMap from "../extensions/messaging_services/services"; +import serviceMap from "../extensions/messaging_services"; import twilio from "../extensions/messaging_services/twilio"; import { getLastMessage, diff --git a/src/workers/tasks.js b/src/workers/tasks.js index da46e2ed1..9c26bf293 100644 --- a/src/workers/tasks.js +++ b/src/workers/tasks.js @@ -1,7 +1,7 @@ // Tasks are lightweight, fire-and-forget functions run in the background. // Unlike Jobs, tasks are not tracked in the database. // See src/extensions/job-runners/README.md for more details -import serviceMap from "../extensions/messaging_services/services"; +import serviceMap from "../extensions/messaging_services"; import * as ActionHandlers from "../extensions/action-handlers"; import { cacheableData } from "../server/models"; From 39bd7be6f37f198e10a2380e279293a4693d750d Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sat, 13 Feb 2021 18:55:48 -0500 Subject: [PATCH 027/191] Delegate config calls to message services --- .../messaging_services/twilio.test.js | 2 + __test__/server/api/campaign/campaign.test.js | 10 ++- .../cacheable_queries/organization.test.js | 66 +++++++++++++++++++ __test__/test_helpers.js | 27 ++++++++ .../messaging_services/service_map.js | 11 ++++ .../messaging_services/twilio/index.js | 39 ++++++++++- .../models/cacheable_queries/organization.js | 35 ++++++++-- 7 files changed, 180 insertions(+), 10 deletions(-) create mode 100644 __test__/server/models/cacheable_queries/organization.test.js create mode 100644 src/extensions/messaging_services/service_map.js diff --git a/__test__/extensions/messaging_services/twilio.test.js b/__test__/extensions/messaging_services/twilio.test.js index cc69d0b64..7c505673c 100644 --- a/__test__/extensions/messaging_services/twilio.test.js +++ b/__test__/extensions/messaging_services/twilio.test.js @@ -14,6 +14,7 @@ import { createScript, createTexter, createUser, + ensureOrganizationMessagingService, getCampaignContact, setTwilioAuth, setupTest, @@ -99,6 +100,7 @@ describe("twilio", () => { testOrganization2 = await createOrganization(testAdminUser, testInvite2); organizationId2 = testOrganization2.data.createOrganization.id; await setTwilioAuth(testAdminUser, testOrganization2); + await ensureOrganizationMessagingService(testOrganization, testCampaign); // use caching await cacheableData.organization.load(organizationId); diff --git a/__test__/server/api/campaign/campaign.test.js b/__test__/server/api/campaign/campaign.test.js index 54977f323..01ac44d4d 100644 --- a/__test__/server/api/campaign/campaign.test.js +++ b/__test__/server/api/campaign/campaign.test.js @@ -13,7 +13,7 @@ import { dataQuery as TexterTodoListQuery } from "../../../../src/containers/Tex import * as twilio from "../../../../src/extensions/messaging_services/twilio"; import { makeTree } from "../../../../src/lib"; import { getConfig } from "../../../../src/server/api/lib/config"; -import { r } from "../../../../src/server/models"; +import { cacheableData, r } from "../../../../src/server/models"; import { assignTexter, bulkSendMessages, @@ -27,6 +27,7 @@ import { createScript, createTexter, createUser, + ensureOrganizationMessagingService, getCampaignContact, runGql, saveCampaign, @@ -36,8 +37,6 @@ import { startCampaign } from "../../../test_helpers"; -jest.mock("../../../../src/extensions/messaging_services/twilio"); - let testAdminUser; let testInvite; let testOrganization; @@ -92,6 +91,7 @@ afterEach(async () => { queryLog = null; r.knex.removeListener("query", spokeDbListener); await cleanupTest(); + jest.restoreAllMocks(); if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); @@ -1213,6 +1213,10 @@ describe("all interaction steps fields travel round trip", () => { }); describe("useOwnMessagingService", async () => { + beforeEach(async () => { + await ensureOrganizationMessagingService(testOrganization, testCampaign); + }); + it("uses default messaging service when false", async () => { await startCampaign(testAdminUser, testCampaign); diff --git a/__test__/server/models/cacheable_queries/organization.test.js b/__test__/server/models/cacheable_queries/organization.test.js new file mode 100644 index 000000000..ec80c7c37 --- /dev/null +++ b/__test__/server/models/cacheable_queries/organization.test.js @@ -0,0 +1,66 @@ +import orgCache from "../../../../src/server/models/cacheable_queries/organization"; + +describe("cacheable_queries.organization", () => { + let twilioOrganization; + let fakeServiceOrganization; + beforeEach(async () => { + twilioOrganization = { + features: { + service: "twilio", + TWILIO_AUTH_TOKEN: "fake_twilio_auth_token", + TWILIO_ACCOUNT_SID: "fake_twilio_auth_account_sid", + TWILIO_MESSAGE_SERVICE_SID: "fake_twilio_message_service_sid" + } + }; + fakeServiceOrganization = { + features: { + service: "fakeservice" + } + }; + }); + + describe("getMessageServiceConfig", () => { + describe("when the message service has getConfigFromCache", () => { + it("returns a config", async () => { + const allegedConfig = await orgCache.getMessageServiceConfig( + twilioOrganization + ); + expect(allegedConfig).toEqual({ + authToken: twilioOrganization.features.TWILIO_AUTH_TOKEN, + accountSid: twilioOrganization.features.TWILIO_ACCOUNT_SID, + messageServiceSid: + twilioOrganization.features.TWILIO_MESSAGE_SERVICE_SID + }); + }); + }); + describe("when the message service doesn't have getConfigFromCache", () => { + it("does not return a config", async () => { + const allegedConfig = await orgCache.getMessageServiceConfig( + fakeServiceOrganization + ); + expect(allegedConfig).toBeNull(); + }); + }); + }); + + describe("getMessageServiceSid", () => { + describe("when the message service has getConfigFromCache", () => { + it("returns a config", async () => { + const allegedMessageServiceSid = await orgCache.getMessageServiceSid( + twilioOrganization + ); + expect(allegedMessageServiceSid).toEqual( + twilioOrganization.features.TWILIO_MESSAGE_SERVICE_SID + ); + }); + }); + describe("when the message service doesn't have getConfigFromCache", () => { + it("does not return a config", async () => { + const allegedConfig = await orgCache.getMessageServiceSid( + fakeServiceOrganization + ); + expect(allegedConfig).toBeNull(); + }); + }); + }); +}); diff --git a/__test__/test_helpers.js b/__test__/test_helpers.js index d52498c71..56b45ffcb 100644 --- a/__test__/test_helpers.js +++ b/__test__/test_helpers.js @@ -1,5 +1,6 @@ import _ from "lodash"; import { + cacheableData, createLoaders, createTables, dropTables, @@ -186,6 +187,32 @@ export async function createOrganization(user, invite) { return result; } +export const ensureOrganizationMessagingService = async ( + testOrganization, + testCampaign +) => { + const organization = testOrganization.data.createOrganization; + const existingFeatures = organization.features || {}; + const features = { + ...existingFeatures, + TWILIO_MESSAGE_SERVICE_SID: global.TWILIO_MESSAGE_SERVICE_SID, + service: "twilio" + }; + + await r + .knex("organization") + .where({ id: organization.id }) + .update({ features: JSON.stringify(features) }); + organization.feature = features; + cacheableData.organization.clear(organization.id); + + await r + .knex("campaign") + .where({ id: testCampaign.id }) + .update({ organization_id: organization.id }); + cacheableData.campaign.clear(testCampaign.id); +}; + export async function setTwilioAuth(user, organization) { const rootValue = {}; const accountSid = "test_twilio_account_sid"; diff --git a/src/extensions/messaging_services/service_map.js b/src/extensions/messaging_services/service_map.js new file mode 100644 index 000000000..d62bec4e7 --- /dev/null +++ b/src/extensions/messaging_services/service_map.js @@ -0,0 +1,11 @@ +import nexmo from "./nexmo"; +import * as twilio from "./twilio"; +import fakeservice from "./fakeservice"; + +const serviceMap = { + nexmo, + twilio, + fakeservice +}; + +export default serviceMap; diff --git a/src/extensions/messaging_services/twilio/index.js b/src/extensions/messaging_services/twilio/index.js index 297854bd6..0324e00bd 100644 --- a/src/extensions/messaging_services/twilio/index.js +++ b/src/extensions/messaging_services/twilio/index.js @@ -4,7 +4,7 @@ import Twilio, { twiml } from "twilio"; import urlJoin from "url-join"; import { log } from "../../../lib"; import { getFormattedPhoneNumber } from "../../../lib/phone-format"; -import { getConfig } from "../../../server/api/lib/config"; +import { getConfig, hasConfig } from "../../../server/api/lib/config"; import { cacheableData, Log, @@ -14,6 +14,7 @@ import { } from "../../../server/models"; import wrap from "../../../server/wrap"; import { saveNewIncomingMessage } from "../message-sending"; +import { symmetricDecrypt } from "../../../server/api/lib/crypto"; // TWILIO error_codes: // > 1 (i.e. positive) error_codes are reserved for Twilio error codes @@ -816,6 +817,38 @@ async function clearMessagingServicePhones(organization, messagingServiceSid) { } } +export const getConfigFromCache = async organization => { + const hasOrgToken = hasConfig("TWILIO_AUTH_TOKEN_ENCRYPTED", organization); + // Note, allows unencrypted auth tokens to be (manually) stored in the db + // @todo: decide if this is necessary, or if UI/envars is sufficient. + const authToken = hasOrgToken + ? symmetricDecrypt(getConfig("TWILIO_AUTH_TOKEN_ENCRYPTED", organization)) + : getConfig("TWILIO_AUTH_TOKEN", organization); + const accountSid = hasConfig("TWILIO_ACCOUNT_SID", organization) + ? getConfig("TWILIO_ACCOUNT_SID", organization) + : // Check old TWILIO_API_KEY variable for backwards compatibility. + getConfig("TWILIO_API_KEY", organization); + + const messageServiceSid = getConfig( + "TWILIO_MESSAGE_SERVICE_SID", + organization + ); + + return { authToken, accountSid, messageServiceSid }; +}; + +export const getMessageServiceSidFromCache = async ( + organization, + contact, + messageText +) => { + // Note organization won't always be available, so we'll need to conditionally look it up based on contact + if (messageText && /twilioapitest/.test(messageText)) { + return "fakeSid_MK123"; + } + return getConfig("TWILIO_MESSAGE_SERVICE_SID", organization); +}; + export default { syncMessagePartProcessing: !!process.env.JOBS_SAME_PROCESS, addServerEndpoints, @@ -832,5 +865,7 @@ export default { addNumbersToMessagingService, deleteMessagingService, clearMessagingServicePhones, - getTwilio + getTwilio, + getConfigFromCache, + getMessageServiceSidFromCache }; diff --git a/src/server/models/cacheable_queries/organization.js b/src/server/models/cacheable_queries/organization.js index d06171f9f..2fc9c9df5 100644 --- a/src/server/models/cacheable_queries/organization.js +++ b/src/server/models/cacheable_queries/organization.js @@ -1,21 +1,46 @@ import { r } from "../../models"; import { getConfig, hasConfig } from "../../api/lib/config"; import { symmetricDecrypt } from "../../api/lib/crypto"; +import serviceMap from "../../../extensions/messaging_services/service_map"; const cacheKey = orgId => `${process.env.CACHE_PREFIX || ""}org-${orgId}`; +const getMessageServiceFromCache = organization => + getConfig("service", organization) || getConfig("DEFAULT_SERVICE"); + +const tryGetFunctionFromOrganizationMessageService = ( + organization, + functionName +) => { + const messageServiceName = getMessageServiceFromCache(organization); + const messageService = serviceMap[messageServiceName]; + const fn = messageService[functionName]; + return fn && typeof fn === "function" ? fn : null; +}; + const organizationCache = { clear: async id => { if (r.redis) { await r.redis.delAsync(cacheKey(id)); } }, + getMessageService: getMessageServiceFromCache, + getMessageServiceConfig: async organization => { + const getConfigFromCache = tryGetFunctionFromOrganizationMessageService( + organization, + "getConfigFromCache" + ); + return getConfigFromCache && (await getConfigFromCache(organization)); + }, getMessageServiceSid: async (organization, contact, messageText) => { - // Note organization won't always be available, so we'll need to conditionally look it up based on contact - if (messageText && /twilioapitest/.test(messageText)) { - return "fakeSid_MK123"; - } - return getConfig("TWILIO_MESSAGE_SERVICE_SID", organization); + const getMessageServiceSidFromCache = tryGetFunctionFromOrganizationMessageService( + organization, + "getMessageServiceSidFromCache" + ); + return ( + getMessageServiceSidFromCache && + (await getMessageServiceSidFromCache(organization, contact, messageText)) + ); }, getTwilioAuth: async organization => { const hasOrgToken = hasConfig("TWILIO_AUTH_TOKEN_ENCRYPTED", organization); From cc53f25403bf1008a991741a612cba7d75674def Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sun, 14 Feb 2021 14:27:25 -0500 Subject: [PATCH 028/191] some refactoring --- .../messaging_services/twilio.test.js | 15 +++++++++----- __test__/server/api/campaign/campaign.test.js | 7 +++++-- __test__/test_helpers.js | 20 ++++++++++--------- .../messaging_services/service_map.js | 9 +++++++++ .../models/cacheable_queries/organization.js | 6 ++---- 5 files changed, 37 insertions(+), 20 deletions(-) diff --git a/__test__/extensions/messaging_services/twilio.test.js b/__test__/extensions/messaging_services/twilio.test.js index 7c505673c..c7a2b64f9 100644 --- a/__test__/extensions/messaging_services/twilio.test.js +++ b/__test__/extensions/messaging_services/twilio.test.js @@ -14,7 +14,7 @@ import { createScript, createTexter, createUser, - ensureOrganizationMessagingService, + ensureOrganizationTwilioWithMessagingService, getCampaignContact, setTwilioAuth, setupTest, @@ -100,7 +100,8 @@ describe("twilio", () => { testOrganization2 = await createOrganization(testAdminUser, testInvite2); organizationId2 = testOrganization2.data.createOrganization.id; await setTwilioAuth(testAdminUser, testOrganization2); - await ensureOrganizationMessagingService(testOrganization, testCampaign); + await ensureOrganizationTwilioWithMessagingService(testOrganization); + await ensureOrganizationTwilioWithMessagingService(testOrganization2); // use caching await cacheableData.organization.load(organizationId); @@ -448,13 +449,17 @@ describe("twilio", () => { it("orgs should have separate twilio credentials", async () => { const org1 = await cacheableData.organization.load(organizationId); - const org1Auth = await cacheableData.organization.getTwilioAuth(org1); + const org1Auth = await cacheableData.organization.getMessageServiceConfig( + org1 + ); expect(org1Auth.authToken).toBeUndefined(); expect(org1Auth.accountSid).toBeUndefined(); const org2 = await cacheableData.organization.load(organizationId2); - const org2Auth = await cacheableData.organization.getTwilioAuth(org2); - expect(org2Auth.authToken).toBe("test_twlio_auth_token"); + const org2Auth = await cacheableData.organization.getMessageServiceConfig( + org2 + ); + expect(org2Auth.authToken).toBe("test_twilio_auth_token"); expect(org2Auth.accountSid).toBe("test_twilio_account_sid"); }); diff --git a/__test__/server/api/campaign/campaign.test.js b/__test__/server/api/campaign/campaign.test.js index 01ac44d4d..53ed01b46 100644 --- a/__test__/server/api/campaign/campaign.test.js +++ b/__test__/server/api/campaign/campaign.test.js @@ -27,7 +27,7 @@ import { createScript, createTexter, createUser, - ensureOrganizationMessagingService, + ensureOrganizationTwilioWithMessagingService, getCampaignContact, runGql, saveCampaign, @@ -1214,7 +1214,10 @@ describe("all interaction steps fields travel round trip", () => { describe("useOwnMessagingService", async () => { beforeEach(async () => { - await ensureOrganizationMessagingService(testOrganization, testCampaign); + await ensureOrganizationTwilioWithMessagingService( + testOrganization, + testCampaign + ); }); it("uses default messaging service when false", async () => { diff --git a/__test__/test_helpers.js b/__test__/test_helpers.js index 56b45ffcb..3ac8028d2 100644 --- a/__test__/test_helpers.js +++ b/__test__/test_helpers.js @@ -187,9 +187,9 @@ export async function createOrganization(user, invite) { return result; } -export const ensureOrganizationMessagingService = async ( +export const ensureOrganizationTwilioWithMessagingService = async ( testOrganization, - testCampaign + testCampaign = null ) => { const organization = testOrganization.data.createOrganization; const existingFeatures = organization.features || {}; @@ -204,13 +204,15 @@ export const ensureOrganizationMessagingService = async ( .where({ id: organization.id }) .update({ features: JSON.stringify(features) }); organization.feature = features; - cacheableData.organization.clear(organization.id); - - await r - .knex("campaign") - .where({ id: testCampaign.id }) - .update({ organization_id: organization.id }); - cacheableData.campaign.clear(testCampaign.id); + await cacheableData.organization.clear(organization.id); + + if (testCampaign) { + await r + .knex("campaign") + .where({ id: testCampaign.id }) + .update({ organization_id: organization.id }); + await cacheableData.campaign.clear(testCampaign.id); + } }; export async function setTwilioAuth(user, organization) { diff --git a/src/extensions/messaging_services/service_map.js b/src/extensions/messaging_services/service_map.js index d62bec4e7..d0b809eb8 100644 --- a/src/extensions/messaging_services/service_map.js +++ b/src/extensions/messaging_services/service_map.js @@ -8,4 +8,13 @@ const serviceMap = { fakeservice }; +export const tryGetFunctionFromService = (serviceName, functionName) => { + const messageService = serviceMap[serviceName]; + if (!messageService) { + throw new Error(`${serviceName} is not a message service`); + } + const fn = messageService[functionName]; + return fn && typeof fn === "function" ? fn : null; +}; + export default serviceMap; diff --git a/src/server/models/cacheable_queries/organization.js b/src/server/models/cacheable_queries/organization.js index 2fc9c9df5..f2a754062 100644 --- a/src/server/models/cacheable_queries/organization.js +++ b/src/server/models/cacheable_queries/organization.js @@ -1,7 +1,7 @@ import { r } from "../../models"; import { getConfig, hasConfig } from "../../api/lib/config"; import { symmetricDecrypt } from "../../api/lib/crypto"; -import serviceMap from "../../../extensions/messaging_services/service_map"; +import { tryGetFunctionFromService } from "../../../extensions/messaging_services/service_map"; const cacheKey = orgId => `${process.env.CACHE_PREFIX || ""}org-${orgId}`; @@ -13,9 +13,7 @@ const tryGetFunctionFromOrganizationMessageService = ( functionName ) => { const messageServiceName = getMessageServiceFromCache(organization); - const messageService = serviceMap[messageServiceName]; - const fn = messageService[functionName]; - return fn && typeof fn === "function" ? fn : null; + return tryGetFunctionFromService(messageServiceName, functionName); }; const organizationCache = { From 74e35b1fe29deb49ab500e0fdb2e7a550fab593a Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sun, 14 Feb 2021 14:28:52 -0500 Subject: [PATCH 029/191] introducing ... updateMessageServiceConfig --- .../updateMessageServiceConfig.test.js | 127 ++++++++++++++++++ src/api/schema.js | 5 + src/containers/Settings.jsx | 70 ++++++---- src/extensions/messaging_services/index.js | 39 +++++- .../messaging_services/twilio/index.js | 56 +++++++- src/server/api/mutations/index.js | 44 ++---- .../mutations/updateMessageServiceConfig.js | 57 ++++++++ src/server/api/schema.js | 4 +- 8 files changed, 334 insertions(+), 68 deletions(-) create mode 100644 __test__/server/api/mutations/updateMessageServiceConfig.test.js create mode 100644 src/server/api/mutations/updateMessageServiceConfig.js diff --git a/__test__/server/api/mutations/updateMessageServiceConfig.test.js b/__test__/server/api/mutations/updateMessageServiceConfig.test.js new file mode 100644 index 000000000..5c5160809 --- /dev/null +++ b/__test__/server/api/mutations/updateMessageServiceConfig.test.js @@ -0,0 +1,127 @@ +import { r } from "../../../../src/server/models"; +import { + cleanupTest, + createInvite, + createOrganization, + createUser, + runGql, + setupTest, + ensureOrganizationTwilioWithMessagingService +} from "../../../test_helpers"; +import { updateMessageServiceConfigGql } from "../../../../src/containers/Settings"; +import * as twilio from "../../../../src/extensions/messaging_services/twilio"; +import * as messagingServices from "../../../../src/extensions/messaging_services"; +import * as serviceMap from "../../../../src/extensions/messaging_services/service_map"; + +describe("updateMessageServiceConfig", () => { + beforeEach(async () => { + await setupTest(); + }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); + afterEach(async () => { + await cleanupTest(); + if (r.redis) r.redis.flushdb(); + jest.restoreAllMocks(); + }); + + let user; + let organization; + let vars; + beforeEach(async () => { + user = await createUser(); + const invite = await createInvite(); + const createOrganizationResult = await createOrganization(user, invite); + organization = createOrganizationResult.data.createOrganization; + await ensureOrganizationTwilioWithMessagingService( + createOrganizationResult + ); + + jest + .spyOn(twilio, "updateConfig") + .mockResolvedValue({ fake_config: "fake_config_value" }); + + vars = { + organizationId: organization.id, + messageServiceName: "twilio", + config: JSON.stringify({ fake_config: "fake_config_value" }) + }; + }); + + it("delegates to message service's updateConfig", async () => { + const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); + expect(gqlResult.data.updateMessageServiceConfig).toEqual({ + fake_config: "fake_config_value" + }); + expect(twilio.updateConfig.mock.calls).toEqual([ + [expect.objectContaining({ id: 1 }), { fake_config: "fake_config_value" }] + ]); + }); + + describe("when it's not the configured message service name", () => { + beforeEach(async () => { + vars.messageServiceName = "this will never be a message service name"; + }); + + it("returns an error", async () => { + const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); + expect(gqlResult.errors[0].message).toEqual( + "Can't configure this will never be a message service name. It's not the configured message service" + ); + expect(twilio.updateConfig).not.toHaveBeenCalled(); + }); + }); + + describe("when it's not a valid message service", () => { + beforeEach(async () => { + jest.spyOn(messagingServices, "getService").mockReturnValue(null); + }); + + it("returns an error", async () => { + const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); + expect(gqlResult.errors[0].message).toEqual( + "twilio is not a valid message service" + ); + expect(twilio.updateConfig).not.toHaveBeenCalled(); + }); + }); + + describe("when the service is not configurable", () => { + beforeEach(async () => { + jest.spyOn(serviceMap, "tryGetFunctionFromService").mockReturnValue(null); + }); + + it("returns an error", async () => { + const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); + expect(gqlResult.errors[0].message).toEqual( + "twilio does not support configuration" + ); + expect(twilio.updateConfig).not.toHaveBeenCalled(); + }); + }); + + describe("when the pass config is not valid JSON", () => { + beforeEach(async () => { + vars.config = "not JSON"; + }); + + it("returns an error", async () => { + const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); + expect(gqlResult.errors[0].message).toEqual("Config is not valid JSON"); + expect(twilio.updateConfig).not.toHaveBeenCalled(); + }); + }); + + describe("when the service config function throw an exception", () => { + beforeEach(async () => { + jest.spyOn(twilio, "updateConfig").mockImplementation(() => { + throw new Error("OH NO!"); + }); + }); + + it("returns an error", async () => { + const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); + expect(gqlResult.errors[0].message).toEqual( + "Error updating config for twilio: Error: OH NO!" + ); + }); + }); +}); diff --git a/src/api/schema.js b/src/api/schema.js index 161ccb251..fbae621c9 100644 --- a/src/api/schema.js +++ b/src/api/schema.js @@ -286,6 +286,11 @@ const rootSchema = gql` organizationId: String! optOutMessage: String! ): Organization + updateMessageServiceConfig( + organizationId: String! + messageServiceName: String! + config: JSON! + ): JSON updateTwilioAuth( organizationId: String! twilioAccountSid: String diff --git a/src/containers/Settings.jsx b/src/containers/Settings.jsx index 69560c890..ae3f4548a 100644 --- a/src/containers/Settings.jsx +++ b/src/containers/Settings.jsx @@ -247,7 +247,7 @@ class Settings extends React.Component { /> Changing the Account SID or Messaging Service SID will break any @@ -345,8 +345,8 @@ class Settings extends React.Component {

DEBUG Zone

@@ -490,6 +490,41 @@ export const editOrganizationGql = gql` } `; +export const updateMessageServiceConfigGql = gql` + mutation updateMessageServiceConfig( + $organizationId: String! + $messageServiceName: String! + $config: JSON! + ) { + updateMessageServiceConfig( + organizationId: $organizationId + messageServiceName: $messageServiceName + config: $config + ) + } +`; + +export const updateTwilioAuthGql = gql` + mutation updateTwilioAuth( + $twilioAccountSid: String + $twilioAuthToken: String + $twilioMessageServiceSid: String + $organizationId: String! + ) { + updateTwilioAuth( + twilioAccountSid: $twilioAccountSid + twilioAuthToken: $twilioAuthToken + twilioMessageServiceSid: $twilioMessageServiceSid + organizationId: $organizationId + ) { + id + twilioAccountSid + twilioAuthToken + twilioMessageServiceSid + } + } +`; + const mutations = { editOrganization: ownProps => organizationChanges => ({ mutation: editOrganizationGql, @@ -566,26 +601,7 @@ const mutations = { } }), updateTwilioAuth: ownProps => (accountSid, authToken, messageServiceSid) => ({ - mutation: gql` - mutation updateTwilioAuth( - $twilioAccountSid: String - $twilioAuthToken: String - $twilioMessageServiceSid: String - $organizationId: String! - ) { - updateTwilioAuth( - twilioAccountSid: $twilioAccountSid - twilioAuthToken: $twilioAuthToken - twilioMessageServiceSid: $twilioMessageServiceSid - organizationId: $organizationId - ) { - id - twilioAccountSid - twilioAuthToken - twilioMessageServiceSid - } - } - `, + mutation: updateTwilioAuthGql, variables: { organizationId: ownProps.params.organizationId, twilioAccountSid: accountSid, diff --git a/src/extensions/messaging_services/index.js b/src/extensions/messaging_services/index.js index 7e607f37c..91a1a3a55 100644 --- a/src/extensions/messaging_services/index.js +++ b/src/extensions/messaging_services/index.js @@ -1,6 +1,7 @@ -import nexmo from "./nexmo"; -import twilio from "./twilio"; -import fakeservice from "./fakeservice"; +import serviceMap from "./service_map"; +import orgCache from "../../server/models/cacheable_queries/organization"; + +export { tryGetFunctionFromService } from "./service_map"; // Each service needs the following api points: // async sendMessage(message, contact, trx, organization) -> void @@ -15,10 +16,34 @@ import fakeservice from "./fakeservice"; // async buyNumbersInAreaCode(organization, areaCode, limit, opts) -> Count of successfully purchased numbers // where the `opts` parameter can include service specific options -const serviceMap = { - nexmo, - twilio, - fakeservice +export const getService = serviceName => serviceMap[serviceName]; + +export const getServiceFromOrganization = organization => + serviceMap[orgCache.getMessageService(organization)]; + +export const fullyConfigured = async organization => { + const messagingService = getServiceFromOrganization(organization); + const fn = messagingService.fullyConfigured; + if (!fn || typeof fn !== "function") { + return true; + } + + return fn(); +}; + +export const createMessagingService = (organization, friendlyName) => { + const serviceName = orgCache.getMessageService(organization); + let service; + if (serviceName === "twilio") { + service = serviceMap.twilio; + } else if (service === "signalwire") { + // service = signalwire; + } + + if (service) { + return service.createMessagingService(organization, friendlyName); + } + return null; }; export default serviceMap; diff --git a/src/extensions/messaging_services/twilio/index.js b/src/extensions/messaging_services/twilio/index.js index 0324e00bd..95a58b876 100644 --- a/src/extensions/messaging_services/twilio/index.js +++ b/src/extensions/messaging_services/twilio/index.js @@ -4,17 +4,26 @@ import Twilio, { twiml } from "twilio"; import urlJoin from "url-join"; import { log } from "../../../lib"; import { getFormattedPhoneNumber } from "../../../lib/phone-format"; -import { getConfig, hasConfig } from "../../../server/api/lib/config"; +import { + getFeatures, + getConfig, + hasConfig +} from "../../../server/api/lib/config"; import { cacheableData, Log, Message, PendingMessagePart, + Organization, r } from "../../../server/models"; import wrap from "../../../server/wrap"; import { saveNewIncomingMessage } from "../message-sending"; -import { symmetricDecrypt } from "../../../server/api/lib/crypto"; +import { + symmetricDecrypt, + symmetricEncrypt +} from "../../../server/api/lib/crypto"; +import organizationCache from "../../../server/models/cacheable_queries/organization"; // TWILIO error_codes: // > 1 (i.e. positive) error_codes are reserved for Twilio error codes @@ -849,6 +858,46 @@ export const getMessageServiceSidFromCache = async ( return getConfig("TWILIO_MESSAGE_SERVICE_SID", organization); }; +export const updateConfig = async (organization, config) => { + const featuresJSON = getFeatures(organization); + const { twilioAccountSid, twilioAuthToken, twilioMessageServiceSid } = config; + + if (!twilioAccountSid || !twilioMessageServiceSid) { + throw new Error( + "twilioAccountSid and twilioMessageServiceSid are required" + ); + } + + featuresJSON.TWILIO_ACCOUNT_SID = twilioAccountSid.substr(0, 64); + + // TODO(lperson) is twilioAuthToken required? + featuresJSON.TWILIO_AUTH_TOKEN_ENCRYPTED = twilioAuthToken + ? symmetricEncrypt(twilioAuthToken).substr(0, 256) + : twilioAuthToken; + featuresJSON.TWILIO_MESSAGE_SERVICE_SID = twilioMessageServiceSid.substr( + 0, + 64 + ); + + const dbOrganization = Organization.get(organization.id); + dbOrganization.features = JSON.stringify(featuresJSON); + + try { + if (twilioAuthToken && global.TEST_ENVIRONMENT !== "1") { + // Make sure Twilio credentials work. + const twilio = Twilio(twilioAccountSid, twilioAuthToken); // eslint-disable-line new-cap + await twilio.api.accounts.list(); + } + } catch (err) { + throw new Error("Invalid Twilio credentials"); + } + + await dbOrganization.save(); + await cacheableData.organization.clear(organization.id); + + return organizationCache.getMessageServiceConfig(organization); +}; + export default { syncMessagePartProcessing: !!process.env.JOBS_SAME_PROCESS, addServerEndpoints, @@ -867,5 +916,6 @@ export default { clearMessagingServicePhones, getTwilio, getConfigFromCache, - getMessageServiceSidFromCache + getMessageServiceSidFromCache, + updateConfig }; diff --git a/src/server/api/mutations/index.js b/src/server/api/mutations/index.js index 8d96ca8ca..f75796278 100644 --- a/src/server/api/mutations/index.js +++ b/src/server/api/mutations/index.js @@ -1,30 +1,14 @@ -import { bulkSendMessages } from "./bulkSendMessages"; -import { buyPhoneNumbers, deletePhoneNumbers } from "./buyPhoneNumbers"; -import { editOrganization } from "./editOrganization"; -import { findNewCampaignContact } from "./findNewCampaignContact"; -import { joinOrganization } from "./joinOrganization"; -import { releaseContacts } from "./releaseContacts"; -import { sendMessage } from "./sendMessage"; -import { startCampaign } from "./startCampaign"; -import { updateContactTags } from "./updateContactTags"; -import { updateQuestionResponses } from "./updateQuestionResponses"; -import { releaseCampaignNumbers } from "./releaseCampaignNumbers"; -import { clearCachedOrgAndExtensionCaches } from "./clearCachedOrgAndExtensionCaches"; -import { updateFeedback } from "./updateFeedback"; - -export { - bulkSendMessages, - buyPhoneNumbers, - deletePhoneNumbers, - editOrganization, - findNewCampaignContact, - joinOrganization, - releaseContacts, - sendMessage, - startCampaign, - updateContactTags, - updateQuestionResponses, - releaseCampaignNumbers, - clearCachedOrgAndExtensionCaches, - updateFeedback -}; +export { bulkSendMessages } from "./bulkSendMessages"; +export { buyPhoneNumbers, deletePhoneNumbers } from "./buyPhoneNumbers"; +export { editOrganization } from "./editOrganization"; +export { findNewCampaignContact } from "./findNewCampaignContact"; +export { joinOrganization } from "./joinOrganization"; +export { releaseContacts } from "./releaseContacts"; +export { sendMessage } from "./sendMessage"; +export { startCampaign } from "./startCampaign"; +export { updateContactTags } from "./updateContactTags"; +export { updateQuestionResponses } from "./updateQuestionResponses"; +export { releaseCampaignNumbers } from "./releaseCampaignNumbers"; +export { clearCachedOrgAndExtensionCaches } from "./clearCachedOrgAndExtensionCaches"; +export { updateFeedback } from "./updateFeedback"; +export { updateMessageServiceConfig } from "./updateMessageServiceConfig"; diff --git a/src/server/api/mutations/updateMessageServiceConfig.js b/src/server/api/mutations/updateMessageServiceConfig.js new file mode 100644 index 000000000..2c0a6fa90 --- /dev/null +++ b/src/server/api/mutations/updateMessageServiceConfig.js @@ -0,0 +1,57 @@ +import orgCache from "../../models/cacheable_queries/organization"; +import { accessRequired } from "../errors"; +import { GraphQLError } from "graphql/error"; +import { + getService, + tryGetFunctionFromService +} from "../../../extensions/messaging_services"; + +// TODO(lperson) this should allow the message service +// to modify only its own object +export const updateMessageServiceConfig = async ( + _, + { organizationId, messageServiceName, config }, + { user } +) => { + await accessRequired(user, organizationId, "OWNER"); + const organization = await orgCache.load(organizationId); + const configuredMessageServiceName = orgCache.getMessageService(organization); + if (configuredMessageServiceName !== messageServiceName) { + throw new GraphQLError( + `Can't configure ${messageServiceName}. It's not the configured message service` + ); + } + + const service = getService(messageServiceName); + if (!service) { + throw new GraphQLError( + `${messageServiceName} is not a valid message service` + ); + } + + const serviceConfigFunction = tryGetFunctionFromService( + messageServiceName, + "updateConfig" + ); + if (!serviceConfigFunction) { + throw new GraphQLError( + `${messageServiceName} does not support configuration` + ); + } + + let configObject; + try { + configObject = JSON.parse(config); + } catch (caught) { + throw new GraphQLError("Config is not valid JSON"); + } + + try { + return serviceConfigFunction(organization, configObject); + } catch (caught) { + const message = `Error updating config for ${messageServiceName}: ${caught}`; + // eslint-disable-next-line no-console + console.error(message); + throw new GraphQLError(message); + } +}; diff --git a/src/server/api/schema.js b/src/server/api/schema.js index 0096a691c..1540b5f20 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -65,7 +65,8 @@ import { updateQuestionResponses, releaseCampaignNumbers, clearCachedOrgAndExtensionCaches, - updateFeedback + updateFeedback, + updateMessageServiceConfig } from "./mutations"; import { jobRunner } from "../../extensions/job-runners"; @@ -499,6 +500,7 @@ const rootMutations = { startCampaign, releaseCampaignNumbers, clearCachedOrgAndExtensionCaches, + updateMessageServiceConfig, userAgreeTerms: async (_, { userId }, { user }) => { // We ignore userId: you can only agree to terms for yourself await r From 9be4071d0dd920ad0517622eae6dc2e726822ebe Mon Sep 17 00:00:00 2001 From: Larry Person Date: Mon, 15 Feb 2021 12:45:10 -0500 Subject: [PATCH 030/191] work proceeds apace, a cornucopia of tests --- .../messaging_services/service_map.test.js | 65 +++++++++ .../updateMessageServiceConfig.test.js | 80 ++++++++--- .../cacheable_queries/organization.test.js | 125 +++++++++++++----- src/extensions/messaging_services/index.js | 14 +- .../messaging_services/service_map.js | 6 +- .../messaging_services/twilio/index.js | 93 +++++++------ .../mutations/updateMessageServiceConfig.js | 25 +++- .../models/cacheable_queries/organization.js | 34 +++-- 8 files changed, 325 insertions(+), 117 deletions(-) create mode 100644 __test__/extensions/messaging_services/service_map.test.js diff --git a/__test__/extensions/messaging_services/service_map.test.js b/__test__/extensions/messaging_services/service_map.test.js new file mode 100644 index 000000000..80f1ccbdb --- /dev/null +++ b/__test__/extensions/messaging_services/service_map.test.js @@ -0,0 +1,65 @@ +import * as serviceMap from "../../../src/extensions/messaging_services/service_map"; + +describe("service_map", () => { + let serviceWith; + let serviceWithout; + let fakeServiceMap; + beforeEach(async () => { + serviceWith = { + getServiceConfig: jest.fn().mockImplementation(() => "fake_config"), + getMessageServiceSid: jest.fn() + }; + + serviceWithout = {}; + + fakeServiceMap = { serviceWith, serviceWithout }; + + jest + .spyOn(serviceMap, "getService") + .mockImplementation(serviceName => fakeServiceMap[serviceName]); + }); + + describe("getConfigKey", () => { + it("returns the correct config key", async () => { + const configKey = serviceMap.getConfigKey("fake_service_name"); + expect(configKey).toEqual("message_service_fake_service_name"); + }); + }); + + describe("tryGetFunctionFromService", () => { + it("returns the function", async () => { + const fn = serviceMap.tryGetFunctionFromService( + "serviceWith", + "getServiceConfig" + ); + expect(fn).not.toBeNull(); + expect(typeof fn).toEqual("function"); + const fnReturn = fn(); + expect(fnReturn).toEqual("fake_config"); + }); + describe("when the service doesn't exist", () => { + it("throws an exception", async () => { + let error; + try { + serviceMap.tryGetFunctionFromService( + "not_a_service", + "getServiceConfig" + ); + } catch (caught) { + error = caught; + } + expect(error).toBeDefined(); + expect(error.message).toEqual("not_a_service is not a message service"); + }); + }); + describe("when the service doesn't have the function", () => { + it("returns null", async () => { + const fn = serviceMap.tryGetFunctionFromService( + "serviceWithout", + "getServiceConfig" + ); + expect(fn).toBeNull(); + }); + }); + }); +}); diff --git a/__test__/server/api/mutations/updateMessageServiceConfig.test.js b/__test__/server/api/mutations/updateMessageServiceConfig.test.js index 5c5160809..828892e27 100644 --- a/__test__/server/api/mutations/updateMessageServiceConfig.test.js +++ b/__test__/server/api/mutations/updateMessageServiceConfig.test.js @@ -1,17 +1,18 @@ -import { r } from "../../../../src/server/models"; +import { updateMessageServiceConfigGql } from "../../../../src/containers/Settings"; +// import * as messagingServices from "../../../../src/extensions/messaging_services"; +import * as serviceMap from "../../../../src/extensions/messaging_services/service_map"; +import * as twilio from "../../../../src/extensions/messaging_services/twilio"; +import { r, Organization } from "../../../../src/server/models"; +import orgCache from "../../../../src/server/models/cacheable_queries/organization"; import { cleanupTest, createInvite, createOrganization, createUser, + ensureOrganizationTwilioWithMessagingService, runGql, - setupTest, - ensureOrganizationTwilioWithMessagingService + setupTest } from "../../../test_helpers"; -import { updateMessageServiceConfigGql } from "../../../../src/containers/Settings"; -import * as twilio from "../../../../src/extensions/messaging_services/twilio"; -import * as messagingServices from "../../../../src/extensions/messaging_services"; -import * as serviceMap from "../../../../src/extensions/messaging_services/service_map"; describe("updateMessageServiceConfig", () => { beforeEach(async () => { @@ -26,6 +27,7 @@ describe("updateMessageServiceConfig", () => { let user; let organization; let vars; + let newConfig; beforeEach(async () => { user = await createUser(); const invite = await createInvite(); @@ -35,25 +37,34 @@ describe("updateMessageServiceConfig", () => { createOrganizationResult ); + newConfig = { fake_config: "fake_config_value" }; + jest.spyOn(twilio, "updateConfig").mockResolvedValue(newConfig); + jest - .spyOn(twilio, "updateConfig") - .mockResolvedValue({ fake_config: "fake_config_value" }); + .spyOn(orgCache, "getMessageServiceConfig") + .mockResolvedValue(newConfig); vars = { organizationId: organization.id, messageServiceName: "twilio", - config: JSON.stringify({ fake_config: "fake_config_value" }) + config: JSON.stringify(newConfig) }; }); it("delegates to message service's updateConfig", async () => { const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); - expect(gqlResult.data.updateMessageServiceConfig).toEqual({ - fake_config: "fake_config_value" - }); - expect(twilio.updateConfig.mock.calls).toEqual([ - [expect.objectContaining({ id: 1 }), { fake_config: "fake_config_value" }] + expect(twilio.updateConfig.mock.calls).toEqual([[undefined, newConfig]]); + expect(orgCache.getMessageServiceConfig.mock.calls).toEqual([ + [ + expect.objectContaining({ + id: 1, + feature: expect.objectContaining({ + message_service_twilio: newConfig + }) + }) + ] ]); + expect(gqlResult.data.updateMessageServiceConfig).toEqual(newConfig); }); describe("when it's not the configured message service name", () => { @@ -72,7 +83,7 @@ describe("updateMessageServiceConfig", () => { describe("when it's not a valid message service", () => { beforeEach(async () => { - jest.spyOn(messagingServices, "getService").mockReturnValue(null); + jest.spyOn(serviceMap, "getService").mockReturnValue(null); }); it("returns an error", async () => { @@ -110,7 +121,7 @@ describe("updateMessageServiceConfig", () => { }); }); - describe("when the service config function throw an exception", () => { + describe("when the service config function throws an exception", () => { beforeEach(async () => { jest.spyOn(twilio, "updateConfig").mockImplementation(() => { throw new Error("OH NO!"); @@ -124,4 +135,39 @@ describe("updateMessageServiceConfig", () => { ); }); }); + + describe("when there is an existing config", () => { + let fakeExistingConfig; + beforeEach(async () => { + const configKey = serviceMap.getConfigKey("twilio"); + fakeExistingConfig = { + fake_existing_config_key: "fake_existing_config_value" + }; + const dbOrganization = await Organization.get(organization.id); + const newFeatures = JSON.stringify({ + ...JSON.parse(dbOrganization.features), + [configKey]: fakeExistingConfig + }); + dbOrganization.features = newFeatures; + await dbOrganization.save(); + await orgCache.clear(organization.id); + }); + it("passes it to updateConfig", async () => { + const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); + expect(twilio.updateConfig.mock.calls).toEqual([ + [fakeExistingConfig, newConfig] + ]); + expect(orgCache.getMessageServiceConfig.mock.calls).toEqual([ + [ + expect.objectContaining({ + id: 1, + feature: expect.objectContaining({ + message_service_twilio: newConfig + }) + }) + ] + ]); + expect(gqlResult.data.updateMessageServiceConfig).toEqual(newConfig); + }); + }); }); diff --git a/__test__/server/models/cacheable_queries/organization.test.js b/__test__/server/models/cacheable_queries/organization.test.js index ec80c7c37..d746561f4 100644 --- a/__test__/server/models/cacheable_queries/organization.test.js +++ b/__test__/server/models/cacheable_queries/organization.test.js @@ -1,65 +1,122 @@ import orgCache from "../../../../src/server/models/cacheable_queries/organization"; +import * as serviceMap from "../../../../src/extensions/messaging_services/service_map"; describe("cacheable_queries.organization", () => { - let twilioOrganization; - let fakeServiceOrganization; + let serviceWith; + let serviceWithout; + let fakeServiceMap; + let organizationWith; + let organizationWithConfig; + let organizationWithout; beforeEach(async () => { - twilioOrganization = { - features: { - service: "twilio", - TWILIO_AUTH_TOKEN: "fake_twilio_auth_token", - TWILIO_ACCOUNT_SID: "fake_twilio_auth_account_sid", - TWILIO_MESSAGE_SERVICE_SID: "fake_twilio_message_service_sid" - } + serviceWith = { + getServiceConfig: jest.fn().mockImplementation(() => "fake_config"), + getMessageServiceSid: jest + .fn() + .mockImplementation(() => "fake_message_service_sid") }; - fakeServiceOrganization = { + + serviceWithout = {}; + + fakeServiceMap = { serviceWith, serviceWithout }; + + organizationWith = { features: { service: "serviceWith" } }; + organizationWithConfig = { features: { - service: "fakeservice" + service: "serviceWith", + message_service_serviceWith: { + fake_key: "fake_value" + } } }; + organizationWithout = { features: { service: "serviceWithout" } }; + + jest + .spyOn(serviceMap, "getService") + .mockImplementation(serviceName => fakeServiceMap[serviceName]); + + jest.spyOn(serviceMap, "getConfigKey"); + jest.spyOn(serviceMap, "tryGetFunctionFromService"); + }); + + afterEach(async () => { + jest.restoreAllMocks(); }); describe("getMessageServiceConfig", () => { - describe("when the message service has getConfigFromCache", () => { - it("returns a config", async () => { + describe("when the message service has getMessageServiceConfig", () => { + it("delegates to its dependencies and returns a config", async () => { const allegedConfig = await orgCache.getMessageServiceConfig( - twilioOrganization + organizationWith ); - expect(allegedConfig).toEqual({ - authToken: twilioOrganization.features.TWILIO_AUTH_TOKEN, - accountSid: twilioOrganization.features.TWILIO_ACCOUNT_SID, - messageServiceSid: - twilioOrganization.features.TWILIO_MESSAGE_SERVICE_SID + expect(allegedConfig).toEqual("fake_config"); + expect(serviceMap.tryGetFunctionFromService.mock.calls).toEqual([ + ["serviceWith", "getServiceConfig"] + ]); + expect(serviceMap.getConfigKey.mock.calls).toEqual([["serviceWith"]]); + expect(serviceWith.getServiceConfig.mock.calls).toEqual([ + [undefined, organizationWith] + ]); + }); + describe("when an organization has a config", () => { + it("delegates to its dependencies and returns a config", async () => { + const allegedConfig = await orgCache.getMessageServiceConfig( + organizationWithConfig + ); + expect(allegedConfig).toEqual("fake_config"); + expect(serviceMap.tryGetFunctionFromService.mock.calls).toEqual([ + ["serviceWith", "getServiceConfig"] + ]); + expect(serviceMap.getConfigKey.mock.calls).toEqual([["serviceWith"]]); + expect(serviceWith.getServiceConfig.mock.calls).toEqual([ + [ + organizationWithConfig.features.message_service_serviceWith, + organizationWithConfig + ] + ]); }); }); }); - describe("when the message service doesn't have getConfigFromCache", () => { - it("does not return a config", async () => { + describe("when the message service doesn't have getMessageServiceConfig", () => { + it("delegates to its dependencies and doesn't return a config", async () => { const allegedConfig = await orgCache.getMessageServiceConfig( - fakeServiceOrganization + organizationWithout ); expect(allegedConfig).toBeNull(); + expect(serviceMap.tryGetFunctionFromService.mock.calls).toEqual([ + ["serviceWithout", "getServiceConfig"] + ]); + expect(serviceMap.getConfigKey).not.toHaveBeenCalled(); + expect(serviceWith.getServiceConfig).not.toHaveBeenCalled(); }); }); }); describe("getMessageServiceSid", () => { - describe("when the message service has getConfigFromCache", () => { - it("returns a config", async () => { - const allegedMessageServiceSid = await orgCache.getMessageServiceSid( - twilioOrganization - ); - expect(allegedMessageServiceSid).toEqual( - twilioOrganization.features.TWILIO_MESSAGE_SERVICE_SID + describe("when the message service has getMessageServiceSid", () => { + it("delegates to its dependencies and returns a config", async () => { + const allegedSid = await orgCache.getMessageServiceSid( + organizationWith ); + expect(allegedSid).toEqual("fake_message_service_sid"); + expect(serviceMap.tryGetFunctionFromService.mock.calls).toEqual([ + ["serviceWith", "getMessageServiceSid"] + ]); + expect(serviceWith.getMessageServiceSid.mock.calls).toEqual([ + [organizationWith, undefined, undefined] + ]); }); }); - describe("when the message service doesn't have getConfigFromCache", () => { - it("does not return a config", async () => { - const allegedConfig = await orgCache.getMessageServiceSid( - fakeServiceOrganization + describe("when the message service doesn't have getMessageServiceConfig", () => { + it("delegates to its dependencies and doesn't return a config", async () => { + const allegedSid = await orgCache.getMessageServiceSid( + organizationWithout ); - expect(allegedConfig).toBeNull(); + expect(allegedSid).toBeNull(); + expect(serviceMap.tryGetFunctionFromService.mock.calls).toEqual([ + ["serviceWithout", "getMessageServiceSid"] + ]); + expect(serviceWith.getMessageServiceSid).not.toHaveBeenCalled(); }); }); }); diff --git a/src/extensions/messaging_services/index.js b/src/extensions/messaging_services/index.js index 91a1a3a55..fef7ee958 100644 --- a/src/extensions/messaging_services/index.js +++ b/src/extensions/messaging_services/index.js @@ -1,7 +1,11 @@ -import serviceMap from "./service_map"; +import serviceMap, { getService } from "./service_map"; import orgCache from "../../server/models/cacheable_queries/organization"; -export { tryGetFunctionFromService } from "./service_map"; +export { + getConfigKey, + getService, + tryGetFunctionFromService +} from "./service_map"; // Each service needs the following api points: // async sendMessage(message, contact, trx, organization) -> void @@ -16,10 +20,8 @@ export { tryGetFunctionFromService } from "./service_map"; // async buyNumbersInAreaCode(organization, areaCode, limit, opts) -> Count of successfully purchased numbers // where the `opts` parameter can include service specific options -export const getService = serviceName => serviceMap[serviceName]; - export const getServiceFromOrganization = organization => - serviceMap[orgCache.getMessageService(organization)]; + getService(orgCache.getMessageService(organization)); export const fullyConfigured = async organization => { const messagingService = getServiceFromOrganization(organization); @@ -35,7 +37,7 @@ export const createMessagingService = (organization, friendlyName) => { const serviceName = orgCache.getMessageService(organization); let service; if (serviceName === "twilio") { - service = serviceMap.twilio; + service = getService("twilio"); } else if (service === "signalwire") { // service = signalwire; } diff --git a/src/extensions/messaging_services/service_map.js b/src/extensions/messaging_services/service_map.js index d0b809eb8..e2ea3e3db 100644 --- a/src/extensions/messaging_services/service_map.js +++ b/src/extensions/messaging_services/service_map.js @@ -8,8 +8,12 @@ const serviceMap = { fakeservice }; +export const getConfigKey = serviceName => `message_service_${serviceName}`; + +export const getService = serviceName => serviceMap[serviceName]; + export const tryGetFunctionFromService = (serviceName, functionName) => { - const messageService = serviceMap[serviceName]; + const messageService = exports.getService(serviceName); if (!messageService) { throw new Error(`${serviceName} is not a message service`); } diff --git a/src/extensions/messaging_services/twilio/index.js b/src/extensions/messaging_services/twilio/index.js index 95a58b876..601b7c603 100644 --- a/src/extensions/messaging_services/twilio/index.js +++ b/src/extensions/messaging_services/twilio/index.js @@ -4,26 +4,21 @@ import Twilio, { twiml } from "twilio"; import urlJoin from "url-join"; import { log } from "../../../lib"; import { getFormattedPhoneNumber } from "../../../lib/phone-format"; -import { - getFeatures, - getConfig, - hasConfig -} from "../../../server/api/lib/config"; +import { getConfig, hasConfig } from "../../../server/api/lib/config"; import { cacheableData, Log, Message, PendingMessagePart, - Organization, r } from "../../../server/models"; import wrap from "../../../server/wrap"; import { saveNewIncomingMessage } from "../message-sending"; +import { getConfigKey } from "../service_map"; import { symmetricDecrypt, symmetricEncrypt } from "../../../server/api/lib/crypto"; -import organizationCache from "../../../server/models/cacheable_queries/organization"; // TWILIO error_codes: // > 1 (i.e. positive) error_codes are reserved for Twilio error codes @@ -826,27 +821,43 @@ async function clearMessagingServicePhones(organization, messagingServiceSid) { } } -export const getConfigFromCache = async organization => { - const hasOrgToken = hasConfig("TWILIO_AUTH_TOKEN_ENCRYPTED", organization); - // Note, allows unencrypted auth tokens to be (manually) stored in the db - // @todo: decide if this is necessary, or if UI/envars is sufficient. - const authToken = hasOrgToken - ? symmetricDecrypt(getConfig("TWILIO_AUTH_TOKEN_ENCRYPTED", organization)) - : getConfig("TWILIO_AUTH_TOKEN", organization); - const accountSid = hasConfig("TWILIO_ACCOUNT_SID", organization) - ? getConfig("TWILIO_ACCOUNT_SID", organization) - : // Check old TWILIO_API_KEY variable for backwards compatibility. - getConfig("TWILIO_API_KEY", organization); - - const messageServiceSid = getConfig( - "TWILIO_MESSAGE_SERVICE_SID", - organization - ); - +export const getServiceConfig = async (serviceConfig, organization) => { + let authToken; + let accountSid; + let messageServiceSid; + if (serviceConfig) { + const hasOrgToken = serviceConfig.TWILIO_AUTH_TOKEN_ENCRYPTED; + // Note, allows unencrypted auth tokens to be (manually) stored in the db + // @todo: decide if this is necessary, or if UI/envars is sufficient. + authToken = hasOrgToken + ? symmetricDecrypt(serviceConfig.TWILIO_AUTH_TOKEN_ENCRYPTED) + : serviceConfig.TWILIO_AUTH_TOKEN; + accountSid = serviceConfig.TWILIO_ACCOUNT_SID + ? serviceConfig.TWILIO_ACCOUNT_SID + : // Check old TWILIO_API_KEY variable for backwards compatibility. + serviceConfig.TWILIO_API_KEY; + + messageServiceSid = serviceConfig.TWILIO_MESSAGE_SERVICE_SID; + } else { + // for backward compatibility + + const hasOrgToken = hasConfig("TWILIO_AUTH_TOKEN_ENCRYPTED", organization); + // Note, allows unencrypted auth tokens to be (manually) stored in the db + // @todo: decide if this is necessary, or if UI/envars is sufficient. + authToken = hasOrgToken + ? symmetricDecrypt(getConfig("TWILIO_AUTH_TOKEN_ENCRYPTED", organization)) + : getConfig("TWILIO_AUTH_TOKEN", organization); + accountSid = hasConfig("TWILIO_ACCOUNT_SID", organization) + ? getConfig("TWILIO_ACCOUNT_SID", organization) + : // Check old TWILIO_API_KEY variable for backwards compatibility. + getConfig("TWILIO_API_KEY", organization); + + messageServiceSid = getConfig("TWILIO_MESSAGE_SERVICE_SID", organization); + } return { authToken, accountSid, messageServiceSid }; }; -export const getMessageServiceSidFromCache = async ( +export const getMessageServiceSid = async ( organization, contact, messageText @@ -855,11 +866,14 @@ export const getMessageServiceSidFromCache = async ( if (messageText && /twilioapitest/.test(messageText)) { return "fakeSid_MK123"; } - return getConfig("TWILIO_MESSAGE_SERVICE_SID", organization); + + const configKey = getConfigKey("twilio"); + const config = getConfig(configKey, organization); + const { messageServiceSid } = exports.getServiceConfig(config, organization); + return messageServiceSid; }; -export const updateConfig = async (organization, config) => { - const featuresJSON = getFeatures(organization); +export const updateConfig = async (oldConfig, config) => { const { twilioAccountSid, twilioAuthToken, twilioMessageServiceSid } = config; if (!twilioAccountSid || !twilioMessageServiceSid) { @@ -868,19 +882,15 @@ export const updateConfig = async (organization, config) => { ); } - featuresJSON.TWILIO_ACCOUNT_SID = twilioAccountSid.substr(0, 64); + const newConfig = {}; + + newConfig.TWILIO_ACCOUNT_SID = twilioAccountSid.substr(0, 64); // TODO(lperson) is twilioAuthToken required? - featuresJSON.TWILIO_AUTH_TOKEN_ENCRYPTED = twilioAuthToken + newConfig.TWILIO_AUTH_TOKEN_ENCRYPTED = twilioAuthToken ? symmetricEncrypt(twilioAuthToken).substr(0, 256) : twilioAuthToken; - featuresJSON.TWILIO_MESSAGE_SERVICE_SID = twilioMessageServiceSid.substr( - 0, - 64 - ); - - const dbOrganization = Organization.get(organization.id); - dbOrganization.features = JSON.stringify(featuresJSON); + newConfig.TWILIO_MESSAGE_SERVICE_SID = twilioMessageServiceSid.substr(0, 64); try { if (twilioAuthToken && global.TEST_ENVIRONMENT !== "1") { @@ -892,10 +902,7 @@ export const updateConfig = async (organization, config) => { throw new Error("Invalid Twilio credentials"); } - await dbOrganization.save(); - await cacheableData.organization.clear(organization.id); - - return organizationCache.getMessageServiceConfig(organization); + return newConfig; }; export default { @@ -915,7 +922,7 @@ export default { deleteMessagingService, clearMessagingServicePhones, getTwilio, - getConfigFromCache, - getMessageServiceSidFromCache, + getServiceConfig, + getMessageServiceSid, updateConfig }; diff --git a/src/server/api/mutations/updateMessageServiceConfig.js b/src/server/api/mutations/updateMessageServiceConfig.js index 2c0a6fa90..9695efe63 100644 --- a/src/server/api/mutations/updateMessageServiceConfig.js +++ b/src/server/api/mutations/updateMessageServiceConfig.js @@ -1,10 +1,13 @@ -import orgCache from "../../models/cacheable_queries/organization"; -import { accessRequired } from "../errors"; import { GraphQLError } from "graphql/error"; import { + getConfigKey, getService, tryGetFunctionFromService } from "../../../extensions/messaging_services"; +import { getConfig } from "../../../server/api/lib/config"; +import orgCache from "../../models/cacheable_queries/organization"; +import { accessRequired } from "../errors"; +import { Organization } from "../../../server/models"; // TODO(lperson) this should allow the message service // to modify only its own object @@ -46,12 +49,28 @@ export const updateMessageServiceConfig = async ( throw new GraphQLError("Config is not valid JSON"); } + const configKey = getConfigKey(messageServiceName); + const existingConfig = getConfig(configKey, organization); + + let newConfig; try { - return serviceConfigFunction(organization, configObject); + newConfig = await serviceConfigFunction(existingConfig, configObject); } catch (caught) { const message = `Error updating config for ${messageServiceName}: ${caught}`; // eslint-disable-next-line no-console console.error(message); throw new GraphQLError(message); } + + const dbOrganization = await Organization.get(organizationId); + dbOrganization.features = JSON.stringify({ + ...JSON.parse(dbOrganization.features), + [configKey]: newConfig + }); + + await dbOrganization.save(); + await orgCache.clear(organization.id); + const updatedOrganization = await orgCache.load(organization.id); + + return orgCache.getMessageServiceConfig(updatedOrganization); }; diff --git a/src/server/models/cacheable_queries/organization.js b/src/server/models/cacheable_queries/organization.js index f2a754062..b658cdf76 100644 --- a/src/server/models/cacheable_queries/organization.js +++ b/src/server/models/cacheable_queries/organization.js @@ -1,18 +1,21 @@ import { r } from "../../models"; import { getConfig, hasConfig } from "../../api/lib/config"; import { symmetricDecrypt } from "../../api/lib/crypto"; -import { tryGetFunctionFromService } from "../../../extensions/messaging_services/service_map"; +import { + getConfigKey, + tryGetFunctionFromService +} from "../../../extensions/messaging_services/service_map"; const cacheKey = orgId => `${process.env.CACHE_PREFIX || ""}org-${orgId}`; -const getMessageServiceFromCache = organization => +const getOrganizationMessageService = organization => getConfig("service", organization) || getConfig("DEFAULT_SERVICE"); const tryGetFunctionFromOrganizationMessageService = ( organization, functionName ) => { - const messageServiceName = getMessageServiceFromCache(organization); + const messageServiceName = getOrganizationMessageService(organization); return tryGetFunctionFromService(messageServiceName, functionName); }; @@ -22,23 +25,28 @@ const organizationCache = { await r.redis.delAsync(cacheKey(id)); } }, - getMessageService: getMessageServiceFromCache, + getMessageService: getOrganizationMessageService, getMessageServiceConfig: async organization => { - const getConfigFromCache = tryGetFunctionFromOrganizationMessageService( + const getServiceConfig = tryGetFunctionFromOrganizationMessageService( organization, - "getConfigFromCache" + "getServiceConfig" ); - return getConfigFromCache && (await getConfigFromCache(organization)); + if (!getServiceConfig) { + return null; + } + const configKey = getConfigKey(getOrganizationMessageService(organization)); + const config = getConfig(configKey, organization); + return getServiceConfig(config, organization); }, getMessageServiceSid: async (organization, contact, messageText) => { - const getMessageServiceSidFromCache = tryGetFunctionFromOrganizationMessageService( + const getMessageServiceSid = tryGetFunctionFromOrganizationMessageService( organization, - "getMessageServiceSidFromCache" - ); - return ( - getMessageServiceSidFromCache && - (await getMessageServiceSidFromCache(organization, contact, messageText)) + "getMessageServiceSid" ); + if (!getMessageServiceSid) { + return null; + } + return getMessageServiceSid(organization, contact, messageText); }, getTwilioAuth: async organization => { const hasOrgToken = hasConfig("TWILIO_AUTH_TOKEN_ENCRYPTED", organization); From c2067880cace8bc451f8b120785f51fff7d94987 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Mon, 15 Feb 2021 16:56:25 -0500 Subject: [PATCH 031/191] Tests for new twilio methods --- .../messaging_services/twilio.test.js | 243 +++++++++++++++++- __test__/test_helpers.js | 2 +- .../messaging_services/twilio/index.js | 26 +- 3 files changed, 259 insertions(+), 12 deletions(-) diff --git a/__test__/extensions/messaging_services/twilio.test.js b/__test__/extensions/messaging_services/twilio.test.js index c7a2b64f9..d168ef443 100644 --- a/__test__/extensions/messaging_services/twilio.test.js +++ b/__test__/extensions/messaging_services/twilio.test.js @@ -1,8 +1,10 @@ /* eslint-disable no-unused-expressions, consistent-return */ -import { r, Message, cacheableData } from "../../../src/server/models/"; -import { getConfig } from "../../../src/server/api/lib/config"; -import * as twilio from "../../../src/extensions/messaging_services/twilio"; +import * as twilioLibrary from "twilio"; import { getLastMessage } from "../../../src/extensions/messaging_services/message-sending"; +import * as twilio from "../../../src/extensions/messaging_services/twilio"; +import { getConfig } from "../../../src/server/api/lib/config"; +import crypto from "../../../src/server/api/lib/crypto"; +import { cacheableData, Message, r } from "../../../src/server/models/"; import { erroredMessageSender } from "../../../src/workers/job-processes"; import { assignTexter, @@ -99,9 +101,9 @@ describe("twilio", () => { testInvite2 = await createInvite(); testOrganization2 = await createOrganization(testAdminUser, testInvite2); organizationId2 = testOrganization2.data.createOrganization.id; - await setTwilioAuth(testAdminUser, testOrganization2); await ensureOrganizationTwilioWithMessagingService(testOrganization); await ensureOrganizationTwilioWithMessagingService(testOrganization2); + await setTwilioAuth(testAdminUser, testOrganization2); // use caching await cacheableData.organization.load(organizationId); @@ -114,6 +116,7 @@ describe("twilio", () => { afterEach(async () => { queryLog = null; r.knex.removeListener("query", spokeDbListener); + jest.restoreAllMocks(); await cleanupTest(); if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); @@ -495,6 +498,238 @@ describe("twilio", () => { expect(inventoryCount).toEqual(12); }); }); + + describe("config functions", () => { + let twilioConfig; + let organization; + let fakeAuthToken; + let fakeAccountSid; + let fakeMessageServiceSid; + let encryptedFakeAuthToken; + let fakeApiKey; + beforeEach(async () => { + fakeAuthToken = "fake_twilio_auth_token"; + fakeAccountSid = "fake_twilio_account_sid"; + fakeMessageServiceSid = "fake_twilio_message_service_sid"; + encryptedFakeAuthToken = crypto.symmetricEncrypt(fakeAuthToken); + fakeApiKey = "fake_twilio_api_key"; + organization = { feature: { TWILIO_AUTH_TOKEN: "should_be_ignored" } }; + twilioConfig = { + TWILIO_AUTH_TOKEN: fakeAuthToken, + TWILIO_ACCOUNT_SID: fakeAccountSid, + TWILIO_MESSAGE_SERVICE_SID: fakeMessageServiceSid + }; + + jest + .spyOn(crypto, "symmetricEncrypt") + .mockReturnValue(encryptedFakeAuthToken); + }); + describe("getServiceConfig", () => { + it("returns the config elements", async () => { + const config = await twilio.getServiceConfig( + twilioConfig, + organization + ); + expect(config).toEqual({ + authToken: fakeAuthToken, + accountSid: fakeAccountSid, + messageServiceSid: fakeMessageServiceSid + }); + }); + describe("when the auth token is encrypted", () => { + beforeEach(async () => { + twilioConfig.TWILIO_AUTH_TOKEN_ENCRYPTED = encryptedFakeAuthToken; + delete twilioConfig.TWILIO_AUTH_TOKEN; + }); + it("returns the config elements", async () => { + const config = await twilio.getServiceConfig( + twilioConfig, + organization + ); + expect(config).toEqual({ + authToken: fakeAuthToken, + accountSid: fakeAccountSid, + messageServiceSid: fakeMessageServiceSid + }); + }); + }); + describe("when it has an API key instead of account sid", () => { + beforeEach(async () => { + twilioConfig.TWILIO_API_KEY = fakeApiKey; + delete twilioConfig.TWILIO_ACCOUNT_SID; + }); + it("returns the config elements", async () => { + const config = await twilio.getServiceConfig( + twilioConfig, + organization + ); + expect(config).toEqual({ + authToken: fakeAuthToken, + accountSid: fakeApiKey, + messageServiceSid: fakeMessageServiceSid + }); + }); + }); + describe("when using legacy config -- all the elements are at the top level", () => { + beforeEach(async () => { + organization = { feature: { ...twilioConfig } }; + }); + it("returns the config elements", async () => { + const config = await twilio.getServiceConfig(undefined, organization); + expect(config).toEqual({ + authToken: fakeAuthToken, + accountSid: fakeAccountSid, + messageServiceSid: fakeMessageServiceSid + }); + }); + describe("when the auth token is encrypted", () => { + beforeEach(async () => { + twilioConfig.TWILIO_AUTH_TOKEN_ENCRYPTED = encryptedFakeAuthToken; + delete twilioConfig.TWILIO_AUTH_TOKEN; + organization = { feature: { ...twilioConfig } }; + }); + it("returns the config elements", async () => { + const config = await twilio.getServiceConfig( + undefined, + organization + ); + expect(config).toEqual({ + authToken: fakeAuthToken, + accountSid: fakeAccountSid, + messageServiceSid: fakeMessageServiceSid + }); + }); + }); + describe("when it has an API key instead of account sid", () => { + beforeEach(async () => { + twilioConfig.TWILIO_API_KEY = fakeApiKey; + delete twilioConfig.TWILIO_ACCOUNT_SID; + organization = { feature: { ...twilioConfig } }; + }); + it("returns the config elements", async () => { + const config = await twilio.getServiceConfig( + undefined, + organization + ); + expect(config).toEqual({ + authToken: fakeAuthToken, + accountSid: fakeApiKey, + messageServiceSid: fakeMessageServiceSid + }); + }); + }); + }); + }); + describe("getMessageServiceSid", () => { + beforeEach(async () => { + organization = { feature: { message_service_twilio: twilioConfig } }; + }); + it("returns the sid", async () => { + const sid = await twilio.getMessageServiceSid( + organization, + undefined, + undefined + ); + expect(sid).toEqual(fakeMessageServiceSid); + }); + describe("when using legacy config -- all the elements are at the top level", () => { + beforeEach(async () => { + organization = { feature: { ...twilioConfig } }; + }); + it("returns the sid", async () => { + const sid = await twilio.getMessageServiceSid( + organization, + undefined, + undefined + ); + expect(sid).toEqual(fakeMessageServiceSid); + }); + }); + }); + describe("updateConfig", () => { + let twilioApiAccountsListMock; + let oldConfig; + let newConfig; + let expectedConfig; + let globalTestEnvironment; + beforeEach(async () => { + globalTestEnvironment = global.TEST_ENVIRONMENT; + global.TEST_ENVIRONMENT = 0; + + oldConfig = "__IGNORED__"; + newConfig = { + twilioAccountSid: fakeAccountSid, + twilioMessageServiceSid: fakeMessageServiceSid, + twilioAuthToken: fakeAuthToken + }; + + expectedConfig = { + TWILIO_ACCOUNT_SID: fakeAccountSid, + TWILIO_AUTH_TOKEN_ENCRYPTED: encryptedFakeAuthToken, + TWILIO_MESSAGE_SERVICE_SID: fakeMessageServiceSid + }; + + twilioApiAccountsListMock = jest.fn().mockResolvedValue({}); + jest.spyOn(twilioLibrary.default, "Twilio").mockReturnValue({ + api: { accounts: { list: twilioApiAccountsListMock } } + }); + }); + + afterEach(async () => { + global.TEST_ENVIRONMENT = globalTestEnvironment; + }); + + it("delegates to its dependencies and returns the new config", async () => { + const returnedConfig = await twilio.updateConfig(oldConfig, newConfig); + expect(returnedConfig).toEqual(expectedConfig); + expect(crypto.symmetricEncrypt.mock.calls).toEqual([ + ["fake_twilio_auth_token"] + ]); + expect(twilioLibrary.default.Twilio.mock.calls).toEqual([ + [fakeAccountSid, fakeAuthToken] + ]); + expect(twilioApiAccountsListMock.mock.calls).toEqual([[]]); + }); + describe("when the new config doesn't contain required elements", () => { + beforeEach(async () => { + delete newConfig.twilioAccountSid; + }); + it("throws an exception", async () => { + let error; + try { + await twilio.updateConfig(oldConfig, newConfig); + } catch (caught) { + error = caught; + } + expect(error.message).toEqual( + "twilioAccountSid and twilioMessageServiceSid are required" + ); + expect(crypto.symmetricEncrypt).not.toHaveBeenCalled(); + expect(twilioLibrary.default.Twilio).not.toHaveBeenCalled(); + expect(twilioApiAccountsListMock).not.toHaveBeenCalled(); + }); + }); + describe("when the twilio credentials are invalid", () => { + beforeEach(async () => { + twilioApiAccountsListMock = jest.fn().mockImplementation(() => { + throw new Error("OH NO!"); + }); + jest.spyOn(twilioLibrary.default, "Twilio").mockReturnValue({ + api: { accounts: { list: twilioApiAccountsListMock } } + }); + }); + it("throws an exception", async () => { + let error; + try { + await twilio.updateConfig(oldConfig, newConfig); + } catch (caught) { + error = caught; + } + expect(error.message).toEqual("Invalid Twilio credentials"); + }); + }); + }); + }); }); // FUTURE diff --git a/__test__/test_helpers.js b/__test__/test_helpers.js index 3ac8028d2..af6216065 100644 --- a/__test__/test_helpers.js +++ b/__test__/test_helpers.js @@ -218,7 +218,7 @@ export const ensureOrganizationTwilioWithMessagingService = async ( export async function setTwilioAuth(user, organization) { const rootValue = {}; const accountSid = "test_twilio_account_sid"; - const authToken = "test_twlio_auth_token"; + const authToken = "test_twilio_auth_token"; const messageServiceSid = "test_message_service"; const orgId = organization.data.createOrganization.id; diff --git a/src/extensions/messaging_services/twilio/index.js b/src/extensions/messaging_services/twilio/index.js index 601b7c603..48a6c7b27 100644 --- a/src/extensions/messaging_services/twilio/index.js +++ b/src/extensions/messaging_services/twilio/index.js @@ -1,6 +1,6 @@ /* eslint-disable no-use-before-define, no-console */ import _ from "lodash"; -import Twilio, { twiml } from "twilio"; +import * as twilioLibrary from "twilio"; import urlJoin from "url-join"; import { log } from "../../../lib"; import { getFormattedPhoneNumber } from "../../../lib/phone-format"; @@ -41,7 +41,7 @@ export async function getTwilio(organization) { accountSid } = await cacheableData.organization.getTwilioAuth(organization); if (accountSid && authToken) { - return Twilio(accountSid, authToken); // eslint-disable-line new-cap + return twilioLibrary.default.Twilio(accountSid, authToken); // eslint-disable-line new-cap } return null; } @@ -67,7 +67,11 @@ const headerValidator = url => { url }; - return Twilio.webhook(authToken, options)(req, res, next); + return twilioLibrary.default.Twilio.webhook(authToken, options)( + req, + res, + next + ); }; }; @@ -116,7 +120,7 @@ function addServerEndpoints(expressApp) { } catch (ex) { log.error(ex); } - const resp = new twiml.MessagingResponse(); + const resp = new twilioLibrary.default.twiml.MessagingResponse(); res.writeHead(200, { "Content-Type": "text/xml" }); res.end(resp.toString()); }) @@ -142,7 +146,7 @@ function addServerEndpoints(expressApp) { } catch (ex) { log.error(ex); } - const resp = new twiml.MessagingResponse(); + const resp = new twilioLibrary.default.twiml.MessagingResponse(); res.writeHead(200, { "Content-Type": "text/xml" }); res.end(resp.toString()); }) @@ -869,10 +873,14 @@ export const getMessageServiceSid = async ( const configKey = getConfigKey("twilio"); const config = getConfig(configKey, organization); - const { messageServiceSid } = exports.getServiceConfig(config, organization); + const { messageServiceSid } = await exports.getServiceConfig( + config, + organization + ); return messageServiceSid; }; +// TODO(lperson) maybe we should support backward compatibility here? export const updateConfig = async (oldConfig, config) => { const { twilioAccountSid, twilioAuthToken, twilioMessageServiceSid } = config; @@ -895,7 +903,11 @@ export const updateConfig = async (oldConfig, config) => { try { if (twilioAuthToken && global.TEST_ENVIRONMENT !== "1") { // Make sure Twilio credentials work. - const twilio = Twilio(twilioAccountSid, twilioAuthToken); // eslint-disable-line new-cap + // eslint-disable-next-line new-cap + const twilio = twilioLibrary.default.Twilio( + twilioAccountSid, + twilioAuthToken + ); // eslint-disable-line new-cap await twilio.api.accounts.list(); } } catch (err) { From 5d36b9e7aaf0375e25cda558aaf81e10b3271556 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Mon, 15 Feb 2021 18:26:53 -0500 Subject: [PATCH 032/191] start to retire getTwilioAuth --- .../messaging_services/service_map.js | 14 ++++++++++ .../messaging_services/twilio/index.js | 14 ++++------ .../models/cacheable_queries/organization.js | 28 +++++-------------- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/extensions/messaging_services/service_map.js b/src/extensions/messaging_services/service_map.js index e2ea3e3db..58eb1a6ae 100644 --- a/src/extensions/messaging_services/service_map.js +++ b/src/extensions/messaging_services/service_map.js @@ -1,6 +1,7 @@ import nexmo from "./nexmo"; import * as twilio from "./twilio"; import fakeservice from "./fakeservice"; +import { getConfig } from "../../server/api/lib/config"; const serviceMap = { nexmo, @@ -21,4 +22,17 @@ export const tryGetFunctionFromService = (serviceName, functionName) => { return fn && typeof fn === "function" ? fn : null; }; +export const getMessageServiceConfig = async (serviceName, organization) => { + const getServiceConfig = exports.tryGetFunctionFromService( + serviceName, + "getServiceConfig" + ); + if (!getServiceConfig) { + return null; + } + const configKey = exports.getConfigKey(serviceName); + const config = getConfig(configKey, organization); + return getServiceConfig(config, organization); +}; + export default serviceMap; diff --git a/src/extensions/messaging_services/twilio/index.js b/src/extensions/messaging_services/twilio/index.js index 48a6c7b27..f2b6e26ee 100644 --- a/src/extensions/messaging_services/twilio/index.js +++ b/src/extensions/messaging_services/twilio/index.js @@ -14,7 +14,7 @@ import { } from "../../../server/models"; import wrap from "../../../server/wrap"; import { saveNewIncomingMessage } from "../message-sending"; -import { getConfigKey } from "../service_map"; +import { getMessageServiceConfig, getConfigKey } from "../service_map"; import { symmetricDecrypt, symmetricEncrypt @@ -36,10 +36,10 @@ const BULK_REQUEST_CONCURRENCY = 5; const MAX_NUMBERS_PER_BUY_JOB = getConfig("MAX_NUMBERS_PER_BUY_JOB") || 100; export async function getTwilio(organization) { - const { - authToken, - accountSid - } = await cacheableData.organization.getTwilioAuth(organization); + const { authToken, accountSid } = await getMessageServiceConfig( + "twilio", + organization + ); if (accountSid && authToken) { return twilioLibrary.default.Twilio(accountSid, authToken); // eslint-disable-line new-cap } @@ -58,9 +58,7 @@ const headerValidator = url => { const organization = req.params.orgId ? await cacheableData.organization.load(req.params.orgId) : null; - const { authToken } = await cacheableData.organization.getTwilioAuth( - organization - ); + const { authToken } = await getMessageServiceConfig("twilio", organization); const options = { validate: true, protocol: "https", diff --git a/src/server/models/cacheable_queries/organization.js b/src/server/models/cacheable_queries/organization.js index b658cdf76..27c4de181 100644 --- a/src/server/models/cacheable_queries/organization.js +++ b/src/server/models/cacheable_queries/organization.js @@ -2,7 +2,7 @@ import { r } from "../../models"; import { getConfig, hasConfig } from "../../api/lib/config"; import { symmetricDecrypt } from "../../api/lib/crypto"; import { - getConfigKey, + getMessageServiceConfig, tryGetFunctionFromService } from "../../../extensions/messaging_services/service_map"; @@ -11,14 +11,6 @@ const cacheKey = orgId => `${process.env.CACHE_PREFIX || ""}org-${orgId}`; const getOrganizationMessageService = organization => getConfig("service", organization) || getConfig("DEFAULT_SERVICE"); -const tryGetFunctionFromOrganizationMessageService = ( - organization, - functionName -) => { - const messageServiceName = getOrganizationMessageService(organization); - return tryGetFunctionFromService(messageServiceName, functionName); -}; - const organizationCache = { clear: async id => { if (r.redis) { @@ -27,22 +19,16 @@ const organizationCache = { }, getMessageService: getOrganizationMessageService, getMessageServiceConfig: async organization => { - const getServiceConfig = tryGetFunctionFromOrganizationMessageService( - organization, - "getServiceConfig" - ); - if (!getServiceConfig) { - return null; - } - const configKey = getConfigKey(getOrganizationMessageService(organization)); - const config = getConfig(configKey, organization); - return getServiceConfig(config, organization); + const serviceName = getOrganizationMessageService(organization); + return getMessageServiceConfig(serviceName, organization); }, getMessageServiceSid: async (organization, contact, messageText) => { - const getMessageServiceSid = tryGetFunctionFromOrganizationMessageService( - organization, + const messageServiceName = getOrganizationMessageService(organization); + const getMessageServiceSid = tryGetFunctionFromService( + messageServiceName, "getMessageServiceSid" ); + if (!getMessageServiceSid) { return null; } From 47497a6694236c43f3b309b129a89566c7384e4d Mon Sep 17 00:00:00 2001 From: Larry Person Date: Mon, 15 Feb 2021 19:17:41 -0500 Subject: [PATCH 033/191] retire getTwilioAuth; refactor fullyConfigured --- __test__/server/api/organization.test.js | 27 ++++++++++ src/extensions/messaging_services/index.js | 16 ++++-- .../messaging_services/twilio/index.js | 49 +++++++++++++++++ src/server/api/organization.js | 53 ++----------------- .../models/cacheable_queries/organization.js | 18 +------ 5 files changed, 92 insertions(+), 71 deletions(-) diff --git a/__test__/server/api/organization.test.js b/__test__/server/api/organization.test.js index f84812fc3..619321762 100644 --- a/__test__/server/api/organization.test.js +++ b/__test__/server/api/organization.test.js @@ -2,6 +2,8 @@ import { r } from "../../../src/server/models/"; import { getCampaignsQuery } from "../../../src/containers/AdminCampaignList"; import { GraphQLError } from "graphql/error"; +import gql from "graphql-tag"; +import * as messagingServices from "../../../src/extensions/messaging_services"; import { cleanupTest, @@ -291,4 +293,29 @@ describe("organization", async () => { ]); }); }); + + describe(".fullyconfigured", () => { + let gqlQuery; + let variables; + beforeEach(async () => { + gqlQuery = gql` + query fullyConfigured($organizationId: String!) { + organization(id: $organizationId) { + fullyConfigured + } + } + `; + + variables = { organizationId: 1 }; + + jest.spyOn(messagingServices, "fullyConfigured").mockResolvedValue(false); + }); + it("delegates to its dependency and returns the result", async () => { + const result = await runGql(gqlQuery, variables, testAdminUser); + expect(result.data.organization.fullyConfigured).toEqual(false); + expect(messagingServices.fullyConfigured.mock.calls).toEqual([ + [expect.objectContaining({ id: 1 })] + ]); + }); + }); }); diff --git a/src/extensions/messaging_services/index.js b/src/extensions/messaging_services/index.js index fef7ee958..50d57002c 100644 --- a/src/extensions/messaging_services/index.js +++ b/src/extensions/messaging_services/index.js @@ -1,4 +1,7 @@ -import serviceMap, { getService } from "./service_map"; +import serviceMap, { + tryGetFunctionFromService, + getService +} from "./service_map"; import orgCache from "../../server/models/cacheable_queries/organization"; export { @@ -20,13 +23,16 @@ export { // async buyNumbersInAreaCode(organization, areaCode, limit, opts) -> Count of successfully purchased numbers // where the `opts` parameter can include service specific options +export const getServiceNameFromOrganization = organization => + orgCache.getMessageService(organization); + export const getServiceFromOrganization = organization => - getService(orgCache.getMessageService(organization)); + getService(getServiceNameFromOrganization(organization)); export const fullyConfigured = async organization => { - const messagingService = getServiceFromOrganization(organization); - const fn = messagingService.fullyConfigured; - if (!fn || typeof fn !== "function") { + const serviceName = getServiceNameFromOrganization(organization); + const fn = tryGetFunctionFromService(serviceName, "fullyConfigured"); + if (!fn) { return true; } diff --git a/src/extensions/messaging_services/twilio/index.js b/src/extensions/messaging_services/twilio/index.js index f2b6e26ee..3cb022eea 100644 --- a/src/extensions/messaging_services/twilio/index.js +++ b/src/extensions/messaging_services/twilio/index.js @@ -915,6 +915,55 @@ export const updateConfig = async (oldConfig, config) => { return newConfig; }; +const campaignNumbersEnabled = organization => { + const inventoryEnabled = + getConfig("EXPERIMENTAL_PHONE_INVENTORY", organization, { + truthy: true + }) || + getConfig("PHONE_INVENTORY", organization, { + truthy: true + }); + + return ( + inventoryEnabled && + getConfig("EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS", organization, { + truthy: true + }) + ); +}; + +const manualMessagingServicesEnabled = organization => + getConfig( + "EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE", + organization, + { truthy: true } + ); + +export const fullyConfigured = async organization => { + const { authToken, accountSid } = await getMessageServiceConfig( + "twilio", + organization + ); + + let messagingServiceConfigured; + if ( + manualMessagingServicesEnabled(organization) || + campaignNumbersEnabled(organization) + ) { + messagingServiceConfigured = true; + } else { + messagingServiceConfigured = await cacheableData.organization.getMessageServiceSid( + organization + ); + } + + if (!(authToken && accountSid && messagingServiceConfigured)) { + return false; + } + + return true; +}; + export default { syncMessagePartProcessing: !!process.env.JOBS_SAME_PROCESS, addServerEndpoints, diff --git a/src/server/api/organization.js b/src/server/api/organization.js index 8abd03df3..5a5e4d45d 100644 --- a/src/server/api/organization.js +++ b/src/server/api/organization.js @@ -3,12 +3,13 @@ import { getConfig, getFeatures } from "./lib/config"; import { r, Organization, cacheableData } from "../models"; import { getTags } from "./tag"; import { accessRequired } from "./errors"; -import { getCampaigns, getCampaignsCount } from "./campaign"; +import { getCampaigns } from "./campaign"; import { buildUsersQuery } from "./user"; import { getAvailableActionHandlers, getActionChoiceData } from "../../extensions/action-handlers"; +import { fullyConfigured } from "../../extensions/messaging_services"; export const ownerConfigurable = { // ACTION_HANDLERS: 1, @@ -51,30 +52,6 @@ export const getSideboxChoices = organization => { return sideboxChoices; }; -const campaignNumbersEnabled = organization => { - const inventoryEnabled = - getConfig("EXPERIMENTAL_PHONE_INVENTORY", organization, { - truthy: true - }) || - getConfig("PHONE_INVENTORY", organization, { - truthy: true - }); - - return ( - inventoryEnabled && - getConfig("EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS", organization, { - truthy: true - }) - ); -}; - -const manualMessagingServicesEnabled = organization => - getConfig( - "EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE", - organization, - { truthy: true } - ); - export const resolvers = { Organization: { ...mapFieldsToModel(["id", "name"], Organization), @@ -264,31 +241,7 @@ export const resolvers = { } }, fullyConfigured: async organization => { - const serviceName = - getConfig("service", organization) || getConfig("DEFAULT_SERVICE"); - if (serviceName === "twilio") { - const { - authToken, - accountSid - } = await cacheableData.organization.getTwilioAuth(organization); - - let messagingServiceConfigured; - if ( - manualMessagingServicesEnabled(organization) || - campaignNumbersEnabled(organization) - ) { - messagingServiceConfigured = true; - } else { - messagingServiceConfigured = await cacheableData.organization.getMessageServiceSid( - organization - ); - } - - if (!(authToken && accountSid && messagingServiceConfigured)) { - return false; - } - } - return true; + return fullyConfigured(organization); }, emailEnabled: async (organization, _, { user }) => { await accessRequired(user, organization.id, "SUPERVOLUNTEER", true); diff --git a/src/server/models/cacheable_queries/organization.js b/src/server/models/cacheable_queries/organization.js index 27c4de181..8a7849aeb 100644 --- a/src/server/models/cacheable_queries/organization.js +++ b/src/server/models/cacheable_queries/organization.js @@ -1,10 +1,9 @@ -import { r } from "../../models"; -import { getConfig, hasConfig } from "../../api/lib/config"; -import { symmetricDecrypt } from "../../api/lib/crypto"; import { getMessageServiceConfig, tryGetFunctionFromService } from "../../../extensions/messaging_services/service_map"; +import { getConfig } from "../../api/lib/config"; +import { r } from "../../models"; const cacheKey = orgId => `${process.env.CACHE_PREFIX || ""}org-${orgId}`; @@ -34,19 +33,6 @@ const organizationCache = { } return getMessageServiceSid(organization, contact, messageText); }, - getTwilioAuth: async organization => { - const hasOrgToken = hasConfig("TWILIO_AUTH_TOKEN_ENCRYPTED", organization); - // Note, allows unencrypted auth tokens to be (manually) stored in the db - // @todo: decide if this is necessary, or if UI/envars is sufficient. - const authToken = hasOrgToken - ? symmetricDecrypt(getConfig("TWILIO_AUTH_TOKEN_ENCRYPTED", organization)) - : getConfig("TWILIO_AUTH_TOKEN", organization); - const accountSid = hasConfig("TWILIO_ACCOUNT_SID", organization) - ? getConfig("TWILIO_ACCOUNT_SID", organization) - : // Check old TWILIO_API_KEY variable for backwards compatibility. - getConfig("TWILIO_API_KEY", organization); - return { authToken, accountSid }; - }, load: async id => { if (r.redis) { const orgData = await r.redis.getAsync(cacheKey(id)); From e9f16100394bcc6978fce56608d68c9387f6d697 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sat, 20 Feb 2021 12:02:45 -0500 Subject: [PATCH 034/191] Tests for twilio.fullyConfigured --- .../messaging_services/twilio.test.js | 169 ++++++++++++++++++ .../messaging_services/twilio/index.js | 28 ++- 2 files changed, 180 insertions(+), 17 deletions(-) diff --git a/__test__/extensions/messaging_services/twilio.test.js b/__test__/extensions/messaging_services/twilio.test.js index d168ef443..e26cabce9 100644 --- a/__test__/extensions/messaging_services/twilio.test.js +++ b/__test__/extensions/messaging_services/twilio.test.js @@ -729,6 +729,175 @@ describe("twilio", () => { }); }); }); + describe("campaignNumbersEnabled", () => { + beforeEach(async () => { + organization = { + feature: { + EXPERIMENTAL_PHONE_INVENTORY: true, + PHONE_INVENTORY: true, + EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS: true + } + }; + }); + + it("returns true when all the configs are true", async () => { + expect(twilio.campaignNumbersEnabled(organization)).toEqual(true); + }); + describe("when EXPERIMENTAL_PHONE_INVENTORY is false", () => { + beforeEach(async () => { + organization.feature.EXPERIMENTAL_PHONE_INVENTORY = false; + }); + + it("returns true", async () => { + expect(twilio.campaignNumbersEnabled(organization)).toEqual(true); + }); + }); + describe("when PHONE_INVENTORY is false", () => { + beforeEach(async () => { + organization.feature.PHONE_INVENTORY = false; + }); + + it("returns true", async () => { + expect(twilio.campaignNumbersEnabled(organization)).toEqual(true); + }); + }); + describe("when EXPERIMENTAL_PHONE_INVENTORY and PHONE_INVENTORY are both false", () => { + beforeEach(async () => { + organization.feature.PHONE_INVENTORY = false; + organization.feature.EXPERIMENTAL_PHONE_INVENTORY = false; + }); + + it("returns false", async () => { + expect(twilio.campaignNumbersEnabled(organization)).toEqual(false); + }); + }); + describe("when EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS is false", () => { + beforeEach(async () => { + organization.feature.EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS = false; + }); + + it("returns false", async () => { + expect(twilio.campaignNumbersEnabled(organization)).toEqual(false); + }); + }); + }); + describe("manualMessagingServicesEnabled", () => { + beforeEach(async () => { + organization = { + feature: { + EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE: true + } + }; + }); + + it("it returns true with the config is true", async () => { + expect(twilio.manualMessagingServicesEnabled(organization)).toEqual( + true + ); + }); + + describe("when the config is false", () => { + beforeEach(async () => { + organization.feature.EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE = false; + }); + it("returns flse", async () => { + expect(twilio.manualMessagingServicesEnabled(organization)).toEqual( + false + ); + }); + }); + }); + describe.only("fullyConfigured", () => { + beforeEach(async () => { + jest.spyOn(twilio, "getServiceConfig").mockResolvedValue({ + authToken: "fake_auth_token", + accountSid: "fake_account_sid" + }); + jest + .spyOn(twilio, "manualMessagingServicesEnabled") + .mockReturnValue(true); + jest.spyOn(twilio, "campaignNumbersEnabled").mockReturnValue(true); + jest + .spyOn(twilio, "getMessageServiceSid") + .mockResolvedValue("fake_message_service_sid"); + }); + it("returns true", async () => { + expect(await twilio.fullyConfigured("everything_is_mocked")).toEqual( + true + ); + expect(twilio.getMessageServiceSid).not.toHaveBeenCalled(); + }); + describe("when getServiceConfig doesn't return a full configuration", () => { + beforeEach(async () => { + jest.spyOn(twilio, "getServiceConfig").mockResolvedValue({ + authToken: "fake_auth_token" + }); + }); + it("returns false", async () => { + expect(await twilio.fullyConfigured("everything_is_mocked")).toEqual( + false + ); + expect(twilio.manualMessagingServicesEnabled).not.toHaveBeenCalled(); + expect(twilio.campaignNumbersEnabled).not.toHaveBeenCalled(); + expect(twilio.getMessageServiceSid).not.toHaveBeenCalled(); + }); + }); + describe("when manualmessagingServicesEnabled returns false", () => { + beforeEach(async () => { + jest + .spyOn(twilio, "manualMessagingServicesEnabled") + .mockReturnValue(false); + }); + it("returns true", async () => { + expect(await twilio.fullyConfigured("everything_is_mocked")).toEqual( + true + ); + expect(twilio.getMessageServiceSid).not.toHaveBeenCalled(); + }); + }); + describe("when campaignNumbersEnabled returns false", () => { + beforeEach(async () => { + jest.spyOn(twilio, "campaignNumbersEnabled").mockReturnValue(false); + }); + it("returns true", async () => { + expect(await twilio.fullyConfigured("everything_is_mocked")).toEqual( + true + ); + expect(twilio.getMessageServiceSid).not.toHaveBeenCalled(); + }); + }); + describe("when manualMessagingServiceEnabled and campaignNumbersEnabled both return false", () => { + beforeEach(async () => { + jest + .spyOn(twilio, "manualMessagingServicesEnabled") + .mockReturnValue(false); + jest.spyOn(twilio, "campaignNumbersEnabled").mockReturnValue(false); + }); + describe("when getMessageServiceSid returns true", () => { + it("returns true", async () => { + expect( + await twilio.fullyConfigured("everything_is_mocked") + ).toEqual(true); + expect(twilio.getMessageServiceSid.mock.calls).toEqual([ + ["everything_is_mocked"] + ]); + }); + }); + describe("when getMessageServiceSid returns null", () => { + beforeEach(async () => { + jest.spyOn(twilio, "getMessageServiceSid").mockResolvedValue(null); + }); + it("returns false", async () => { + expect( + await twilio.fullyConfigured("everything_is_mocked") + ).toEqual(false); + expect(twilio.getMessageServiceSid.mock.calls).toEqual([ + ["everything_is_mocked"] + ]); + }); + }); + }); + }); }); }); diff --git a/src/extensions/messaging_services/twilio/index.js b/src/extensions/messaging_services/twilio/index.js index 3cb022eea..307cdc61e 100644 --- a/src/extensions/messaging_services/twilio/index.js +++ b/src/extensions/messaging_services/twilio/index.js @@ -915,7 +915,7 @@ export const updateConfig = async (oldConfig, config) => { return newConfig; }; -const campaignNumbersEnabled = organization => { +export const campaignNumbersEnabled = organization => { const inventoryEnabled = getConfig("EXPERIMENTAL_PHONE_INVENTORY", organization, { truthy: true @@ -932,7 +932,7 @@ const campaignNumbersEnabled = organization => { ); }; -const manualMessagingServicesEnabled = organization => +export const manualMessagingServicesEnabled = organization => getConfig( "EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE", organization, @@ -940,28 +940,22 @@ const manualMessagingServicesEnabled = organization => ); export const fullyConfigured = async organization => { - const { authToken, accountSid } = await getMessageServiceConfig( + const { authToken, accountSid } = await exports.getServiceConfig( "twilio", organization ); - let messagingServiceConfigured; - if ( - manualMessagingServicesEnabled(organization) || - campaignNumbersEnabled(organization) - ) { - messagingServiceConfigured = true; - } else { - messagingServiceConfigured = await cacheableData.organization.getMessageServiceSid( - organization - ); - } - - if (!(authToken && accountSid && messagingServiceConfigured)) { + if (!(authToken && accountSid)) { return false; } - return true; + if ( + exports.manualMessagingServicesEnabled(organization) || + exports.campaignNumbersEnabled(organization) + ) { + return true; + } + return !!(await exports.getMessageServiceSid(organization)); }; export default { From fd86393a18b921c1cc18921d725cd6613828d04c Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sat, 20 Feb 2021 12:22:42 -0500 Subject: [PATCH 035/191] remove only --- __test__/extensions/messaging_services/twilio.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__test__/extensions/messaging_services/twilio.test.js b/__test__/extensions/messaging_services/twilio.test.js index e26cabce9..3f8c99d32 100644 --- a/__test__/extensions/messaging_services/twilio.test.js +++ b/__test__/extensions/messaging_services/twilio.test.js @@ -807,7 +807,7 @@ describe("twilio", () => { }); }); }); - describe.only("fullyConfigured", () => { + describe("fullyConfigured", () => { beforeEach(async () => { jest.spyOn(twilio, "getServiceConfig").mockResolvedValue({ authToken: "fake_auth_token", From aafdb321e4f3d949d9445df4d834f50b7f351719 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sat, 20 Feb 2021 12:31:07 -0500 Subject: [PATCH 036/191] tests for message_services fullyConfigured --- .../messaging_services/index.test.js | 40 +++++++++++++++++++ src/extensions/messaging_services/index.js | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 __test__/extensions/messaging_services/index.test.js diff --git a/__test__/extensions/messaging_services/index.test.js b/__test__/extensions/messaging_services/index.test.js new file mode 100644 index 000000000..3539b73f8 --- /dev/null +++ b/__test__/extensions/messaging_services/index.test.js @@ -0,0 +1,40 @@ +import * as serviceMap from "../../../src/extensions/messaging_services/service_map"; +import * as messagingServices from "../../../src/extensions/messaging_services/index"; + +describe("extensions/messaging_services index", () => { + describe("fullyConfigured", () => { + let fullyConfiguredFunction; + let organization; + beforeEach(async () => { + organization = { + feature: { + service: "fake_fake_service" + } + }; + fullyConfiguredFunction = jest.fn().mockReturnValue(false); + jest + .spyOn(serviceMap, "tryGetFunctionFromService") + .mockReturnValue(fullyConfiguredFunction); + }); + it("calls functions and returns something", async () => { + expect(await messagingServices.fullyConfigured(organization)).toEqual( + false + ); + expect(messagingServices.tryGetFunctionFromService.mock.calls).toEqual([ + ["fake_fake_service", "fullyConfigured"] + ]); + }); + describe("when the services doesn't have fullyConfigured", () => { + beforeEach(async () => { + jest + .spyOn(serviceMap, "tryGetFunctionFromService") + .mockReturnValue(null); + }); + it("returns true", async () => { + expect(await messagingServices.fullyConfigured(organization)).toEqual( + true + ); + }); + }); + }); +}); diff --git a/src/extensions/messaging_services/index.js b/src/extensions/messaging_services/index.js index 50d57002c..7003c4ef8 100644 --- a/src/extensions/messaging_services/index.js +++ b/src/extensions/messaging_services/index.js @@ -30,7 +30,7 @@ export const getServiceFromOrganization = organization => getService(getServiceNameFromOrganization(organization)); export const fullyConfigured = async organization => { - const serviceName = getServiceNameFromOrganization(organization); + const serviceName = exports.getServiceNameFromOrganization(organization); const fn = tryGetFunctionFromService(serviceName, "fullyConfigured"); if (!fn) { return true; From 69067bbcb7c2093238455289a4ac56d537e9a4ea Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sat, 20 Feb 2021 13:39:09 -0500 Subject: [PATCH 037/191] add messageService to schema --- src/api/message-service.js | 15 +++++++++++++++ src/api/organization.js | 12 +----------- src/api/schema.js | 4 +++- 3 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 src/api/message-service.js diff --git a/src/api/message-service.js b/src/api/message-service.js new file mode 100644 index 000000000..385526efd --- /dev/null +++ b/src/api/message-service.js @@ -0,0 +1,15 @@ +import gql from "graphql-tag"; + +export const schema = gql` + enum MessageServiceType { + SMS + } + + type MessageService { + name: String! + type: MessageServiceType! + config: JSON + supportsOrgConfig: Boolean! + supportsCampaignConfig: Boolean! + } +`; diff --git a/src/api/organization.js b/src/api/organization.js index 08659baf7..7dd55d44e 100644 --- a/src/api/organization.js +++ b/src/api/organization.js @@ -41,16 +41,6 @@ export const schema = gql` unsetFeatures: [String] } - enum MessagingServiceType { - SMS - } - - type MessagingService { - name: String! - type: MessagingServiceType - config: JSON - } - input OrgSettingsInput { messageHandlers: [String] actionHandlers: [String] @@ -86,7 +76,7 @@ export const schema = gql` twilioAccountSid: String twilioAuthToken: String twilioMessageServiceSid: String - messagingService: MessagingService + messageService: MessageService fullyConfigured: Boolean emailEnabled: Boolean phoneInventoryEnabled: Boolean! diff --git a/src/api/schema.js b/src/api/schema.js index fbae621c9..f14559f3e 100644 --- a/src/api/schema.js +++ b/src/api/schema.js @@ -14,6 +14,7 @@ import { schema as campaignContactSchema } from "./campaign-contact"; import { schema as cannedResponseSchema } from "./canned-response"; import { schema as inviteSchema } from "./invite"; import { schema as tagSchema } from "./tag"; +import { schema as messageServiceSchema } from "./message-service"; const rootSchema = gql` input CampaignContactInput { @@ -401,5 +402,6 @@ export const schema = [ questionSchema, inviteSchema, conversationSchema, - tagSchema + tagSchema, + messageServiceSchema ]; From 43f49d37594c264ec069d50b769299ae7d7e7e59 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sat, 20 Feb 2021 14:09:08 -0500 Subject: [PATCH 038/191] get service metadata --- .../messaging_services/service_map.test.js | 150 +++++++++++++----- .../messaging_services/fakeservice/index.js | 10 +- src/extensions/messaging_services/index.js | 3 +- .../messaging_services/nexmo/index.js | 10 +- .../messaging_services/service_map.js | 15 ++ .../messaging_services/twilio/index.js | 10 +- 6 files changed, 151 insertions(+), 47 deletions(-) diff --git a/__test__/extensions/messaging_services/service_map.test.js b/__test__/extensions/messaging_services/service_map.test.js index 80f1ccbdb..8f7de3e26 100644 --- a/__test__/extensions/messaging_services/service_map.test.js +++ b/__test__/extensions/messaging_services/service_map.test.js @@ -1,64 +1,128 @@ import * as serviceMap from "../../../src/extensions/messaging_services/service_map"; describe("service_map", () => { - let serviceWith; - let serviceWithout; - let fakeServiceMap; - beforeEach(async () => { - serviceWith = { - getServiceConfig: jest.fn().mockImplementation(() => "fake_config"), - getMessageServiceSid: jest.fn() - }; + afterEach(() => { + jest.restoreAllMocks(); + }); + describe("tests with mocked service map", () => { + let serviceWith; + let serviceWithout; + let fakeServiceMap; + beforeEach(async () => { + serviceWith = { + getServiceConfig: jest.fn().mockImplementation(() => "fake_config"), + getMessageServiceSid: jest.fn() + }; - serviceWithout = {}; + serviceWithout = {}; - fakeServiceMap = { serviceWith, serviceWithout }; + fakeServiceMap = { serviceWith, serviceWithout }; - jest - .spyOn(serviceMap, "getService") - .mockImplementation(serviceName => fakeServiceMap[serviceName]); - }); + jest + .spyOn(serviceMap, "getService") + .mockImplementation(serviceName => fakeServiceMap[serviceName]); + }); - describe("getConfigKey", () => { - it("returns the correct config key", async () => { - const configKey = serviceMap.getConfigKey("fake_service_name"); - expect(configKey).toEqual("message_service_fake_service_name"); + describe("getConfigKey", () => { + it("returns the correct config key", async () => { + const configKey = serviceMap.getConfigKey("fake_service_name"); + expect(configKey).toEqual("message_service_fake_service_name"); + }); }); - }); - describe("tryGetFunctionFromService", () => { - it("returns the function", async () => { - const fn = serviceMap.tryGetFunctionFromService( - "serviceWith", - "getServiceConfig" - ); - expect(fn).not.toBeNull(); - expect(typeof fn).toEqual("function"); - const fnReturn = fn(); - expect(fnReturn).toEqual("fake_config"); + describe("tryGetFunctionFromService", () => { + it("returns the function", async () => { + const fn = serviceMap.tryGetFunctionFromService( + "serviceWith", + "getServiceConfig" + ); + expect(fn).not.toBeNull(); + expect(typeof fn).toEqual("function"); + const fnReturn = fn(); + expect(fnReturn).toEqual("fake_config"); + }); + describe("when the service doesn't exist", () => { + it("throws an exception", async () => { + let error; + try { + serviceMap.tryGetFunctionFromService( + "not_a_service", + "getServiceConfig" + ); + } catch (caught) { + error = caught; + } + expect(error).toBeDefined(); + expect(error.message).toEqual( + "not_a_service is not a message service" + ); + }); + }); + describe("when the service doesn't have the function", () => { + it("returns null", async () => { + const fn = serviceMap.tryGetFunctionFromService( + "serviceWithout", + "getServiceConfig" + ); + expect(fn).toBeNull(); + }); + }); }); - describe("when the service doesn't exist", () => { + }); + + describe("getServiceMetadata", () => { + describe("service doesn't have the function", () => { + beforeEach(() => { + jest + .spyOn(serviceMap, "tryGetFunctionFromService") + .mockReturnValue(null); + }); it("throws an exception", async () => { let error; try { - serviceMap.tryGetFunctionFromService( - "not_a_service", - "getServiceConfig" - ); + serviceMap.getServiceMetadata("incomplete_service"); } catch (caught) { error = caught; } - expect(error).toBeDefined(); - expect(error.message).toEqual("not_a_service is not a message service"); + expect(serviceMap.tryGetFunctionFromService.mock.calls).toEqual([ + ["incomplete_service", "getMetadata"] + ]); + expect(error.message).toEqual( + "Message service incomplete_service is missing required method getMetadata!" + ); }); }); - describe("when the service doesn't have the function", () => { - it("returns null", async () => { - const fn = serviceMap.tryGetFunctionFromService( - "serviceWithout", - "getServiceConfig" - ); - expect(fn).toBeNull(); + describe("twilio", () => { + it("returns the metadata", () => { + const metadata = serviceMap.getServiceMetadata("twilio"); + expect(metadata).toEqual({ + name: "twilio", + supportsCampaignConfig: false, + supportsOrgConfig: true, + type: "SMS" + }); + }); + }); + describe("nexo", () => { + it("returns the metadata", () => { + const metadata = serviceMap.getServiceMetadata("nexmo"); + expect(metadata).toEqual({ + name: "nexmo", + supportsCampaignConfig: false, + supportsOrgConfig: false, + type: "SMS" + }); + }); + }); + describe("fakeservice", () => { + it("returns the metadata", () => { + const metadata = serviceMap.getServiceMetadata("fakeservice"); + expect(metadata).toEqual({ + name: "fakeservice", + supportsCampaignConfig: false, + supportsOrgConfig: false, + type: "SMS" + }); }); }); }); diff --git a/src/extensions/messaging_services/fakeservice/index.js b/src/extensions/messaging_services/fakeservice/index.js index 926e2bd15..cd3c761aa 100644 --- a/src/extensions/messaging_services/fakeservice/index.js +++ b/src/extensions/messaging_services/fakeservice/index.js @@ -11,6 +11,13 @@ import uuid from "uuid"; // that end up just in the db appropriately and then using sendReply() graphql // queries for the reception (rather than a real service) +export const getMetadata = () => ({ + supportsOrgConfig: false, + supportsCampaignConfig: false, + type: "SMS", + name: "fakeservice" +}); + async function sendMessage(message, contact, trx, organization, campaign) { const errorCode = message.text.match(/error(\d+)/); const changes = { @@ -165,5 +172,6 @@ export default { deleteNumbersInAreaCode, // useless unused stubs convertMessagePartsToMessage, - handleIncomingMessage + handleIncomingMessage, + getMetadata }; diff --git a/src/extensions/messaging_services/index.js b/src/extensions/messaging_services/index.js index 7003c4ef8..91236548a 100644 --- a/src/extensions/messaging_services/index.js +++ b/src/extensions/messaging_services/index.js @@ -7,7 +7,8 @@ import orgCache from "../../server/models/cacheable_queries/organization"; export { getConfigKey, getService, - tryGetFunctionFromService + tryGetFunctionFromService, + getServiceMetadata } from "./service_map"; // Each service needs the following api points: diff --git a/src/extensions/messaging_services/nexmo/index.js b/src/extensions/messaging_services/nexmo/index.js index 64598e3c3..d7dc78549 100644 --- a/src/extensions/messaging_services/nexmo/index.js +++ b/src/extensions/messaging_services/nexmo/index.js @@ -12,6 +12,13 @@ import { log } from "../../../lib"; // rejected: 114 // network error or other connection failure: 1 +export const getMetadata = () => ({ + supportsOrgConfig: false, + supportsCampaignConfig: false, + type: "SMS", + name: "nexmo" +}); + let nexmo = null; const MAX_SEND_ATTEMPTS = 5; if (process.env.NEXMO_API_KEY && process.env.NEXMO_API_SECRET) { @@ -256,5 +263,6 @@ export default { rentNewCell, sendMessage, handleDeliveryReport, - handleIncomingMessage + handleIncomingMessage, + getMetadata }; diff --git a/src/extensions/messaging_services/service_map.js b/src/extensions/messaging_services/service_map.js index 58eb1a6ae..e633dfcc8 100644 --- a/src/extensions/messaging_services/service_map.js +++ b/src/extensions/messaging_services/service_map.js @@ -13,6 +13,21 @@ export const getConfigKey = serviceName => `message_service_${serviceName}`; export const getService = serviceName => serviceMap[serviceName]; +export const getServiceMetadata = serviceName => { + const getMetadata = exports.tryGetFunctionFromService( + serviceName, + "getMetadata" + ); + + if (!getMetadata) { + throw new Error( + `Message service ${serviceName} is missing required method getMetadata!` + ); + } + + return getMetadata(); +}; + export const tryGetFunctionFromService = (serviceName, functionName) => { const messageService = exports.getService(serviceName); if (!messageService) { diff --git a/src/extensions/messaging_services/twilio/index.js b/src/extensions/messaging_services/twilio/index.js index 307cdc61e..2cb450b59 100644 --- a/src/extensions/messaging_services/twilio/index.js +++ b/src/extensions/messaging_services/twilio/index.js @@ -35,6 +35,13 @@ const TWILIO_SKIP_VALIDATION = getConfig("TWILIO_SKIP_VALIDATION"); const BULK_REQUEST_CONCURRENCY = 5; const MAX_NUMBERS_PER_BUY_JOB = getConfig("MAX_NUMBERS_PER_BUY_JOB") || 100; +export const getMetadata = () => ({ + supportsOrgConfig: true, + supportsCampaignConfig: false, + type: "SMS", + name: "twilio" +}); + export async function getTwilio(organization) { const { authToken, accountSid } = await getMessageServiceConfig( "twilio", @@ -977,5 +984,6 @@ export default { getTwilio, getServiceConfig, getMessageServiceSid, - updateConfig + updateConfig, + getMetadata }; From b9926e3ce6e5dd09e19bf65db19d0df55e41d982 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sat, 20 Feb 2021 19:13:21 -0500 Subject: [PATCH 039/191] function to set org features for tests --- __test__/test_helpers.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/__test__/test_helpers.js b/__test__/test_helpers.js index af6216065..c14843f14 100644 --- a/__test__/test_helpers.js +++ b/__test__/test_helpers.js @@ -187,16 +187,16 @@ export async function createOrganization(user, invite) { return result; } -export const ensureOrganizationTwilioWithMessagingService = async ( +export const updateOrganizationFeatures = async ( testOrganization, + newFeatures, testCampaign = null ) => { const organization = testOrganization.data.createOrganization; const existingFeatures = organization.features || {}; const features = { ...existingFeatures, - TWILIO_MESSAGE_SERVICE_SID: global.TWILIO_MESSAGE_SERVICE_SID, - service: "twilio" + ...newFeatures }; await r @@ -215,6 +215,21 @@ export const ensureOrganizationTwilioWithMessagingService = async ( } }; +export const ensureOrganizationTwilioWithMessagingService = async ( + testOrganization, + testCampaign = null +) => { + const newFeatures = { + TWILIO_MESSAGE_SERVICE_SID: global.TWILIO_MESSAGE_SERVICE_SID, + service: "twilio" + }; + return updateOrganizationFeatures( + testOrganization, + newFeatures, + testCampaign + ); +}; + export async function setTwilioAuth(user, organization) { const rootValue = {}; const accountSid = "test_twilio_account_sid"; From 770f914c5c8d65fd2d6d50ead278302e156099b9 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sat, 20 Feb 2021 19:14:15 -0500 Subject: [PATCH 040/191] resolver for organization messageServer --- __test__/server/api/organization.test.js | 74 ++++++++++++++++++++++++ src/server/api/organization.js | 22 ++++++- 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/__test__/server/api/organization.test.js b/__test__/server/api/organization.test.js index 619321762..5cef010c4 100644 --- a/__test__/server/api/organization.test.js +++ b/__test__/server/api/organization.test.js @@ -16,6 +16,9 @@ import { runGql, setupTest } from "../../test_helpers"; +import * as srcServerApiErrors from "../../../src/server/api/errors"; +import * as orgCache from "../../../src/server/models/cacheable_queries/organization"; +import * as serviceMap from "../../../src/extensions/messaging_services/service_map"; const ActionHandlerFramework = require("../../../src/extensions/action-handlers"); @@ -45,6 +48,7 @@ describe("organization", async () => { afterEach(async () => { await cleanupTest(); + jest.restoreAllMocks(); if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); @@ -318,4 +322,74 @@ describe("organization", async () => { ]); }); }); + + describe.only(".messageService", () => { + let gqlQuery; + let variables; + let fakeConfig; + let fakeMetadata; + beforeEach(async () => { + fakeConfig = { fake: "faker_and_faker" }; + fakeMetadata = { + name: "super_fake", + type: "SMS", + supportsOrgConfig: true, + supportsCampaignConfig: false + }; + + jest + .spyOn(srcServerApiErrors, "accessRequired") + .mockImplementation(() => {}); + jest + .spyOn(orgCache.default, "getMessageService") + .mockReturnValue("fake_fake_fake"); + jest + .spyOn(orgCache.default, "getMessageServiceConfig") + .mockReturnValue(fakeConfig); + jest + .spyOn(serviceMap, "getServiceMetadata") + .mockReturnValue(fakeMetadata); + + gqlQuery = gql` + query messageService($organizationId: String!) { + organization(id: $organizationId) { + messageService { + name + type + supportsOrgConfig + supportsCampaignConfig + config + } + } + } + `; + + variables = { + organizationId: testOrganization.data.createOrganization.id + }; + }); + it("calls functions and returns the result", async () => { + const result = await runGql(gqlQuery, variables, testAdminUser); + expect(result.data.organization.messageService).toEqual({ + ...fakeMetadata, + config: fakeConfig + }); + expect(srcServerApiErrors.accessRequired.mock.calls[1]).toEqual([ + expect.objectContaining({ + auth0_id: "test123" + }), + parseInt(testOrganization.data.createOrganization.id, 10), + "OWNER" + ]); + expect(orgCache.default.getMessageService.mock.calls).toEqual([ + [expect.objectContaining({ id: 1 })] + ]); + expect(serviceMap.getServiceMetadata.mock.calls).toEqual([ + ["fake_fake_fake"] + ]); + expect(orgCache.default.getMessageServiceConfig.mock.calls).toEqual([ + [expect.objectContaining({ id: 1 })] + ]); + }); + }); }); diff --git a/src/server/api/organization.js b/src/server/api/organization.js index 5a5e4d45d..fa29b9016 100644 --- a/src/server/api/organization.js +++ b/src/server/api/organization.js @@ -9,7 +9,10 @@ import { getAvailableActionHandlers, getActionChoiceData } from "../../extensions/action-handlers"; -import { fullyConfigured } from "../../extensions/messaging_services"; +import { + fullyConfigured, + getServiceMetadata +} from "../../extensions/messaging_services"; export const ownerConfigurable = { // ACTION_HANDLERS: 1, @@ -240,6 +243,23 @@ export const resolvers = { return null; } }, + messageService: async (organization, _, { user }) => { + try { + await accessRequired(user, organization.id, "OWNER"); + const serviceName = cacheableData.organization.getMessageService( + organization + ); + const serviceMetadata = getServiceMetadata(serviceName); + return { + ...serviceMetadata, + config: cacheableData.organization.getMessageServiceConfig( + organization + ) + }; + } catch (caught) { + return null; + } + }, fullyConfigured: async organization => { return fullyConfigured(organization); }, From 3abe6c73049a8e3a83daf2f29048f06988ee97fc Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sat, 20 Feb 2021 19:46:40 -0500 Subject: [PATCH 041/191] get rid of only --- __test__/server/api/organization.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__test__/server/api/organization.test.js b/__test__/server/api/organization.test.js index 5cef010c4..26511a770 100644 --- a/__test__/server/api/organization.test.js +++ b/__test__/server/api/organization.test.js @@ -323,7 +323,7 @@ describe("organization", async () => { }); }); - describe.only(".messageService", () => { + describe(".messageService", () => { let gqlQuery; let variables; let fakeConfig; From 7342d83b68563fe2f96776eb21b3ca936896fad3 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sat, 20 Feb 2021 19:47:20 -0500 Subject: [PATCH 042/191] get twilio supportsOrgConfig from TWILIO_MULTI_ORG --- .../messaging_services/service_map.test.js | 23 ++++++++++++++++++- .../messaging_services/twilio/index.js | 2 +- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/__test__/extensions/messaging_services/service_map.test.js b/__test__/extensions/messaging_services/service_map.test.js index 8f7de3e26..d84441ad4 100644 --- a/__test__/extensions/messaging_services/service_map.test.js +++ b/__test__/extensions/messaging_services/service_map.test.js @@ -93,15 +93,36 @@ describe("service_map", () => { }); }); describe("twilio", () => { + const oldProcessEnv = process.env; + beforeEach(async () => { + process.env.TWILIO_MULTI_ORG = false; + }); + afterEach(async () => { + process.env = oldProcessEnv; + }); it("returns the metadata", () => { const metadata = serviceMap.getServiceMetadata("twilio"); expect(metadata).toEqual({ name: "twilio", supportsCampaignConfig: false, - supportsOrgConfig: true, + supportsOrgConfig: false, type: "SMS" }); }); + describe("when TWILIO_MULTI_ORG is true", () => { + beforeEach(async () => { + process.env.TWILIO_MULTI_ORG = true; + }); + it("returns the metadata", () => { + const metadata = serviceMap.getServiceMetadata("twilio"); + expect(metadata).toEqual({ + name: "twilio", + supportsCampaignConfig: false, + supportsOrgConfig: true, + type: "SMS" + }); + }); + }); }); describe("nexo", () => { it("returns the metadata", () => { diff --git a/src/extensions/messaging_services/twilio/index.js b/src/extensions/messaging_services/twilio/index.js index 2cb450b59..966d43a32 100644 --- a/src/extensions/messaging_services/twilio/index.js +++ b/src/extensions/messaging_services/twilio/index.js @@ -36,7 +36,7 @@ const BULK_REQUEST_CONCURRENCY = 5; const MAX_NUMBERS_PER_BUY_JOB = getConfig("MAX_NUMBERS_PER_BUY_JOB") || 100; export const getMetadata = () => ({ - supportsOrgConfig: true, + supportsOrgConfig: getConfig("TWILIO_MULTI_ORG", null, { truthy: true }), supportsCampaignConfig: false, type: "SMS", name: "twilio" From c923fe69792c62bd4adad22d73ee566a72ad4558 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sun, 21 Feb 2021 13:45:13 -0500 Subject: [PATCH 043/191] start to make org config pluggable --- src/containers/Settings.jsx | 171 +++++------------- .../messaging_services/react-components.js | 16 ++ .../twilio/react-components/org-config.js | 164 +++++++++++++++++ 3 files changed, 222 insertions(+), 129 deletions(-) create mode 100644 src/extensions/messaging_services/react-components.js create mode 100644 src/extensions/messaging_services/twilio/react-components/org-config.js diff --git a/src/containers/Settings.jsx b/src/containers/Settings.jsx index ae3f4548a..da5ee3506 100644 --- a/src/containers/Settings.jsx +++ b/src/containers/Settings.jsx @@ -1,3 +1,4 @@ +/* eslint no-console: 0 */ import PropTypes from "prop-types"; import React from "react"; import loadData from "./hoc/load-data"; @@ -8,7 +9,6 @@ import Dialog from "material-ui/Dialog"; import GSSubmitButton from "../components/forms/GSSubmitButton"; import FlatButton from "material-ui/FlatButton"; import RaisedButton from "material-ui/RaisedButton"; -import DisplayLink from "../components/DisplayLink"; import yup from "yup"; import { Card, CardText, CardActions, CardHeader } from "material-ui/Card"; import { StyleSheet, css } from "aphrodite"; @@ -17,6 +17,7 @@ import Toggle from "material-ui/Toggle"; import moment from "moment"; import CampaignTexterUIForm from "../components/CampaignTexterUIForm"; import OrganizationFeatureSettings from "../components/OrganizationFeatureSettings"; +import getMessagingServiceConfigComponent from "../extensions/messaging_services/react-components"; const styles = StyleSheet.create({ section: { @@ -125,138 +126,34 @@ class Settings extends React.Component { ); } - handleOpenTwilioDialog = () => this.setState({ twilioDialogOpen: true }); - - handleCloseTwilioDialog = () => this.setState({ twilioDialogOpen: false }); - - handleSubmitTwilioAuthForm = async ({ - accountSid, - authToken, - messageServiceSid - }) => { - const res = await this.props.mutations.updateTwilioAuth( - accountSid, - authToken === "" ? false : authToken, - messageServiceSid - ); - if (res.errors) { - this.setState({ twilioError: res.errors.message }); - } else { - this.setState({ twilioError: undefined }); + renderMessageServiceConfig() { + const { messageService } = this.props.data.organization; + if (!messageService) { + return null; } - this.handleCloseTwilioDialog(); - }; - renderTwilioAuthForm() { - const { organization } = this.props.data; - const { - twilioAccountSid, - twilioAuthToken, - twilioMessageServiceSid - } = organization; - const allSet = - twilioAccountSid && twilioAuthToken && twilioMessageServiceSid; - let baseUrl = "http://base"; - if (typeof window !== "undefined") { - baseUrl = window.location.origin; + const { name, supportsOrgConfig, config } = messageService; + if (!supportsOrgConfig) { + return null; } - const formSchema = yup.object({ - accountSid: yup - .string() - .nullable() - .max(64), - authToken: yup - .string() - .nullable() - .max(64), - messageServiceSid: yup - .string() - .nullable() - .max(64) - }); - const dialogActions = [ - , - - ]; + console.log("BEFORE"); - return ( - - - {allSet && ( - - - - )} - {this.state.twilioError && ( - - {this.state.twilioError} - - )} - -
- - You can set Twilio API credentials specifically for this - Organization by entering them here. - - - - - + const ConfigMessageService = getMessagingServiceConfigComponent(name); + if (!ConfigMessageService) { + return null; + } - - - Changing the Account SID or Messaging Service SID will break any - campaigns that are currently running. Do you want to contunue? - - -
-
-
+ console.log("AFTER"); + console.log("this.props.saveLabel", this.props.saveLabel); + return ( + {}} + /> ); } @@ -337,7 +234,7 @@ class Settings extends React.Component {
{this.renderTextingHoursForm()}
- {window.TWILIO_MULTI_ORG && this.renderTwilioAuthForm()} + {this.renderMessageServiceConfig()} {this.props.data.organization && this.props.data.organization.texterUIConfig && this.props.data.organization.texterUIConfig.sideboxChoices.length ? ( @@ -430,7 +327,8 @@ class Settings extends React.Component { Settings.propTypes = { data: PropTypes.object, params: PropTypes.object, - mutations: PropTypes.object + mutations: PropTypes.object, + saveLabel: PropTypes.string }; const queries = { @@ -457,6 +355,13 @@ const queries = { twilioAccountSid twilioAuthToken twilioMessageServiceSid + messageService { + name + type + supportsOrgConfig + supportsCampaignConfig + config + } } } `, @@ -600,6 +505,14 @@ const mutations = { optOutMessage } }), + updateMessageServiceConfig: ownProps => newConfig => ({ + mutation: updateMessageServiceConfigGql, + variables: { + organizationId: ownProps.params.organizationId, + messageServiceName: ownProps.organization.messageService.name, + config: JSON.stringify(newConfig) + } + }), updateTwilioAuth: ownProps => (accountSid, authToken, messageServiceSid) => ({ mutation: updateTwilioAuthGql, variables: { diff --git a/src/extensions/messaging_services/react-components.js b/src/extensions/messaging_services/react-components.js new file mode 100644 index 000000000..58329eff9 --- /dev/null +++ b/src/extensions/messaging_services/react-components.js @@ -0,0 +1,16 @@ +/* eslint no-console: 0 */ +export const orgConfig = serviceName => { + try { + // eslint-disable-next-line global-require + const component = require(`./${serviceName}/react-components/org-config.js`); + return component.OrgConfig; + } catch (caught) { + console.log("caught", caught); + console.error( + `MESSAGING_SERVICES failed to load orgConfig reaction component for ${serviceName}` + ); + return null; + } +}; + +export default orgConfig; diff --git a/src/extensions/messaging_services/twilio/react-components/org-config.js b/src/extensions/messaging_services/twilio/react-components/org-config.js new file mode 100644 index 000000000..f687a754c --- /dev/null +++ b/src/extensions/messaging_services/twilio/react-components/org-config.js @@ -0,0 +1,164 @@ +import { css } from "aphrodite"; +import { Card, CardHeader, CardText } from "material-ui/Card"; +import Dialog from "material-ui/Dialog"; +import FlatButton from "material-ui/FlatButton"; +import PropTypes from "prop-types"; +import React from "react"; +import Form from "react-formal"; +import yup from "yup"; +import DisplayLink from "../../../../components/DisplayLink"; +import GSForm from "../../../../components/forms/GSForm"; +import GSSubmitButton from "../../../../components/forms/GSSubmitButton"; +import theme from "../../../../styles/theme"; + +export class OrgConfig extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + + handleOpenTwilioDialog = () => this.setState({ twilioDialogOpen: true }); + + handleCloseTwilioDialog = () => this.setState({ twilioDialogOpen: false }); + + handleSubmitTwilioAuthForm = async ({ + accountSid, + authToken, + messageServiceSid + }) => { + // const res = await this.props.mutations.updateTwilioAuth( + // accountSid, + // authToken === "" ? false : authToken, + // messageServiceSid + // ); + // if (res.errors) { + // this.setState({ twilioError: res.errors.message }); + // } else { + // this.setState({ twilioError: undefined }); + // } + // this.handleCloseTwilioDialog(); + }; + + render() { + const { inlineStyles, styles, organization } = this.props; + const { + twilioAccountSid, + twilioAuthToken, + twilioMessageServiceSid + } = organization; + const allSet = + twilioAccountSid && twilioAuthToken && twilioMessageServiceSid; + let baseUrl = "http://base"; + if (typeof window !== "undefined") { + baseUrl = window.location.origin; + } + const formSchema = yup.object({ + accountSid: yup + .string() + .nullable() + .max(64), + authToken: yup + .string() + .nullable() + .max(64), + messageServiceSid: yup + .string() + .nullable() + .max(64) + }); + + const dialogActions = [ + , + + ]; + + return ( + + + {allSet && ( + + + + )} + {this.state.twilioError && ( + + {this.state.twilioError} + + )} + +
+ + You can set Twilio API credentials specifically for this + Organization by entering them here. + + + + + + + + + Changing the Account SID or Messaging Service SID will break any + campaigns that are currently running. Do you want to contunue? + + +
+
+
+ ); + } +} + +OrgConfig.propTypes = { + organization: PropTypes.object, + inlineStyles: PropTypes.object, + styles: PropTypes.object, + saveLabel: PropTypes.string, + onSubmit: PropTypes.func +}; + +export default OrgConfig; From 6c34d9ed1166653e7312426a89187a389b4c2b0f Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sun, 21 Feb 2021 15:44:13 -0500 Subject: [PATCH 044/191] Twilio react component WIP --- src/containers/Settings.jsx | 34 ++++++++++++------- .../twilio/react-components/org-config.js | 21 +++++------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/containers/Settings.jsx b/src/containers/Settings.jsx index da5ee3506..4e72af719 100644 --- a/src/containers/Settings.jsx +++ b/src/containers/Settings.jsx @@ -127,7 +127,7 @@ class Settings extends React.Component { } renderMessageServiceConfig() { - const { messageService } = this.props.data.organization; + const { id: organizationId, messageService } = this.props.data.organization; if (!messageService) { return null; } @@ -137,23 +137,33 @@ class Settings extends React.Component { return null; } - console.log("BEFORE"); - const ConfigMessageService = getMessagingServiceConfigComponent(name); if (!ConfigMessageService) { return null; } - console.log("AFTER"); - console.log("this.props.saveLabel", this.props.saveLabel); return ( - {}} - /> + + + {}} + onAllSetChanged={allSet => { + this.setState({ messageServiceAllSet: allSet }); + }} + /> + ); } diff --git a/src/extensions/messaging_services/twilio/react-components/org-config.js b/src/extensions/messaging_services/twilio/react-components/org-config.js index f687a754c..fb20d01b6 100644 --- a/src/extensions/messaging_services/twilio/react-components/org-config.js +++ b/src/extensions/messaging_services/twilio/react-components/org-config.js @@ -1,5 +1,5 @@ import { css } from "aphrodite"; -import { Card, CardHeader, CardText } from "material-ui/Card"; +import { CardText } from "material-ui/Card"; import Dialog from "material-ui/Dialog"; import FlatButton from "material-ui/FlatButton"; import PropTypes from "prop-types"; @@ -9,12 +9,12 @@ import yup from "yup"; import DisplayLink from "../../../../components/DisplayLink"; import GSForm from "../../../../components/forms/GSForm"; import GSSubmitButton from "../../../../components/forms/GSSubmitButton"; -import theme from "../../../../styles/theme"; export class OrgConfig extends React.Component { constructor(props) { super(props); this.state = {}; + this.props.onAllSetChanged(false); } handleOpenTwilioDialog = () => this.setState({ twilioDialogOpen: true }); @@ -82,13 +82,7 @@ export class OrgConfig extends React.Component { ]; return ( - - +
{allSet && (
- + ); } } OrgConfig.propTypes = { - organization: PropTypes.object, + organizationId: PropTypes.string, + config: PropTypes.object, inlineStyles: PropTypes.object, styles: PropTypes.object, saveLabel: PropTypes.string, - onSubmit: PropTypes.func + onSubmit: PropTypes.func, + onAllSetChanged: PropTypes.func }; export default OrgConfig; From 221f6176334926993f2ab80596327a2cbb7a3454 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sun, 21 Feb 2021 18:51:11 -0500 Subject: [PATCH 045/191] Ongoing WIP --- src/containers/Settings.jsx | 6 +- .../messaging_services/twilio/index.js | 3 +- .../twilio/react-components/org-config.js | 67 ++++++++++++------- .../mutations/updateMessageServiceConfig.js | 2 +- 4 files changed, 48 insertions(+), 30 deletions(-) diff --git a/src/containers/Settings.jsx b/src/containers/Settings.jsx index 4e72af719..873196daf 100644 --- a/src/containers/Settings.jsx +++ b/src/containers/Settings.jsx @@ -158,7 +158,9 @@ class Settings extends React.Component { inlineStyles={inlineStyles} styles={styles} saveLabel={this.props.saveLabel} - onSubmit={() => {}} + onSubmit={newConfig => { + return this.props.mutations.updateMessageServiceConfig(newConfig); + }} onAllSetChanged={allSet => { this.setState({ messageServiceAllSet: allSet }); }} @@ -519,7 +521,7 @@ const mutations = { mutation: updateMessageServiceConfigGql, variables: { organizationId: ownProps.params.organizationId, - messageServiceName: ownProps.organization.messageService.name, + messageServiceName: ownProps.data.organization.messageService.name, config: JSON.stringify(newConfig) } }), diff --git a/src/extensions/messaging_services/twilio/index.js b/src/extensions/messaging_services/twilio/index.js index 966d43a32..616a93d7a 100644 --- a/src/extensions/messaging_services/twilio/index.js +++ b/src/extensions/messaging_services/twilio/index.js @@ -888,7 +888,6 @@ export const getMessageServiceSid = async ( // TODO(lperson) maybe we should support backward compatibility here? export const updateConfig = async (oldConfig, config) => { const { twilioAccountSid, twilioAuthToken, twilioMessageServiceSid } = config; - if (!twilioAccountSid || !twilioMessageServiceSid) { throw new Error( "twilioAccountSid and twilioMessageServiceSid are required" @@ -912,7 +911,7 @@ export const updateConfig = async (oldConfig, config) => { const twilio = twilioLibrary.default.Twilio( twilioAccountSid, twilioAuthToken - ); // eslint-disable-line new-cap + ); await twilio.api.accounts.list(); } } catch (err) { diff --git a/src/extensions/messaging_services/twilio/react-components/org-config.js b/src/extensions/messaging_services/twilio/react-components/org-config.js index fb20d01b6..c3a6c732a 100644 --- a/src/extensions/messaging_services/twilio/react-components/org-config.js +++ b/src/extensions/messaging_services/twilio/react-components/org-config.js @@ -13,8 +13,26 @@ import GSSubmitButton from "../../../../components/forms/GSSubmitButton"; export class OrgConfig extends React.Component { constructor(props) { super(props); - this.state = {}; - this.props.onAllSetChanged(false); + const { accountSid, authToken, messageServiceSid } = this.props.config; + const allSet = accountSid && authToken && messageServiceSid; + this.state = { allSet }; + this.props.onAllSetChanged(allSet); + } + + componentDidUpdate(prevProps) { + const { + accountSid: prevAccountSid, + authToken: prevAuthToken, + messageServiceSid: prevMessageServiceSid + } = prevProps.config; + const prevAllSet = prevAccountSid && prevAuthToken && prevMessageServiceSid; + + const { accountSid, authToken, messageServiceSid } = this.props.config; + const allSet = accountSid && authToken && messageServiceSid; + + if (!!prevAllSet !== !!allSet) { + this.props.onAllSetChanged(allSet); + } } handleOpenTwilioDialog = () => this.setState({ twilioDialogOpen: true }); @@ -26,28 +44,27 @@ export class OrgConfig extends React.Component { authToken, messageServiceSid }) => { - // const res = await this.props.mutations.updateTwilioAuth( - // accountSid, - // authToken === "" ? false : authToken, - // messageServiceSid - // ); - // if (res.errors) { - // this.setState({ twilioError: res.errors.message }); - // } else { - // this.setState({ twilioError: undefined }); - // } - // this.handleCloseTwilioDialog(); + try { + const res = await this.props.onSubmit({ + twilioAccountSid: accountSid, + twilioAuthToken: authToken === "" ? false : authToken, + twilioMessageServiceSid: messageServiceSid + }); + if (res.errors) { + this.setState({ twilioError: res.errors.message }); + } else { + this.setState({ twilioError: undefined }); + } + } catch (caught) { + console.log("caught", typeof caught, JSON.stringify(caught)); + } + this.handleCloseTwilioDialog(); }; render() { - const { inlineStyles, styles, organization } = this.props; - const { - twilioAccountSid, - twilioAuthToken, - twilioMessageServiceSid - } = organization; - const allSet = - twilioAccountSid && twilioAuthToken && twilioMessageServiceSid; + const { organizationId, inlineStyles, styles, config } = this.props; + const { accountSid, authToken, messageServiceSid } = config; + const allSet = accountSid && authToken && messageServiceSid; let baseUrl = "http://base"; if (typeof window !== "undefined") { baseUrl = window.location.origin; @@ -86,7 +103,7 @@ export class OrgConfig extends React.Component { {allSet && ( @@ -107,9 +124,9 @@ export class OrgConfig extends React.Component { onChange={this.onFormChange} onSubmit={this.handleSubmitTwilioAuthForm} defaultValue={{ - accountSid: twilioAccountSid, - authToken: twilioAuthToken, - messageServiceSid: twilioMessageServiceSid + accountSid, + authToken, + messageServiceSid }} > Date: Mon, 22 Feb 2021 07:00:08 -0500 Subject: [PATCH 046/191] error handling --- .../twilio/react-components/org-config.js | 17 +++++++++++------ .../api/mutations/updateMessageServiceConfig.js | 9 ++++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/extensions/messaging_services/twilio/react-components/org-config.js b/src/extensions/messaging_services/twilio/react-components/org-config.js index c3a6c732a..c276c295f 100644 --- a/src/extensions/messaging_services/twilio/react-components/org-config.js +++ b/src/extensions/messaging_services/twilio/react-components/org-config.js @@ -1,3 +1,4 @@ +/* eslint no-console: 0 */ import { css } from "aphrodite"; import { CardText } from "material-ui/Card"; import Dialog from "material-ui/Dialog"; @@ -44,19 +45,23 @@ export class OrgConfig extends React.Component { authToken, messageServiceSid }) => { + let twilioError; try { - const res = await this.props.onSubmit({ + await this.props.onSubmit({ twilioAccountSid: accountSid, twilioAuthToken: authToken === "" ? false : authToken, twilioMessageServiceSid: messageServiceSid }); - if (res.errors) { - this.setState({ twilioError: res.errors.message }); + this.setState({ twilioError: undefined }); + } catch (caught) { + console.log("Error submitting Twilio auth", JSON.stringify(caught)); + if (caught.graphQLErrors && caught.graphQLErrors.length > 0) { + const twilioErrors = caught.graphQLErrors.map(error => error.message); + twilioError = twilioErrors.join(","); } else { - this.setState({ twilioError: undefined }); + twilioError = caught.message; } - } catch (caught) { - console.log("caught", typeof caught, JSON.stringify(caught)); + this.setState({ twilioError }); } this.handleCloseTwilioDialog(); }; diff --git a/src/server/api/mutations/updateMessageServiceConfig.js b/src/server/api/mutations/updateMessageServiceConfig.js index c58a7e0e2..1970c3110 100644 --- a/src/server/api/mutations/updateMessageServiceConfig.js +++ b/src/server/api/mutations/updateMessageServiceConfig.js @@ -56,10 +56,13 @@ export const updateMessageServiceConfig = async ( try { newConfig = await serviceConfigFunction(existingConfig, configObject); } catch (caught) { - const message = `Error updating config for ${messageServiceName}: ${caught}`; // eslint-disable-next-line no-console - console.error(message); - throw new GraphQLError(message); + console.error( + `Error updating config for ${messageServiceName}: ${JSON.stringify( + caught + )}` + ); + throw new GraphQLError(caught.message); } const dbOrganization = await Organization.get(organizationId); From 0d8bff313becb6ad7aa7151b6517490a91494adb Mon Sep 17 00:00:00 2001 From: Larry Person Date: Mon, 22 Feb 2021 07:13:28 -0500 Subject: [PATCH 047/191] fix test --- .../server/api/mutations/updateMessageServiceConfig.test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/__test__/server/api/mutations/updateMessageServiceConfig.test.js b/__test__/server/api/mutations/updateMessageServiceConfig.test.js index 828892e27..97e936fe6 100644 --- a/__test__/server/api/mutations/updateMessageServiceConfig.test.js +++ b/__test__/server/api/mutations/updateMessageServiceConfig.test.js @@ -130,9 +130,7 @@ describe("updateMessageServiceConfig", () => { it("returns an error", async () => { const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); - expect(gqlResult.errors[0].message).toEqual( - "Error updating config for twilio: Error: OH NO!" - ); + expect(gqlResult.errors[0].message).toEqual("OH NO!"); }); }); From fd99fe7fb23071675744767f2175af3e87ebf767 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sun, 7 Mar 2021 12:05:03 -0500 Subject: [PATCH 048/191] org-config error box style and request refetch --- src/containers/Settings.jsx | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/containers/Settings.jsx b/src/containers/Settings.jsx index 873196daf..cb896d6de 100644 --- a/src/containers/Settings.jsx +++ b/src/containers/Settings.jsx @@ -44,6 +44,11 @@ const inlineStyles = { }, shadeBox: { backgroundColor: theme.colors.lightGray + }, + errorBox: { + backgroundColor: theme.colors.lightGray, + color: theme.colors.darkRed, + fontWeight: "bolder" } }; @@ -145,7 +150,7 @@ class Settings extends React.Component { return ( { this.setState({ messageServiceAllSet: allSet }); }} + requestRefetch={async () => { + return this.props.data.refetch(); + }} /> ); @@ -517,14 +525,16 @@ const mutations = { optOutMessage } }), - updateMessageServiceConfig: ownProps => newConfig => ({ - mutation: updateMessageServiceConfigGql, - variables: { - organizationId: ownProps.params.organizationId, - messageServiceName: ownProps.data.organization.messageService.name, - config: JSON.stringify(newConfig) - } - }), + updateMessageServiceConfig: ownProps => newConfig => { + return { + mutation: updateMessageServiceConfigGql, + variables: { + organizationId: ownProps.params.organizationId, + messageServiceName: ownProps.data.organization.messageService.name, + config: JSON.stringify(newConfig) + } + }; + }, updateTwilioAuth: ownProps => (accountSid, authToken, messageServiceSid) => ({ mutation: updateTwilioAuthGql, variables: { From 318f4186ad81f2e9a418d4cda858559ce963b58a Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sun, 7 Mar 2021 12:07:46 -0500 Subject: [PATCH 049/191] restrict to org features --> localOnly --- src/extensions/messaging_services/service_map.js | 12 +++++++++--- src/server/api/lib/config.js | 4 ++-- src/server/api/organization.js | 3 ++- src/server/models/cacheable_queries/organization.js | 8 ++++++-- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/extensions/messaging_services/service_map.js b/src/extensions/messaging_services/service_map.js index e633dfcc8..472163423 100644 --- a/src/extensions/messaging_services/service_map.js +++ b/src/extensions/messaging_services/service_map.js @@ -37,7 +37,11 @@ export const tryGetFunctionFromService = (serviceName, functionName) => { return fn && typeof fn === "function" ? fn : null; }; -export const getMessageServiceConfig = async (serviceName, organization) => { +export const getMessageServiceConfig = async ( + serviceName, + organization, + options = {} +) => { const getServiceConfig = exports.tryGetFunctionFromService( serviceName, "getServiceConfig" @@ -46,8 +50,10 @@ export const getMessageServiceConfig = async (serviceName, organization) => { return null; } const configKey = exports.getConfigKey(serviceName); - const config = getConfig(configKey, organization); - return getServiceConfig(config, organization); + const config = getConfig(configKey, organization, { + onlyLocal: options.restrictToOrgFeatures + }); + return getServiceConfig(config, organization, options); }; export default serviceMap; diff --git a/src/server/api/lib/config.js b/src/server/api/lib/config.js index 22098d2c1..ff1d93b0c 100644 --- a/src/server/api/lib/config.js +++ b/src/server/api/lib/config.js @@ -60,8 +60,8 @@ export function getConfig(key, organization, opts) { return opts && opts.default; } -export function hasConfig(key, organization) { - const val = getConfig(key, organization); +export function hasConfig(key, organization, options = {}) { + const val = getConfig(key, organization, options); // we need to allow "" as no config since env vars will occasionally be set to that to undefine it return Boolean(typeof val !== "undefined" && val !== ""); } diff --git a/src/server/api/organization.js b/src/server/api/organization.js index fa29b9016..c462e9297 100644 --- a/src/server/api/organization.js +++ b/src/server/api/organization.js @@ -253,7 +253,8 @@ export const resolvers = { return { ...serviceMetadata, config: cacheableData.organization.getMessageServiceConfig( - organization + organization, + { restrictToOrgFeatures: true, obscureSensitiveInformation: true } ) }; } catch (caught) { diff --git a/src/server/models/cacheable_queries/organization.js b/src/server/models/cacheable_queries/organization.js index 8a7849aeb..e741fd36a 100644 --- a/src/server/models/cacheable_queries/organization.js +++ b/src/server/models/cacheable_queries/organization.js @@ -17,9 +17,13 @@ const organizationCache = { } }, getMessageService: getOrganizationMessageService, - getMessageServiceConfig: async organization => { + getMessageServiceConfig: async (organization, options = {}) => { + const { restrictToOrgFeatures, obscureSensitiveInformation } = options; const serviceName = getOrganizationMessageService(organization); - return getMessageServiceConfig(serviceName, organization); + return getMessageServiceConfig(serviceName, organization, { + restrictToOrgFeatures, + obscureSensitiveInformation + }); }, getMessageServiceSid: async (organization, contact, messageText) => { const messageServiceName = getOrganizationMessageService(organization); From 32a41072879cd333137e4fef81529b5174b0b830 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sun, 7 Mar 2021 12:08:41 -0500 Subject: [PATCH 050/191] Restrict to org features; encrypted vs. hidden --- .../messaging_services/twilio/index.js | 63 ++++++++++++++----- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/src/extensions/messaging_services/twilio/index.js b/src/extensions/messaging_services/twilio/index.js index 616a93d7a..926781236 100644 --- a/src/extensions/messaging_services/twilio/index.js +++ b/src/extensions/messaging_services/twilio/index.js @@ -830,17 +830,31 @@ async function clearMessagingServicePhones(organization, messagingServiceSid) { } } -export const getServiceConfig = async (serviceConfig, organization) => { +export const getServiceConfig = async ( + serviceConfig, + organization, + options = {} +) => { + const { + restrictToOrgFeatures = false, + obscureSensitiveInformation = false + } = options; let authToken; let accountSid; let messageServiceSid; if (serviceConfig) { - const hasOrgToken = serviceConfig.TWILIO_AUTH_TOKEN_ENCRYPTED; + const hasEncryptedToken = serviceConfig.TWILIO_AUTH_TOKEN_ENCRYPTED; // Note, allows unencrypted auth tokens to be (manually) stored in the db // @todo: decide if this is necessary, or if UI/envars is sufficient. - authToken = hasOrgToken - ? symmetricDecrypt(serviceConfig.TWILIO_AUTH_TOKEN_ENCRYPTED) - : serviceConfig.TWILIO_AUTH_TOKEN; + if (hasEncryptedToken) { + authToken = obscureSensitiveInformation + ? "" + : symmetricDecrypt(serviceConfig.TWILIO_AUTH_TOKEN_ENCRYPTED); + } else { + authToken = obscureSensitiveInformation + ? "" + : serviceConfig.TWILIO_AUTH_TOKEN; + } accountSid = serviceConfig.TWILIO_ACCOUNT_SID ? serviceConfig.TWILIO_ACCOUNT_SID : // Check old TWILIO_API_KEY variable for backwards compatibility. @@ -850,18 +864,40 @@ export const getServiceConfig = async (serviceConfig, organization) => { } else { // for backward compatibility - const hasOrgToken = hasConfig("TWILIO_AUTH_TOKEN_ENCRYPTED", organization); + const getConfigOptions = { onlyLocal: restrictToOrgFeatures }; + + const hasEncryptedToken = hasConfig( + "TWILIO_AUTH_TOKEN_ENCRYPTED", + organization, + getConfigOptions + ); // Note, allows unencrypted auth tokens to be (manually) stored in the db // @todo: decide if this is necessary, or if UI/envars is sufficient. - authToken = hasOrgToken - ? symmetricDecrypt(getConfig("TWILIO_AUTH_TOKEN_ENCRYPTED", organization)) - : getConfig("TWILIO_AUTH_TOKEN", organization); + if (hasEncryptedToken) { + authToken = obscureSensitiveInformation + ? "" + : symmetricDecrypt( + getConfig( + "TWILIO_AUTH_TOKEN_ENCRYPTED", + organization, + getConfigOptions + ) + ); + } else { + authToken = obscureSensitiveInformation + ? "" + : getConfig("TWILIO_AUTH_TOKEN", organization, getConfigOptions); + } accountSid = hasConfig("TWILIO_ACCOUNT_SID", organization) - ? getConfig("TWILIO_ACCOUNT_SID", organization) + ? getConfig("TWILIO_ACCOUNT_SID", organization, getConfigOptions) : // Check old TWILIO_API_KEY variable for backwards compatibility. - getConfig("TWILIO_API_KEY", organization); + getConfig("TWILIO_API_KEY", organization, getConfigOptions); - messageServiceSid = getConfig("TWILIO_MESSAGE_SERVICE_SID", organization); + messageServiceSid = getConfig( + "TWILIO_MESSAGE_SERVICE_SID", + organization, + getConfigOptions + ); } return { authToken, accountSid, messageServiceSid }; }; @@ -885,7 +921,6 @@ export const getMessageServiceSid = async ( return messageServiceSid; }; -// TODO(lperson) maybe we should support backward compatibility here? export const updateConfig = async (oldConfig, config) => { const { twilioAccountSid, twilioAuthToken, twilioMessageServiceSid } = config; if (!twilioAccountSid || !twilioMessageServiceSid) { @@ -898,7 +933,7 @@ export const updateConfig = async (oldConfig, config) => { newConfig.TWILIO_ACCOUNT_SID = twilioAccountSid.substr(0, 64); - // TODO(lperson) is twilioAuthToken required? + // TODO(lperson) is twilioAuthToken required? -- not for unencrypted newConfig.TWILIO_AUTH_TOKEN_ENCRYPTED = twilioAuthToken ? symmetricEncrypt(twilioAuthToken).substr(0, 256) : twilioAuthToken; From 7ab469672c23c5821bba1f3598d080d8d6a4b1cd Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sun, 7 Mar 2021 12:08:52 -0500 Subject: [PATCH 051/191] lint --- src/styles/theme.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/theme.js b/src/styles/theme.js index 861fe5372..3dc4b7921 100644 --- a/src/styles/theme.js +++ b/src/styles/theme.js @@ -3,7 +3,7 @@ import tinycolor from "tinycolor2"; const coreBackgroundColor = global.CORE_BACKGROUND_COLOR || "rgb(83, 180, 119)"; const colors = { - coreBackgroundColor: coreBackgroundColor, + coreBackgroundColor, coreBackgroundColorDisabled: tinycolor(coreBackgroundColor) .darken(10) .toHexString(), From 2a6e61aa141c1d13276b0ab0265a6dcdc7f593d9 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sun, 7 Mar 2021 12:09:17 -0500 Subject: [PATCH 052/191] legacy save --- src/server/api/mutations/updateMessageServiceConfig.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/server/api/mutations/updateMessageServiceConfig.js b/src/server/api/mutations/updateMessageServiceConfig.js index 1970c3110..e354eae47 100644 --- a/src/server/api/mutations/updateMessageServiceConfig.js +++ b/src/server/api/mutations/updateMessageServiceConfig.js @@ -66,9 +66,11 @@ export const updateMessageServiceConfig = async ( } const dbOrganization = await Organization.get(organizationId); + const features = JSON.parse(dbOrganization.features || "{}"); dbOrganization.features = JSON.stringify({ - ...JSON.parse(dbOrganization.features || "{}"), - [configKey]: newConfig + ...features, + ...(features[configKey] && { [configKey]: newConfig }), + ...(!features[configKey] && { [configKey]: newConfig }) }); await dbOrganization.save(); From 08af90b08db1e4ff6bde5ec51b038acdb3a04e08 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sun, 7 Mar 2021 12:09:33 -0500 Subject: [PATCH 053/191] iterative improvements --- .../twilio/react-components/org-config.js | 61 ++++++++++++++----- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/src/extensions/messaging_services/twilio/react-components/org-config.js b/src/extensions/messaging_services/twilio/react-components/org-config.js index c276c295f..d809e3dc8 100644 --- a/src/extensions/messaging_services/twilio/react-components/org-config.js +++ b/src/extensions/messaging_services/twilio/react-components/org-config.js @@ -3,6 +3,7 @@ import { css } from "aphrodite"; import { CardText } from "material-ui/Card"; import Dialog from "material-ui/Dialog"; import FlatButton from "material-ui/FlatButton"; +import { Table, TableBody, TableRow, TableRowColumn } from "material-ui/Table"; import PropTypes from "prop-types"; import React from "react"; import Form from "react-formal"; @@ -16,7 +17,7 @@ export class OrgConfig extends React.Component { super(props); const { accountSid, authToken, messageServiceSid } = this.props.config; const allSet = accountSid && authToken && messageServiceSid; - this.state = { allSet }; + this.state = { allSet, ...this.props.config }; this.props.onAllSetChanged(allSet); } @@ -36,15 +37,16 @@ export class OrgConfig extends React.Component { } } + onFormChange = value => { + this.setState(value); + }; + handleOpenTwilioDialog = () => this.setState({ twilioDialogOpen: true }); handleCloseTwilioDialog = () => this.setState({ twilioDialogOpen: false }); - handleSubmitTwilioAuthForm = async ({ - accountSid, - authToken, - messageServiceSid - }) => { + handleSubmitTwilioAuthForm = async p => { + const { accountSid, authToken, messageServiceSid } = p; let twilioError; try { await this.props.onSubmit({ @@ -52,7 +54,11 @@ export class OrgConfig extends React.Component { twilioAuthToken: authToken === "" ? false : authToken, twilioMessageServiceSid: messageServiceSid }); - this.setState({ twilioError: undefined }); + await this.props.requestRefetch(); + this.setState({ + twilioError: undefined, + authToken: this.props.config.authToken + }); } catch (caught) { console.log("Error submitting Twilio auth", JSON.stringify(caught)); if (caught.graphQLErrors && caught.graphQLErrors.length > 0) { @@ -111,10 +117,40 @@ export class OrgConfig extends React.Component { url={`${baseUrl}/twilio/${organizationId}`} textContent="Twilio credentials are configured for this organization. You should set the inbound Request URL in your Twilio messaging service to this link." /> + Settings for this organization: + + + + + Twilio Account SID + + + {this.props.config.accountSid} + + + + + Twilio Auth Token + + {this.props.config.authToken} + + + + Default Message Service SID + + + {this.props.config.messageServiceSid} + + + +
)} {this.state.twilioError && ( - + {this.state.twilioError} )} @@ -128,11 +164,7 @@ export class OrgConfig extends React.Component { schema={formSchema} onChange={this.onFormChange} onSubmit={this.handleSubmitTwilioAuthForm} - defaultValue={{ - accountSid, - authToken, - messageServiceSid - }} + defaultValue={this.state} > Date: Sun, 7 Mar 2021 15:07:29 -0500 Subject: [PATCH 054/191] fix tests --- __test__/server/api/organization.test.js | 5 ++++- .../models/cacheable_queries/organization.test.js | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/__test__/server/api/organization.test.js b/__test__/server/api/organization.test.js index 26511a770..63b91e4fa 100644 --- a/__test__/server/api/organization.test.js +++ b/__test__/server/api/organization.test.js @@ -388,7 +388,10 @@ describe("organization", async () => { ["fake_fake_fake"] ]); expect(orgCache.default.getMessageServiceConfig.mock.calls).toEqual([ - [expect.objectContaining({ id: 1 })] + [ + expect.objectContaining({ id: 1 }), + { obscureSensitiveInformation: true, restrictToOrgFeatures: true } + ] ]); }); }); diff --git a/__test__/server/models/cacheable_queries/organization.test.js b/__test__/server/models/cacheable_queries/organization.test.js index d746561f4..d7ab94bd0 100644 --- a/__test__/server/models/cacheable_queries/organization.test.js +++ b/__test__/server/models/cacheable_queries/organization.test.js @@ -55,7 +55,14 @@ describe("cacheable_queries.organization", () => { ]); expect(serviceMap.getConfigKey.mock.calls).toEqual([["serviceWith"]]); expect(serviceWith.getServiceConfig.mock.calls).toEqual([ - [undefined, organizationWith] + [ + undefined, + organizationWith, + { + obscureSensitiveInformation: undefined, + restirctToOrgFeatures: undefined + } + ] ]); }); describe("when an organization has a config", () => { @@ -71,7 +78,11 @@ describe("cacheable_queries.organization", () => { expect(serviceWith.getServiceConfig.mock.calls).toEqual([ [ organizationWithConfig.features.message_service_serviceWith, - organizationWithConfig + organizationWithConfig, + { + restirctToOrgFeatures: undefined, + obscureSensitiveInformation: undefined + } ] ]); }); From 3d6325116215551889ffd84a0f9c634a146b3ea9 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sun, 7 Mar 2021 15:41:03 -0500 Subject: [PATCH 055/191] test for getMessageServiceConfig --- .../messaging_services/service_map.test.js | 90 ++++++++++++++++++- 1 file changed, 87 insertions(+), 3 deletions(-) diff --git a/__test__/extensions/messaging_services/service_map.test.js b/__test__/extensions/messaging_services/service_map.test.js index d84441ad4..005ba59a0 100644 --- a/__test__/extensions/messaging_services/service_map.test.js +++ b/__test__/extensions/messaging_services/service_map.test.js @@ -1,4 +1,5 @@ import * as serviceMap from "../../../src/extensions/messaging_services/service_map"; +import * as config from "../../../src/server/api/lib/config"; describe("service_map", () => { afterEach(() => { @@ -23,14 +24,14 @@ describe("service_map", () => { .mockImplementation(serviceName => fakeServiceMap[serviceName]); }); - describe("getConfigKey", () => { + describe("#getConfigKey", () => { it("returns the correct config key", async () => { const configKey = serviceMap.getConfigKey("fake_service_name"); expect(configKey).toEqual("message_service_fake_service_name"); }); }); - describe("tryGetFunctionFromService", () => { + describe("#tryGetFunctionFromService", () => { it("returns the function", async () => { const fn = serviceMap.tryGetFunctionFromService( "serviceWith", @@ -70,7 +71,7 @@ describe("service_map", () => { }); }); - describe("getServiceMetadata", () => { + describe("#getServiceMetadata", () => { describe("service doesn't have the function", () => { beforeEach(() => { jest @@ -147,4 +148,87 @@ describe("service_map", () => { }); }); }); + + describe("#getMessageServiceConfig", () => { + let fakeGetServiceConfig; + let fakeOrganization; + let fakeOptions; + let fakeConfig; + let expectedGetConfigOptions; + beforeEach(async () => { + fakeOrganization = { id: 1 }; + fakeConfig = { fake_one: "fake1", fake_two: "fake2" }; + fakeOptions = { fake_option: "fakeOpt" }; + expectedGetConfigOptions = { onlyLocal: undefined }; + fakeGetServiceConfig = jest.fn(); + jest + .spyOn(serviceMap, "tryGetFunctionFromService") + .mockReturnValue(fakeGetServiceConfig); + jest.spyOn(serviceMap, "getConfigKey"); + jest.spyOn(config, "getConfig").mockReturnValue(fakeConfig); + }); + it("calls the functions", async () => { + await serviceMap.getMessageServiceConfig( + "fake_fake_service", + fakeOrganization, + fakeOptions + ); + expect(serviceMap.tryGetFunctionFromService.mock.calls).toEqual([ + ["fake_fake_service", "getServiceConfig"] + ]); + expect(serviceMap.getConfigKey.mock.calls).toEqual([ + ["fake_fake_service"] + ]); + expect(config.getConfig.mock.calls).toEqual([ + [ + "message_service_fake_fake_service", + fakeOrganization, + expectedGetConfigOptions + ] + ]); + expect(fakeGetServiceConfig.mock.calls).toEqual([ + [fakeConfig, fakeOrganization, fakeOptions] + ]); + }); + + describe("when restrctToOrgFeatures is truthy", () => { + beforeEach(async () => { + fakeOptions = { restrictToOrgFeatures: true }; + expectedGetConfigOptions = { onlyLocal: true }; + }); + it("passes onlyLocal to getConfig", async () => { + await serviceMap.getMessageServiceConfig( + "fake_fake_service", + fakeOrganization, + fakeOptions + ); + expect(config.getConfig.mock.calls).toEqual([ + [ + "message_service_fake_fake_service", + fakeOrganization, + expectedGetConfigOptions + ] + ]); + }); + }); + + describe("when the services doesn't support configuration", () => { + beforeEach(async () => { + jest + .spyOn(serviceMap, "tryGetFunctionFromService") + .mockReturnValue(undefined); + }); + it("returns null", async () => { + const returned = await serviceMap.getMessageServiceConfig( + "fake_fake_service", + fakeOrganization, + fakeOptions + ); + expect(returned).toEqual(null); + expect(serviceMap.getConfigKey).not.toHaveBeenCalled(); + expect(config.getConfig).not.toHaveBeenCalled(); + expect(fakeGetServiceConfig).not.toHaveBeenCalled(); + }); + }); + }); }); From d3a55391b32a12ba2bed47ca104da92c4253dea8 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Thu, 11 Mar 2021 10:10:12 -0500 Subject: [PATCH 056/191] more tests --- .../messaging_services/twilio.test.js | 78 +++++++++++++++++-- .../updateMessageServiceConfig.test.js | 18 ++++- 2 files changed, 89 insertions(+), 7 deletions(-) diff --git a/__test__/extensions/messaging_services/twilio.test.js b/__test__/extensions/messaging_services/twilio.test.js index 3f8c99d32..cb79e7b37 100644 --- a/__test__/extensions/messaging_services/twilio.test.js +++ b/__test__/extensions/messaging_services/twilio.test.js @@ -200,6 +200,7 @@ describe("twilio", () => { // We loop MAX_SEND_ATTEMPTS + 1 times (starting at i=1! // The last time, the send_status should update to "ERROR" (so we don't keep trying) try { + // eslint-disable-next-line no-loop-func await new Promise((resolve, reject) => { twilio.postMessageSend( message, @@ -501,6 +502,7 @@ describe("twilio", () => { describe("config functions", () => { let twilioConfig; + let encryptedTwilioConfig; let organization; let fakeAuthToken; let fakeAccountSid; @@ -519,21 +521,42 @@ describe("twilio", () => { TWILIO_ACCOUNT_SID: fakeAccountSid, TWILIO_MESSAGE_SERVICE_SID: fakeMessageServiceSid }; - + encryptedTwilioConfig = { + TWILIO_AUTH_TOKEN_ENCRYPTED: encryptedFakeAuthToken, + TWILIO_ACCOUNT_SID: fakeAccountSid, + TWILIO_MESSAGE_SERVICE_SID: fakeMessageServiceSid + }; jest .spyOn(crypto, "symmetricEncrypt") .mockReturnValue(encryptedFakeAuthToken); }); - describe("getServiceConfig", () => { + describe.only("getServiceConfig", () => { + let expectedConfig; + beforeEach(async () => { + expectedConfig = { + authToken: fakeAuthToken, + accountSid: fakeAccountSid, + messageServiceSid: fakeMessageServiceSid + }; + }); it("returns the config elements", async () => { const config = await twilio.getServiceConfig( twilioConfig, organization ); - expect(config).toEqual({ - authToken: fakeAuthToken, - accountSid: fakeAccountSid, - messageServiceSid: fakeMessageServiceSid + expect(config).toEqual(expectedConfig); + }); + describe("when obscureSensitiveInformation is true", () => { + beforeEach(async () => { + expectedConfig.authToken = ""; + }); + it("returns authToken obscured", async () => { + const config = await twilio.getServiceConfig( + twilioConfig, + organization, + { obscureSensitiveInformation: true } + ); + expect(config).toEqual(expectedConfig); }); }); describe("when the auth token is encrypted", () => { @@ -552,6 +575,19 @@ describe("twilio", () => { messageServiceSid: fakeMessageServiceSid }); }); + describe("when obscureSensitiveInformation is true", () => { + beforeEach(async () => { + expectedConfig.authToken = ""; + }); + it("returns authToken obscured", async () => { + const config = await twilio.getServiceConfig( + twilioConfig, + organization, + { obscureSensitiveInformation: true } + ); + expect(config).toEqual(expectedConfig); + }); + }); }); describe("when it has an API key instead of account sid", () => { beforeEach(async () => { @@ -582,12 +618,34 @@ describe("twilio", () => { messageServiceSid: fakeMessageServiceSid }); }); + it("returns global config if there is no org-specific config", async () => {}); + describe("when restrictToOrgFeatures is true", () => { + it("returns global configs", async () => {}); + }); + describe("when obscureSensitiveInformation is true", () => { + it("returns authToken obscured", async () => {}); + }); + describe("when obscureSensitiveInformation is false", () => { + it("returns authToken unobscured", async () => {}); + }); + + it("obscures senstive information by default", async () => {}); + it("restricts to org features (ignoring global configs)", async () => {}); + describe("when restrictToOrgFeatures is false", () => { + it("returns global configs", async () => {}); + }); describe("when the auth token is encrypted", () => { beforeEach(async () => { twilioConfig.TWILIO_AUTH_TOKEN_ENCRYPTED = encryptedFakeAuthToken; delete twilioConfig.TWILIO_AUTH_TOKEN; organization = { feature: { ...twilioConfig } }; }); + describe("when obscureSensitiveInformation is true", () => { + it("returns authToken obscured", async () => {}); + }); + describe("when obscureSensitiveInformation is false", () => { + it("returns authToken unobscured", async () => {}); + }); it("returns the config elements", async () => { const config = await twilio.getServiceConfig( undefined, @@ -600,6 +658,14 @@ describe("twilio", () => { }); }); }); + describe("when the auth token is not encrypted", () => { + describe("when obscureSensitiveInformation is false", () => { + it("returns authToken unobscured", async () => {}); + }); + describe("when obscureSensitiveInformation is true", () => { + it("returns authToken obscured", async () => {}); + }); + }); describe("when it has an API key instead of account sid", () => { beforeEach(async () => { twilioConfig.TWILIO_API_KEY = fakeApiKey; diff --git a/__test__/server/api/mutations/updateMessageServiceConfig.test.js b/__test__/server/api/mutations/updateMessageServiceConfig.test.js index 97e936fe6..d54056c05 100644 --- a/__test__/server/api/mutations/updateMessageServiceConfig.test.js +++ b/__test__/server/api/mutations/updateMessageServiceConfig.test.js @@ -51,7 +51,7 @@ describe("updateMessageServiceConfig", () => { }; }); - it("delegates to message service's updateConfig", async () => { + it("calls message service's updateConfig and other functions", async () => { const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); expect(twilio.updateConfig.mock.calls).toEqual([[undefined, newConfig]]); expect(orgCache.getMessageServiceConfig.mock.calls).toEqual([ @@ -65,8 +65,14 @@ describe("updateMessageServiceConfig", () => { ] ]); expect(gqlResult.data.updateMessageServiceConfig).toEqual(newConfig); + + // TODO + // expect cache.clear to have been called + // expect cache.load to have been called }); + it("updates the config in organization.features", async () => {}); + describe("when it's not the configured message service name", () => { beforeEach(async () => { vars.messageServiceName = "this will never be a message service name"; @@ -168,4 +174,14 @@ describe("updateMessageServiceConfig", () => { expect(gqlResult.data.updateMessageServiceConfig).toEqual(newConfig); }); }); + + describe("when the organization had no features", () => { + it("does not throw an exception", async () => {}); + }); + + describe("when updating legacy config (all config elements at the top level of organization.features)", () => { + // for example, for twilio, this is when all the config elements are not children of + // the `message_service_twilio` key in organization.features + it("updates the config at the top level", async () => {}); + }); }); From 51440f88a44ccc4cda789111de7b44d3f1d9fdbd Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sat, 27 Mar 2021 19:09:07 -0400 Subject: [PATCH 057/191] Twilio get service config with tests --- .../messaging_services/twilio.test.js | 909 +++++++++++------- .../messaging_services/twilio/index.js | 19 +- src/server/api/lib/config.js | 2 +- 3 files changed, 582 insertions(+), 348 deletions(-) diff --git a/__test__/extensions/messaging_services/twilio.test.js b/__test__/extensions/messaging_services/twilio.test.js index cb79e7b37..4b2c31738 100644 --- a/__test__/extensions/messaging_services/twilio.test.js +++ b/__test__/extensions/messaging_services/twilio.test.js @@ -2,7 +2,8 @@ import * as twilioLibrary from "twilio"; import { getLastMessage } from "../../../src/extensions/messaging_services/message-sending"; import * as twilio from "../../../src/extensions/messaging_services/twilio"; -import { getConfig } from "../../../src/server/api/lib/config"; +import { getConfig } from "../../../src/server/api/lib/config"; // eslint-disable-line no-duplicate-imports, import/no-duplicates +import * as configFunctions from "../../../src/server/api/lib/config"; // eslint-disable-line no-duplicate-imports, import/no-duplicates import crypto from "../../../src/server/api/lib/crypto"; import { cacheableData, Message, r } from "../../../src/server/models/"; import { erroredMessageSender } from "../../../src/workers/job-processes"; @@ -499,196 +500,438 @@ describe("twilio", () => { expect(inventoryCount).toEqual(12); }); }); +}); - describe("config functions", () => { - let twilioConfig; - let encryptedTwilioConfig; - let organization; - let fakeAuthToken; - let fakeAccountSid; - let fakeMessageServiceSid; - let encryptedFakeAuthToken; - let fakeApiKey; +describe.only("config functions", () => { + let twilioConfig; + let encryptedTwilioConfig; + let organization; + let fakeAuthToken; + let fakeAccountSid; + let fakeMessageServiceSid; + let encryptedFakeAuthToken; + let fakeApiKey; + let hiddenPlaceholder; + let encryptedPlaceHolder; + beforeEach(async () => { + hiddenPlaceholder = ""; + encryptedPlaceHolder = ""; + fakeAuthToken = "fake_twilio_auth_token"; + fakeAccountSid = "fake_twilio_account_sid"; + fakeMessageServiceSid = "fake_twilio_message_service_sid"; + encryptedFakeAuthToken = crypto.symmetricEncrypt(fakeAuthToken); + fakeApiKey = "fake_twilio_api_key"; + organization = { feature: { TWILIO_AUTH_TOKEN: "should_be_ignored" } }; + twilioConfig = { + TWILIO_AUTH_TOKEN: fakeAuthToken, + TWILIO_ACCOUNT_SID: fakeAccountSid, + TWILIO_MESSAGE_SERVICE_SID: fakeMessageServiceSid + }; + encryptedTwilioConfig = { + TWILIO_AUTH_TOKEN_ENCRYPTED: encryptedFakeAuthToken, + TWILIO_ACCOUNT_SID: fakeAccountSid, + TWILIO_MESSAGE_SERVICE_SID: fakeMessageServiceSid + }; + jest + .spyOn(crypto, "symmetricEncrypt") + .mockReturnValue(encryptedFakeAuthToken); + }); + afterEach(async () => { + jest.restoreAllMocks(); + }); + describe("getServiceConfig", () => { + let expectedConfig; beforeEach(async () => { - fakeAuthToken = "fake_twilio_auth_token"; - fakeAccountSid = "fake_twilio_account_sid"; - fakeMessageServiceSid = "fake_twilio_message_service_sid"; - encryptedFakeAuthToken = crypto.symmetricEncrypt(fakeAuthToken); - fakeApiKey = "fake_twilio_api_key"; - organization = { feature: { TWILIO_AUTH_TOKEN: "should_be_ignored" } }; - twilioConfig = { - TWILIO_AUTH_TOKEN: fakeAuthToken, - TWILIO_ACCOUNT_SID: fakeAccountSid, - TWILIO_MESSAGE_SERVICE_SID: fakeMessageServiceSid + expectedConfig = { + authToken: hiddenPlaceholder, + accountSid: fakeAccountSid, + messageServiceSid: fakeMessageServiceSid }; - encryptedTwilioConfig = { - TWILIO_AUTH_TOKEN_ENCRYPTED: encryptedFakeAuthToken, - TWILIO_ACCOUNT_SID: fakeAccountSid, - TWILIO_MESSAGE_SERVICE_SID: fakeMessageServiceSid - }; - jest - .spyOn(crypto, "symmetricEncrypt") - .mockReturnValue(encryptedFakeAuthToken); }); - describe.only("getServiceConfig", () => { - let expectedConfig; + it("returns the config elements", async () => { + const config = await twilio.getServiceConfig(twilioConfig, organization); + expect(config).toEqual(expectedConfig); + }); + describe("when obscureSensitiveInformation is true", () => { + it("returns authToken obscured", async () => { + const config = await twilio.getServiceConfig( + twilioConfig, + organization, + { obscureSensitiveInformation: true } + ); + expect(config).toEqual({ + ...expectedConfig, + authToken: hiddenPlaceholder + }); + }); + }); + describe("when the auth token is encrypted", () => { + it("returns the config elements", async () => { + const config = await twilio.getServiceConfig( + encryptedTwilioConfig, + organization + ); + expect(config).toEqual({ + ...expectedConfig, + authToken: encryptedPlaceHolder + }); + }); + describe("when obscureSensitiveInformation is true", () => { + it("returns authToken obscured", async () => { + const config = await twilio.getServiceConfig( + twilioConfig, + organization, + { obscureSensitiveInformation: true } + ); + expect(config).toEqual({ + ...expectedConfig, + authToken: hiddenPlaceholder + }); + }); + }); + }); + describe("when it has an API key instead of account sid", () => { beforeEach(async () => { - expectedConfig = { - authToken: fakeAuthToken, - accountSid: fakeAccountSid, - messageServiceSid: fakeMessageServiceSid - }; + twilioConfig.TWILIO_API_KEY = fakeApiKey; + delete twilioConfig.TWILIO_ACCOUNT_SID; }); it("returns the config elements", async () => { const config = await twilio.getServiceConfig( twilioConfig, organization ); - expect(config).toEqual(expectedConfig); + expect(config).toEqual({ + ...expectedConfig, + authToken: hiddenPlaceholder, + accountSid: fakeApiKey + }); }); - describe("when obscureSensitiveInformation is true", () => { + }); + describe("when using legacy config -- all the elements are at the top level", () => { + let fakeConfigs; + let expectedConfigOpts; + beforeEach(async () => { + organization = { feature: { ...twilioConfig } }; + expectedConfigOpts = { onlyLocal: false }; + jest.spyOn(configFunctions, "getConfig"); + jest.spyOn(configFunctions, "hasConfig"); + }); + it("returns the config ", async () => { + const config = await twilio.getServiceConfig(undefined, organization); + expect(config).toEqual({ + ...expectedConfig, + authToken: hiddenPlaceholder + }); + expect(configFunctions.hasConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts] + ]); + expect(configFunctions.getConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_MESSAGE_SERVICE_SID", organization, expectedConfigOpts] + ]); + }); + describe("where there is no config whatsoever", () => { beforeEach(async () => { - expectedConfig.authToken = ""; + organization = { feature: {} }; + configFunctions.getConfig.mockReturnValue(undefined); + }); + it("returns nothing", async () => { + const config = await twilio.getServiceConfig(undefined, organization); + expect(config).toEqual({}); + }); + }); + describe("when there is no org-specific config", () => { + beforeEach(async () => { + organization = { feature: {} }; + fakeConfigs = twilioConfig; + configFunctions.getConfig.mockImplementation(key => fakeConfigs[key]); + }); + it("returns global config", async () => { + const config = await twilio.getServiceConfig(undefined, organization); + expect(config).toEqual({ + ...expectedConfig + }); + expect(configFunctions.hasConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts] + ]); + expect(configFunctions.getConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_MESSAGE_SERVICE_SID", organization, expectedConfigOpts] + ]); }); + }); + describe("when obscureSensitiveInformation is true", () => { it("returns authToken obscured", async () => { const config = await twilio.getServiceConfig( - twilioConfig, + undefined, organization, { obscureSensitiveInformation: true } ); expect(config).toEqual(expectedConfig); + expect(configFunctions.hasConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts] + ]); + expect(configFunctions.getConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_MESSAGE_SERVICE_SID", organization, expectedConfigOpts] + ]); }); }); - describe("when the auth token is encrypted", () => { - beforeEach(async () => { - twilioConfig.TWILIO_AUTH_TOKEN_ENCRYPTED = encryptedFakeAuthToken; - delete twilioConfig.TWILIO_AUTH_TOKEN; - }); - it("returns the config elements", async () => { + describe("when obscureSensitiveInformation is false", () => { + it("returns authToken unobscured", async () => { const config = await twilio.getServiceConfig( - twilioConfig, - organization + undefined, + organization, + { obscureSensitiveInformation: false } ); expect(config).toEqual({ - authToken: fakeAuthToken, - accountSid: fakeAccountSid, - messageServiceSid: fakeMessageServiceSid - }); - }); - describe("when obscureSensitiveInformation is true", () => { - beforeEach(async () => { - expectedConfig.authToken = ""; - }); - it("returns authToken obscured", async () => { - const config = await twilio.getServiceConfig( - twilioConfig, - organization, - { obscureSensitiveInformation: true } - ); - expect(config).toEqual(expectedConfig); + ...expectedConfig, + authToken: fakeAuthToken }); + expect(configFunctions.hasConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts] + ]); + expect(configFunctions.getConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_MESSAGE_SERVICE_SID", organization, expectedConfigOpts] + ]); }); }); - describe("when it has an API key instead of account sid", () => { + + it("obscures senstive information by default", async () => { + const config = await twilio.getServiceConfig(undefined, organization); + expect(config).toEqual(expectedConfig); + expect(configFunctions.hasConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts] + ]); + expect(configFunctions.getConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_MESSAGE_SERVICE_SID", organization, expectedConfigOpts] + ]); + }); + describe("when restrictToOrgFeatures is false", () => { beforeEach(async () => { - twilioConfig.TWILIO_API_KEY = fakeApiKey; - delete twilioConfig.TWILIO_ACCOUNT_SID; + expectedConfigOpts = { onlyLocal: false }; + configFunctions.getConfig.mockRestore(); + jest.spyOn(configFunctions, "getConfig"); }); - it("returns the config elements", async () => { - const config = await twilio.getServiceConfig( - twilioConfig, - organization - ); - expect(config).toEqual({ - authToken: fakeAuthToken, - accountSid: fakeApiKey, - messageServiceSid: fakeMessageServiceSid + it("passes { onlyLocal: true } to hasConfig and getConfig", async () => { + await twilio.getServiceConfig(undefined, organization, { + restrictToOrgFeatures: false }); + expect(configFunctions.hasConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts] + ]); + expect(configFunctions.getConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_MESSAGE_SERVICE_SID", organization, expectedConfigOpts] + ]); }); }); - describe("when using legacy config -- all the elements are at the top level", () => { + describe("when restrictToOrgFeatures is true", () => { beforeEach(async () => { - organization = { feature: { ...twilioConfig } }; + expectedConfigOpts = { onlyLocal: true }; + configFunctions.getConfig.mockRestore(); + jest.spyOn(configFunctions, "getConfig"); }); - it("returns the config elements", async () => { - const config = await twilio.getServiceConfig(undefined, organization); - expect(config).toEqual({ - authToken: fakeAuthToken, - accountSid: fakeAccountSid, - messageServiceSid: fakeMessageServiceSid + it("passes { onlyLocal: true } to hasConfig and getConfig", async () => { + await twilio.getServiceConfig(undefined, organization, { + restrictToOrgFeatures: true }); + expect(configFunctions.hasConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts] + ]); + expect(configFunctions.getConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_MESSAGE_SERVICE_SID", organization, expectedConfigOpts] + ]); }); - it("returns global config if there is no org-specific config", async () => {}); - describe("when restrictToOrgFeatures is true", () => { - it("returns global configs", async () => {}); + }); + describe("when the auth token is encrypted", () => { + beforeEach(async () => { + organization = { feature: { ...encryptedTwilioConfig } }; }); describe("when obscureSensitiveInformation is true", () => { - it("returns authToken obscured", async () => {}); + it("returns authToken obscured", async () => { + const config = await twilio.getServiceConfig( + undefined, + organization, + { obscureSensitiveInformation: true } + ); + expect(config).toEqual({ + ...expectedConfig, + authToken: encryptedPlaceHolder + }); + expect(configFunctions.hasConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts] + ]); + expect(configFunctions.getConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_MESSAGE_SERVICE_SID", organization, expectedConfigOpts] + ]); + }); }); describe("when obscureSensitiveInformation is false", () => { - it("returns authToken unobscured", async () => {}); - }); - - it("obscures senstive information by default", async () => {}); - it("restricts to org features (ignoring global configs)", async () => {}); - describe("when restrictToOrgFeatures is false", () => { - it("returns global configs", async () => {}); - }); - describe("when the auth token is encrypted", () => { - beforeEach(async () => { - twilioConfig.TWILIO_AUTH_TOKEN_ENCRYPTED = encryptedFakeAuthToken; - delete twilioConfig.TWILIO_AUTH_TOKEN; - organization = { feature: { ...twilioConfig } }; - }); - describe("when obscureSensitiveInformation is true", () => { - it("returns authToken obscured", async () => {}); - }); - describe("when obscureSensitiveInformation is false", () => { - it("returns authToken unobscured", async () => {}); - }); - it("returns the config elements", async () => { + it("returns authToken unobscured", async () => { const config = await twilio.getServiceConfig( undefined, - organization + organization, + { obscureSensitiveInformation: false } ); expect(config).toEqual({ - authToken: fakeAuthToken, - accountSid: fakeAccountSid, - messageServiceSid: fakeMessageServiceSid + ...expectedConfig, + authToken: fakeAuthToken }); + expect(configFunctions.hasConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts] + ]); + expect(configFunctions.getConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_MESSAGE_SERVICE_SID", organization, expectedConfigOpts] + ]); }); }); - describe("when the auth token is not encrypted", () => { - describe("when obscureSensitiveInformation is false", () => { - it("returns authToken unobscured", async () => {}); - }); - describe("when obscureSensitiveInformation is true", () => { - it("returns authToken obscured", async () => {}); - }); + }); + describe("when the auth token is not encrypted", () => { + beforeEach(async () => { + organization = { feature: { ...twilioConfig } }; }); - describe("when it has an API key instead of account sid", () => { - beforeEach(async () => { - twilioConfig.TWILIO_API_KEY = fakeApiKey; - delete twilioConfig.TWILIO_ACCOUNT_SID; - organization = { feature: { ...twilioConfig } }; + describe("when obscureSensitiveInformation is false", () => { + it("returns authToken unobscured", async () => { + const config = await twilio.getServiceConfig( + undefined, + organization, + { obscureSensitiveInformation: false } + ); + expect(config).toEqual({ + ...expectedConfig, + authToken: fakeAuthToken + }); + expect(configFunctions.hasConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts] + ]); + expect(configFunctions.getConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_MESSAGE_SERVICE_SID", organization, expectedConfigOpts] + ]); }); - it("returns the config elements", async () => { + }); + describe("when obscureSensitiveInformation is true", () => { + it("returns authToken obscured", async () => { const config = await twilio.getServiceConfig( undefined, - organization + organization, + { obscureSensitiveInformation: true } ); expect(config).toEqual({ - authToken: fakeAuthToken, - accountSid: fakeApiKey, - messageServiceSid: fakeMessageServiceSid + ...expectedConfig, + authToken: hiddenPlaceholder }); + expect(configFunctions.hasConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts] + ]); + expect(configFunctions.getConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_MESSAGE_SERVICE_SID", organization, expectedConfigOpts] + ]); }); }); }); + describe("when it has an API key instead of account sid", () => { + beforeEach(async () => { + twilioConfig.TWILIO_API_KEY = fakeApiKey; + delete twilioConfig.TWILIO_ACCOUNT_SID; + const feature = { ...twilioConfig }; + organization = { feature }; + }); + it("returns the config elements", async () => { + const config = await twilio.getServiceConfig(undefined, organization); + expect(config).toEqual({ ...expectedConfig, accountSid: fakeApiKey }); + expect(configFunctions.hasConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts] + ]); + expect(configFunctions.getConfig.mock.calls).toEqual([ + ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], + ["TWILIO_AUTH_TOKEN", organization, expectedConfigOpts], + ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], + ["TWILIO_API_KEY", organization, expectedConfigOpts], + ["TWILIO_MESSAGE_SERVICE_SID", organization, expectedConfigOpts] + ]); + }); + }); }); - describe("getMessageServiceSid", () => { + }); + describe("getMessageServiceSid", () => { + beforeEach(async () => { + organization = { feature: { message_service_twilio: twilioConfig } }; + }); + it("returns the sid", async () => { + const sid = await twilio.getMessageServiceSid( + organization, + undefined, + undefined + ); + expect(sid).toEqual(fakeMessageServiceSid); + }); + describe("when using legacy config -- all the elements are at the top level", () => { beforeEach(async () => { - organization = { feature: { message_service_twilio: twilioConfig } }; + organization = { feature: { ...twilioConfig } }; }); it("returns the sid", async () => { const sid = await twilio.getMessageServiceSid( @@ -698,194 +941,207 @@ describe("twilio", () => { ); expect(sid).toEqual(fakeMessageServiceSid); }); - describe("when using legacy config -- all the elements are at the top level", () => { - beforeEach(async () => { - organization = { feature: { ...twilioConfig } }; - }); - it("returns the sid", async () => { - const sid = await twilio.getMessageServiceSid( - organization, - undefined, - undefined - ); - expect(sid).toEqual(fakeMessageServiceSid); - }); + }); + }); + describe("updateConfig", () => { + let twilioApiAccountsListMock; + let oldConfig; + let newConfig; + let expectedConfig; + let globalTestEnvironment; + beforeEach(async () => { + globalTestEnvironment = global.TEST_ENVIRONMENT; + global.TEST_ENVIRONMENT = 0; + + oldConfig = "__IGNORED__"; + newConfig = { + twilioAccountSid: fakeAccountSid, + twilioMessageServiceSid: fakeMessageServiceSid, + twilioAuthToken: fakeAuthToken + }; + + expectedConfig = { + TWILIO_ACCOUNT_SID: fakeAccountSid, + TWILIO_AUTH_TOKEN_ENCRYPTED: encryptedFakeAuthToken, + TWILIO_MESSAGE_SERVICE_SID: fakeMessageServiceSid + }; + + twilioApiAccountsListMock = jest.fn().mockResolvedValue({}); + jest.spyOn(twilioLibrary.default, "Twilio").mockReturnValue({ + api: { accounts: { list: twilioApiAccountsListMock } } }); }); - describe("updateConfig", () => { - let twilioApiAccountsListMock; - let oldConfig; - let newConfig; - let expectedConfig; - let globalTestEnvironment; + + afterEach(async () => { + global.TEST_ENVIRONMENT = globalTestEnvironment; + }); + + it("delegates to its dependencies and returns the new config", async () => { + const returnedConfig = await twilio.updateConfig(oldConfig, newConfig); + expect(returnedConfig).toEqual(expectedConfig); + expect(crypto.symmetricEncrypt.mock.calls).toEqual([ + ["fake_twilio_auth_token"] + ]); + expect(twilioLibrary.default.Twilio.mock.calls).toEqual([ + [fakeAccountSid, fakeAuthToken] + ]); + expect(twilioApiAccountsListMock.mock.calls).toEqual([[]]); + }); + describe("when the new config doesn't contain required elements", () => { + beforeEach(async () => { + delete newConfig.twilioAccountSid; + }); + it("throws an exception", async () => { + let error; + try { + await twilio.updateConfig(oldConfig, newConfig); + } catch (caught) { + error = caught; + } + expect(error.message).toEqual( + "twilioAccountSid and twilioMessageServiceSid are required" + ); + expect(crypto.symmetricEncrypt).not.toHaveBeenCalled(); + expect(twilioLibrary.default.Twilio).not.toHaveBeenCalled(); + expect(twilioApiAccountsListMock).not.toHaveBeenCalled(); + }); + }); + describe("when the twilio credentials are invalid", () => { beforeEach(async () => { - globalTestEnvironment = global.TEST_ENVIRONMENT; - global.TEST_ENVIRONMENT = 0; - - oldConfig = "__IGNORED__"; - newConfig = { - twilioAccountSid: fakeAccountSid, - twilioMessageServiceSid: fakeMessageServiceSid, - twilioAuthToken: fakeAuthToken - }; - - expectedConfig = { - TWILIO_ACCOUNT_SID: fakeAccountSid, - TWILIO_AUTH_TOKEN_ENCRYPTED: encryptedFakeAuthToken, - TWILIO_MESSAGE_SERVICE_SID: fakeMessageServiceSid - }; - - twilioApiAccountsListMock = jest.fn().mockResolvedValue({}); + twilioApiAccountsListMock = jest.fn().mockImplementation(() => { + throw new Error("OH NO!"); + }); jest.spyOn(twilioLibrary.default, "Twilio").mockReturnValue({ api: { accounts: { list: twilioApiAccountsListMock } } }); }); - - afterEach(async () => { - global.TEST_ENVIRONMENT = globalTestEnvironment; + it("throws an exception", async () => { + let error; + try { + await twilio.updateConfig(oldConfig, newConfig); + } catch (caught) { + error = caught; + } + expect(error.message).toEqual("Invalid Twilio credentials"); }); + }); + }); + describe("campaignNumbersEnabled", () => { + beforeEach(async () => { + organization = { + feature: { + EXPERIMENTAL_PHONE_INVENTORY: true, + PHONE_INVENTORY: true, + EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS: true + } + }; + }); - it("delegates to its dependencies and returns the new config", async () => { - const returnedConfig = await twilio.updateConfig(oldConfig, newConfig); - expect(returnedConfig).toEqual(expectedConfig); - expect(crypto.symmetricEncrypt.mock.calls).toEqual([ - ["fake_twilio_auth_token"] - ]); - expect(twilioLibrary.default.Twilio.mock.calls).toEqual([ - [fakeAccountSid, fakeAuthToken] - ]); - expect(twilioApiAccountsListMock.mock.calls).toEqual([[]]); - }); - describe("when the new config doesn't contain required elements", () => { - beforeEach(async () => { - delete newConfig.twilioAccountSid; - }); - it("throws an exception", async () => { - let error; - try { - await twilio.updateConfig(oldConfig, newConfig); - } catch (caught) { - error = caught; - } - expect(error.message).toEqual( - "twilioAccountSid and twilioMessageServiceSid are required" - ); - expect(crypto.symmetricEncrypt).not.toHaveBeenCalled(); - expect(twilioLibrary.default.Twilio).not.toHaveBeenCalled(); - expect(twilioApiAccountsListMock).not.toHaveBeenCalled(); - }); + it("returns true when all the configs are true", async () => { + expect(twilio.campaignNumbersEnabled(organization)).toEqual(true); + }); + describe("when EXPERIMENTAL_PHONE_INVENTORY is false", () => { + beforeEach(async () => { + organization.feature.EXPERIMENTAL_PHONE_INVENTORY = false; }); - describe("when the twilio credentials are invalid", () => { - beforeEach(async () => { - twilioApiAccountsListMock = jest.fn().mockImplementation(() => { - throw new Error("OH NO!"); - }); - jest.spyOn(twilioLibrary.default, "Twilio").mockReturnValue({ - api: { accounts: { list: twilioApiAccountsListMock } } - }); - }); - it("throws an exception", async () => { - let error; - try { - await twilio.updateConfig(oldConfig, newConfig); - } catch (caught) { - error = caught; - } - expect(error.message).toEqual("Invalid Twilio credentials"); - }); + + it("returns true", async () => { + expect(twilio.campaignNumbersEnabled(organization)).toEqual(true); }); }); - describe("campaignNumbersEnabled", () => { + describe("when PHONE_INVENTORY is false", () => { beforeEach(async () => { - organization = { - feature: { - EXPERIMENTAL_PHONE_INVENTORY: true, - PHONE_INVENTORY: true, - EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS: true - } - }; + organization.feature.PHONE_INVENTORY = false; }); - it("returns true when all the configs are true", async () => { + it("returns true", async () => { expect(twilio.campaignNumbersEnabled(organization)).toEqual(true); }); - describe("when EXPERIMENTAL_PHONE_INVENTORY is false", () => { - beforeEach(async () => { - organization.feature.EXPERIMENTAL_PHONE_INVENTORY = false; - }); - - it("returns true", async () => { - expect(twilio.campaignNumbersEnabled(organization)).toEqual(true); - }); + }); + describe("when EXPERIMENTAL_PHONE_INVENTORY and PHONE_INVENTORY are both false", () => { + beforeEach(async () => { + organization.feature.PHONE_INVENTORY = false; + organization.feature.EXPERIMENTAL_PHONE_INVENTORY = false; }); - describe("when PHONE_INVENTORY is false", () => { - beforeEach(async () => { - organization.feature.PHONE_INVENTORY = false; - }); - it("returns true", async () => { - expect(twilio.campaignNumbersEnabled(organization)).toEqual(true); - }); + it("returns false", async () => { + expect(twilio.campaignNumbersEnabled(organization)).toEqual(false); }); - describe("when EXPERIMENTAL_PHONE_INVENTORY and PHONE_INVENTORY are both false", () => { - beforeEach(async () => { - organization.feature.PHONE_INVENTORY = false; - organization.feature.EXPERIMENTAL_PHONE_INVENTORY = false; - }); - - it("returns false", async () => { - expect(twilio.campaignNumbersEnabled(organization)).toEqual(false); - }); + }); + describe("when EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS is false", () => { + beforeEach(async () => { + organization.feature.EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS = false; }); - describe("when EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS is false", () => { - beforeEach(async () => { - organization.feature.EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS = false; - }); - it("returns false", async () => { - expect(twilio.campaignNumbersEnabled(organization)).toEqual(false); - }); + it("returns false", async () => { + expect(twilio.campaignNumbersEnabled(organization)).toEqual(false); }); }); - describe("manualMessagingServicesEnabled", () => { + }); + describe("manualMessagingServicesEnabled", () => { + beforeEach(async () => { + organization = { + feature: { + EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE: true + } + }; + }); + + it("it returns true with the config is true", async () => { + expect(twilio.manualMessagingServicesEnabled(organization)).toEqual(true); + }); + + describe("when the config is false", () => { beforeEach(async () => { - organization = { - feature: { - EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE: true - } - }; + organization.feature.EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE = false; }); - - it("it returns true with the config is true", async () => { + it("returns flse", async () => { expect(twilio.manualMessagingServicesEnabled(organization)).toEqual( - true + false ); }); - - describe("when the config is false", () => { - beforeEach(async () => { - organization.feature.EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE = false; - }); - it("returns flse", async () => { - expect(twilio.manualMessagingServicesEnabled(organization)).toEqual( - false - ); - }); + }); + }); + describe("fullyConfigured", () => { + beforeEach(async () => { + jest.spyOn(twilio, "getServiceConfig").mockResolvedValue({ + authToken: "fake_auth_token", + accountSid: "fake_account_sid" }); + jest + .spyOn(twilio, "manualMessagingServicesEnabled") + .mockReturnValue(true); + jest.spyOn(twilio, "campaignNumbersEnabled").mockReturnValue(true); + jest + .spyOn(twilio, "getMessageServiceSid") + .mockResolvedValue("fake_message_service_sid"); + }); + it("returns true", async () => { + expect(await twilio.fullyConfigured("everything_is_mocked")).toEqual( + true + ); + expect(twilio.getMessageServiceSid).not.toHaveBeenCalled(); }); - describe("fullyConfigured", () => { + describe("when getServiceConfig doesn't return a full configuration", () => { beforeEach(async () => { jest.spyOn(twilio, "getServiceConfig").mockResolvedValue({ - authToken: "fake_auth_token", - accountSid: "fake_account_sid" + authToken: "fake_auth_token" }); + }); + it("returns false", async () => { + expect(await twilio.fullyConfigured("everything_is_mocked")).toEqual( + false + ); + expect(twilio.manualMessagingServicesEnabled).not.toHaveBeenCalled(); + expect(twilio.campaignNumbersEnabled).not.toHaveBeenCalled(); + expect(twilio.getMessageServiceSid).not.toHaveBeenCalled(); + }); + }); + describe("when manualmessagingServicesEnabled returns false", () => { + beforeEach(async () => { jest .spyOn(twilio, "manualMessagingServicesEnabled") - .mockReturnValue(true); - jest.spyOn(twilio, "campaignNumbersEnabled").mockReturnValue(true); - jest - .spyOn(twilio, "getMessageServiceSid") - .mockResolvedValue("fake_message_service_sid"); + .mockReturnValue(false); }); it("returns true", async () => { expect(await twilio.fullyConfigured("everything_is_mocked")).toEqual( @@ -893,80 +1149,51 @@ describe("twilio", () => { ); expect(twilio.getMessageServiceSid).not.toHaveBeenCalled(); }); - describe("when getServiceConfig doesn't return a full configuration", () => { - beforeEach(async () => { - jest.spyOn(twilio, "getServiceConfig").mockResolvedValue({ - authToken: "fake_auth_token" - }); - }); - it("returns false", async () => { - expect(await twilio.fullyConfigured("everything_is_mocked")).toEqual( - false - ); - expect(twilio.manualMessagingServicesEnabled).not.toHaveBeenCalled(); - expect(twilio.campaignNumbersEnabled).not.toHaveBeenCalled(); - expect(twilio.getMessageServiceSid).not.toHaveBeenCalled(); - }); + }); + describe("when campaignNumbersEnabled returns false", () => { + beforeEach(async () => { + jest.spyOn(twilio, "campaignNumbersEnabled").mockReturnValue(false); }); - describe("when manualmessagingServicesEnabled returns false", () => { - beforeEach(async () => { - jest - .spyOn(twilio, "manualMessagingServicesEnabled") - .mockReturnValue(false); - }); + it("returns true", async () => { + expect(await twilio.fullyConfigured("everything_is_mocked")).toEqual( + true + ); + expect(twilio.getMessageServiceSid).not.toHaveBeenCalled(); + }); + }); + describe("when manualMessagingServiceEnabled and campaignNumbersEnabled both return false", () => { + beforeEach(async () => { + jest + .spyOn(twilio, "manualMessagingServicesEnabled") + .mockReturnValue(false); + jest.spyOn(twilio, "campaignNumbersEnabled").mockReturnValue(false); + }); + describe("when getMessageServiceSid returns true", () => { it("returns true", async () => { expect(await twilio.fullyConfigured("everything_is_mocked")).toEqual( true ); - expect(twilio.getMessageServiceSid).not.toHaveBeenCalled(); + expect(twilio.getMessageServiceSid.mock.calls).toEqual([ + ["everything_is_mocked"] + ]); }); }); - describe("when campaignNumbersEnabled returns false", () => { + describe("when getMessageServiceSid returns null", () => { beforeEach(async () => { - jest.spyOn(twilio, "campaignNumbersEnabled").mockReturnValue(false); + jest.spyOn(twilio, "getMessageServiceSid").mockResolvedValue(null); }); - it("returns true", async () => { + it("returns false", async () => { expect(await twilio.fullyConfigured("everything_is_mocked")).toEqual( - true + false ); - expect(twilio.getMessageServiceSid).not.toHaveBeenCalled(); - }); - }); - describe("when manualMessagingServiceEnabled and campaignNumbersEnabled both return false", () => { - beforeEach(async () => { - jest - .spyOn(twilio, "manualMessagingServicesEnabled") - .mockReturnValue(false); - jest.spyOn(twilio, "campaignNumbersEnabled").mockReturnValue(false); - }); - describe("when getMessageServiceSid returns true", () => { - it("returns true", async () => { - expect( - await twilio.fullyConfigured("everything_is_mocked") - ).toEqual(true); - expect(twilio.getMessageServiceSid.mock.calls).toEqual([ - ["everything_is_mocked"] - ]); - }); - }); - describe("when getMessageServiceSid returns null", () => { - beforeEach(async () => { - jest.spyOn(twilio, "getMessageServiceSid").mockResolvedValue(null); - }); - it("returns false", async () => { - expect( - await twilio.fullyConfigured("everything_is_mocked") - ).toEqual(false); - expect(twilio.getMessageServiceSid.mock.calls).toEqual([ - ["everything_is_mocked"] - ]); - }); + expect(twilio.getMessageServiceSid.mock.calls).toEqual([ + ["everything_is_mocked"] + ]); }); }); }); }); }); - // FUTURE // * parseMessageText // * convertMessagePartsToMessage diff --git a/src/extensions/messaging_services/twilio/index.js b/src/extensions/messaging_services/twilio/index.js index 926781236..1745aa888 100644 --- a/src/extensions/messaging_services/twilio/index.js +++ b/src/extensions/messaging_services/twilio/index.js @@ -837,7 +837,7 @@ export const getServiceConfig = async ( ) => { const { restrictToOrgFeatures = false, - obscureSensitiveInformation = false + obscureSensitiveInformation = true } = options; let authToken; let accountSid; @@ -864,7 +864,7 @@ export const getServiceConfig = async ( } else { // for backward compatibility - const getConfigOptions = { onlyLocal: restrictToOrgFeatures }; + const getConfigOptions = { onlyLocal: Boolean(restrictToOrgFeatures) }; const hasEncryptedToken = hasConfig( "TWILIO_AUTH_TOKEN_ENCRYPTED", @@ -884,11 +884,18 @@ export const getServiceConfig = async ( ) ); } else { - authToken = obscureSensitiveInformation - ? "" - : getConfig("TWILIO_AUTH_TOKEN", organization, getConfigOptions); + const hasUnencryptedToken = hasConfig( + "TWILIO_AUTH_TOKEN", + organization, + getConfigOptions + ); + if (hasUnencryptedToken) { + authToken = obscureSensitiveInformation + ? "" + : getConfig("TWILIO_AUTH_TOKEN", organization, getConfigOptions); + } } - accountSid = hasConfig("TWILIO_ACCOUNT_SID", organization) + accountSid = hasConfig("TWILIO_ACCOUNT_SID", organization, getConfigOptions) ? getConfig("TWILIO_ACCOUNT_SID", organization, getConfigOptions) : // Check old TWILIO_API_KEY variable for backwards compatibility. getConfig("TWILIO_API_KEY", organization, getConfigOptions); diff --git a/src/server/api/lib/config.js b/src/server/api/lib/config.js index ff1d93b0c..e99025d96 100644 --- a/src/server/api/lib/config.js +++ b/src/server/api/lib/config.js @@ -61,7 +61,7 @@ export function getConfig(key, organization, opts) { } export function hasConfig(key, organization, options = {}) { - const val = getConfig(key, organization, options); + const val = exports.getConfig(key, organization, options); // we need to allow "" as no config since env vars will occasionally be set to that to undefine it return Boolean(typeof val !== "undefined" && val !== ""); } From 103df3649163d19994e1460449ea295e60397031 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sun, 11 Apr 2021 16:38:33 -0400 Subject: [PATCH 058/191] change let to const --- src/server/api/lib/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api/lib/config.js b/src/server/api/lib/config.js index e99025d96..b9f80b988 100644 --- a/src/server/api/lib/config.js +++ b/src/server/api/lib/config.js @@ -28,7 +28,7 @@ export const getOrDefault = (value, defaultValue) => export function getConfig(key, organization, opts) { if (organization) { // TODO: update to not parse if features is an object (vs. a string) - let features = getFeatures(organization); + const features = getFeatures(organization); if (features.hasOwnProperty(key)) { return getOrDefault(features[key], opts && opts.default); } From 239ddf9c7c42129f0c350fd658a4b4313534020e Mon Sep 17 00:00:00 2001 From: Larry Person Date: Thu, 15 Apr 2021 10:48:01 -0400 Subject: [PATCH 059/191] progress is being made --- .../updateMessageServiceConfig.test.js | 43 +++++++++++-------- .../mutations/updateMessageServiceConfig.js | 24 +++++++++-- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/__test__/server/api/mutations/updateMessageServiceConfig.test.js b/__test__/server/api/mutations/updateMessageServiceConfig.test.js index d54056c05..91a7c0c1f 100644 --- a/__test__/server/api/mutations/updateMessageServiceConfig.test.js +++ b/__test__/server/api/mutations/updateMessageServiceConfig.test.js @@ -51,27 +51,32 @@ describe("updateMessageServiceConfig", () => { }; }); - it("calls message service's updateConfig and other functions", async () => { - const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); - expect(twilio.updateConfig.mock.calls).toEqual([[undefined, newConfig]]); - expect(orgCache.getMessageServiceConfig.mock.calls).toEqual([ - [ - expect.objectContaining({ - id: 1, - feature: expect.objectContaining({ - message_service_twilio: newConfig + describe("when there is no message service-specific section in features", () => { + it("calls message service's updateConfig and other functions", async () => { + const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); + expect(twilio.updateConfig.mock.calls).toEqual([[undefined, newConfig]]); + expect(orgCache.getMessageServiceConfig.mock.calls).toEqual([ + [ + expect.objectContaining({ + id: 1, + feature: expect.objectContaining({ + ...newConfig + // message_service_twilio: newConfig + }) }) - }) - ] - ]); - expect(gqlResult.data.updateMessageServiceConfig).toEqual(newConfig); + ] + ]); + expect(gqlResult.data.updateMessageServiceConfig).toEqual(newConfig); - // TODO - // expect cache.clear to have been called - // expect cache.load to have been called + // TODO + // expect cache.clear to have been called + // expect cache.load to have been called + }); }); - it("updates the config in organization.features", async () => {}); + it("updates the config in organization.features", async () => { + // TODO + }); describe("when it's not the configured message service name", () => { beforeEach(async () => { @@ -176,7 +181,9 @@ describe("updateMessageServiceConfig", () => { }); describe("when the organization had no features", () => { - it("does not throw an exception", async () => {}); + it("does not throw an exception", async () => { + // TODO + }); }); describe("when updating legacy config (all config elements at the top level of organization.features)", () => { diff --git a/src/server/api/mutations/updateMessageServiceConfig.js b/src/server/api/mutations/updateMessageServiceConfig.js index e354eae47..ccf761c35 100644 --- a/src/server/api/mutations/updateMessageServiceConfig.js +++ b/src/server/api/mutations/updateMessageServiceConfig.js @@ -9,8 +9,7 @@ import orgCache from "../../models/cacheable_queries/organization"; import { accessRequired } from "../errors"; import { Organization } from "../../../server/models"; -// TODO(lperson) this should allow the message service -// to modify only its own object +// TODO(lperson) this should allow the message service to modify only its own object export const updateMessageServiceConfig = async ( _, { organizationId, messageServiceName, config }, @@ -70,7 +69,7 @@ export const updateMessageServiceConfig = async ( dbOrganization.features = JSON.stringify({ ...features, ...(features[configKey] && { [configKey]: newConfig }), - ...(!features[configKey] && { [configKey]: newConfig }) + ...(!features[configKey] && newConfig) }); await dbOrganization.save(); @@ -79,3 +78,22 @@ export const updateMessageServiceConfig = async ( return orgCache.getMessageServiceConfig(updatedOrganization); }; + +export const getMessageServiceConfig = async ( + serviceName, + organization, + options = {} +) => { + const getServiceConfig = exports.tryGetFunctionFromService( + serviceName, + "getServiceConfig" + ); + if (!getServiceConfig) { + return null; + } + const configKey = exports.getConfigKey(serviceName); + const config = getConfig(configKey, organization, { + onlyLocal: options.restrictToOrgFeatures + }); + return getServiceConfig(config, organization, options); +}; From 9c8384c8bbbe23b327a992da417d3703f6a67bb1 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sat, 17 Apr 2021 13:20:55 -0400 Subject: [PATCH 060/191] _finally_ complete tests for updateMessageService --- .../updateMessageServiceConfig.test.js | 235 ++++++++++++------ .../mutations/updateMessageServiceConfig.js | 16 +- 2 files changed, 177 insertions(+), 74 deletions(-) diff --git a/__test__/server/api/mutations/updateMessageServiceConfig.test.js b/__test__/server/api/mutations/updateMessageServiceConfig.test.js index 91a7c0c1f..de2a6cff8 100644 --- a/__test__/server/api/mutations/updateMessageServiceConfig.test.js +++ b/__test__/server/api/mutations/updateMessageServiceConfig.test.js @@ -28,6 +28,8 @@ describe("updateMessageServiceConfig", () => { let organization; let vars; let newConfig; + let expectedConfig; + let expectedCacheConfig; beforeEach(async () => { user = await createUser(); const invite = await createInvite(); @@ -37,12 +39,25 @@ describe("updateMessageServiceConfig", () => { createOrganizationResult ); - newConfig = { fake_config: "fake_config_value" }; - jest.spyOn(twilio, "updateConfig").mockResolvedValue(newConfig); + newConfig = { + twilioAccountSid: "fake_account_sid", + twilioMessageServiceSid: "fake_message_service_sid" + }; + + expectedConfig = { + TWILIO_ACCOUNT_SID: "fake_account_sid", + TWILIO_MESSAGE_SERVICE_SID: "fake_message_service_sid" + }; - jest - .spyOn(orgCache, "getMessageServiceConfig") - .mockResolvedValue(newConfig); + expectedCacheConfig = { + accountSid: "fake_account_sid", + messageServiceSid: "fake_message_service_sid" + }; + + jest.spyOn(twilio, "updateConfig"); + jest.spyOn(orgCache, "getMessageServiceConfig"); + jest.spyOn(orgCache, "clear"); + jest.spyOn(orgCache, "load"); vars = { organizationId: organization.id, @@ -51,33 +66,6 @@ describe("updateMessageServiceConfig", () => { }; }); - describe("when there is no message service-specific section in features", () => { - it("calls message service's updateConfig and other functions", async () => { - const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); - expect(twilio.updateConfig.mock.calls).toEqual([[undefined, newConfig]]); - expect(orgCache.getMessageServiceConfig.mock.calls).toEqual([ - [ - expect.objectContaining({ - id: 1, - feature: expect.objectContaining({ - ...newConfig - // message_service_twilio: newConfig - }) - }) - ] - ]); - expect(gqlResult.data.updateMessageServiceConfig).toEqual(newConfig); - - // TODO - // expect cache.clear to have been called - // expect cache.load to have been called - }); - }); - - it("updates the config in organization.features", async () => { - // TODO - }); - describe("when it's not the configured message service name", () => { beforeEach(async () => { vars.messageServiceName = "this will never be a message service name"; @@ -92,6 +80,22 @@ describe("updateMessageServiceConfig", () => { }); }); + describe("when the organization has no features", () => { + beforeEach(async () => { + const dbOrganization = await Organization.get(organization.id); + dbOrganization.features = null; + await dbOrganization.save(); + if (r.redis) r.redis.flushdb(); + }); + it("returns an error", async () => { + const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); + expect(gqlResult.errors[0].message).toEqual( + "Can't configure twilio. It's not the configured message service" + ); + expect(twilio.updateConfig).not.toHaveBeenCalled(); + }); + }); + describe("when it's not a valid message service", () => { beforeEach(async () => { jest.spyOn(serviceMap, "getService").mockReturnValue(null); @@ -120,7 +124,7 @@ describe("updateMessageServiceConfig", () => { }); }); - describe("when the pass config is not valid JSON", () => { + describe("when the passed config is not valid JSON", () => { beforeEach(async () => { vars.config = "not JSON"; }); @@ -145,50 +149,141 @@ describe("updateMessageServiceConfig", () => { }); }); - describe("when there is an existing config", () => { - let fakeExistingConfig; + describe("when the organization gets updated", () => { + let configKey; + let dbOrganization; + let service; + let sharedExpectations; + let expectedFeatures; beforeEach(async () => { - const configKey = serviceMap.getConfigKey("twilio"); - fakeExistingConfig = { - fake_existing_config_key: "fake_existing_config_value" + service = "twilio"; + configKey = serviceMap.getConfigKey("twilio"); + dbOrganization = await Organization.get(organization.id); + dbOrganization.features = JSON.stringify({ service: "twilio" }); + await dbOrganization.save(); + if (r.redis) r.redis.flushdb(); + + expectedFeatures = { + service, + [configKey]: expectedConfig }; - const dbOrganization = await Organization.get(organization.id); - const newFeatures = JSON.stringify({ - ...JSON.parse(dbOrganization.features), - [configKey]: fakeExistingConfig + + sharedExpectations = async (gqlResult, features) => { + expect(orgCache.getMessageServiceConfig.mock.calls).toEqual([ + [ + expect.objectContaining({ + id: 1 + }) + ] + ]); + expect(gqlResult.data.updateMessageServiceConfig).toEqual( + expect.objectContaining(expectedCacheConfig) + ); + + dbOrganization = await Organization.get(organization.id); + expect(JSON.parse(dbOrganization.features)).toEqual(features); + + expect(orgCache.clear.mock.calls).toEqual([[dbOrganization.id]]); + expect(orgCache.load.mock.calls).toEqual([ + [organization.id], + [dbOrganization.id] + ]); + }; + }); + describe("when features DOES NOT HAVE an existing config for the message service", () => { + it("writes message service config in features.configKey", async () => { + const gqlResult = await runGql( + updateMessageServiceConfigGql, + vars, + user + ); + expect(twilio.updateConfig.mock.calls).toEqual([ + [undefined, newConfig] + ]); + + sharedExpectations(gqlResult, expectedFeatures); }); - dbOrganization.features = newFeatures; - await dbOrganization.save(); - await orgCache.clear(organization.id); }); - it("passes it to updateConfig", async () => { - const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); - expect(twilio.updateConfig.mock.calls).toEqual([ - [fakeExistingConfig, newConfig] - ]); - expect(orgCache.getMessageServiceConfig.mock.calls).toEqual([ - [ - expect.objectContaining({ - id: 1, - feature: expect.objectContaining({ - message_service_twilio: newConfig - }) - }) - ] - ]); - expect(gqlResult.data.updateMessageServiceConfig).toEqual(newConfig); + + describe("when features DOES HAVE an existing config for the message service", () => { + beforeEach(async () => { + dbOrganization.features = JSON.stringify({ + service, + [configKey]: "it doesn't matter" + }); + await dbOrganization.save(); + if (r.redis) r.redis.flushdb(); + }); + it("writes message service config in features.configKey", async () => { + const gqlResult = await runGql( + updateMessageServiceConfigGql, + vars, + user + ); + expect(twilio.updateConfig.mock.calls).toEqual([ + ["it doesn't matter", newConfig] + ]); + + sharedExpectations(gqlResult, expectedFeatures); + }); }); - }); - describe("when the organization had no features", () => { - it("does not throw an exception", async () => { - // TODO + describe("when updating legacy config (all config elements at the top level of organization.features)", () => { + // for example, for twilio, this is when all the config elements are not children of + // the `message_service_twilio` key in organization.features + beforeEach(async () => { + dbOrganization.features = JSON.stringify({ + service, + TWILIO_ACCOUNT_SID: "the former_fake_account_sid", + TWILIO_MESSAGE_SERVICE_SID: "the_former_fake_message_service_sid" + }); + await dbOrganization.save(); + if (r.redis) r.redis.flushdb(); + }); + it("writes individual config components to the top level of features", async () => { + const gqlResult = await runGql( + updateMessageServiceConfigGql, + vars, + user + ); + expect(twilio.updateConfig.mock.calls).toEqual([ + [undefined, newConfig] + ]); + + sharedExpectations(gqlResult, { service, ...expectedConfig }); + }); }); - }); - describe("when updating legacy config (all config elements at the top level of organization.features)", () => { - // for example, for twilio, this is when all the config elements are not children of - // the `message_service_twilio` key in organization.features - it("updates the config at the top level", async () => {}); + describe("when the message service is not twilio and the config was at the top level", () => { + let extremelyFakeService; + beforeEach(async () => { + service = "extremely_fake_service"; + configKey = serviceMap.getConfigKey(service); + vars.messageServiceName = service; + dbOrganization.features = JSON.stringify({ + service, + TWILIO_ACCOUNT_SID: "the former_fake_account_sid", + TWILIO_MESSAGE_SERVICE_SID: "the_former_fake_message_service_sid" + }); + await dbOrganization.save(); + if (r.redis) r.redis.flushdb(); + + extremelyFakeService = { + updateConfig: jest.fn().mockImplementation(() => { + return expectedConfig; + }) + }; + jest + .spyOn(serviceMap, "getService") + .mockReturnValue(extremelyFakeService); + }); + it("writes the message service config to features.config_key", async () => { + await runGql(updateMessageServiceConfigGql, vars, user); + dbOrganization = await Organization.get(organization.id); + expect(JSON.parse(dbOrganization.features)).toEqual( + expect.objectContaining({ [configKey]: expectedConfig }) + ); + }); + }); }); }); diff --git a/src/server/api/mutations/updateMessageServiceConfig.js b/src/server/api/mutations/updateMessageServiceConfig.js index ccf761c35..0946cd7f1 100644 --- a/src/server/api/mutations/updateMessageServiceConfig.js +++ b/src/server/api/mutations/updateMessageServiceConfig.js @@ -9,7 +9,6 @@ import orgCache from "../../models/cacheable_queries/organization"; import { accessRequired } from "../errors"; import { Organization } from "../../../server/models"; -// TODO(lperson) this should allow the message service to modify only its own object export const updateMessageServiceConfig = async ( _, { organizationId, messageServiceName, config }, @@ -49,7 +48,9 @@ export const updateMessageServiceConfig = async ( } const configKey = getConfigKey(messageServiceName); - const existingConfig = getConfig(configKey, organization); + const existingConfig = getConfig(configKey, organization, { + onlyLocal: true + }); let newConfig; try { @@ -66,10 +67,17 @@ export const updateMessageServiceConfig = async ( const dbOrganization = await Organization.get(organizationId); const features = JSON.parse(dbOrganization.features || "{}"); + const hadMessageServiceConfig = !!features[configKey]; + const newConfigKeys = new Set(Object.keys(newConfig)); + const legacyTwilioConfig = + messageServiceName === "twilio" && + !hadMessageServiceConfig && + Object.keys(features).some(k => newConfigKeys.has(k)); + dbOrganization.features = JSON.stringify({ ...features, - ...(features[configKey] && { [configKey]: newConfig }), - ...(!features[configKey] && newConfig) + ...(!legacyTwilioConfig && { [configKey]: newConfig }), + ...(legacyTwilioConfig && newConfig) }); await dbOrganization.save(); From fbb9a09d092717307f580c4d2aa95671bac72d81 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sat, 17 Apr 2021 15:17:14 -0400 Subject: [PATCH 061/191] delete twilio cruft --- src/containers/Settings.jsx | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/src/containers/Settings.jsx b/src/containers/Settings.jsx index 80edb4057..c1dafd74c 100644 --- a/src/containers/Settings.jsx +++ b/src/containers/Settings.jsx @@ -9,7 +9,6 @@ import Dialog from "material-ui/Dialog"; import GSSubmitButton from "../components/forms/GSSubmitButton"; import FlatButton from "material-ui/FlatButton"; import RaisedButton from "material-ui/RaisedButton"; -import DisplayLink from "../components/DisplayLink"; import * as yup from "yup"; import { Card, CardText, CardActions, CardHeader } from "material-ui/Card"; import { StyleSheet, css } from "aphrodite"; @@ -378,9 +377,6 @@ const queries = { options sideboxChoices } - twilioAccountSid - twilioAuthToken - twilioMessageServiceSid messageService { name type @@ -435,27 +431,6 @@ export const updateMessageServiceConfigGql = gql` } `; -export const updateTwilioAuthGql = gql` - mutation updateTwilioAuth( - $twilioAccountSid: String - $twilioAuthToken: String - $twilioMessageServiceSid: String - $organizationId: String! - ) { - updateTwilioAuth( - twilioAccountSid: $twilioAccountSid - twilioAuthToken: $twilioAuthToken - twilioMessageServiceSid: $twilioMessageServiceSid - organizationId: $organizationId - ) { - id - twilioAccountSid - twilioAuthToken - twilioMessageServiceSid - } - } -`; - const mutations = { editOrganization: ownProps => organizationChanges => ({ mutation: editOrganizationGql, @@ -541,15 +516,6 @@ const mutations = { } }; }, - updateTwilioAuth: ownProps => (accountSid, authToken, messageServiceSid) => ({ - mutation: updateTwilioAuthGql, - variables: { - organizationId: ownProps.params.organizationId, - twilioAccountSid: accountSid, - twilioAuthToken: authToken, - twilioMessageServiceSid: messageServiceSid - } - }), clearCachedOrgAndExtensionCaches: ownProps => () => ({ mutation: gql` mutation clearCachedOrgAndExtensionCaches($organizationId: String!) { From 71258d463d8fb5534dd0b63592b10e8d2da89770 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sat, 17 Apr 2021 15:40:47 -0400 Subject: [PATCH 062/191] make twilio config dialog work with udpated react --- .../twilio/react-components/org-config.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/extensions/messaging_services/twilio/react-components/org-config.js b/src/extensions/messaging_services/twilio/react-components/org-config.js index d809e3dc8..39b5af916 100644 --- a/src/extensions/messaging_services/twilio/react-components/org-config.js +++ b/src/extensions/messaging_services/twilio/react-components/org-config.js @@ -7,9 +7,10 @@ import { Table, TableBody, TableRow, TableRowColumn } from "material-ui/Table"; import PropTypes from "prop-types"; import React from "react"; import Form from "react-formal"; -import yup from "yup"; +import * as yup from "yup"; import DisplayLink from "../../../../components/DisplayLink"; import GSForm from "../../../../components/forms/GSForm"; +import GSTextField from "../../../../components/forms/GSTextField"; import GSSubmitButton from "../../../../components/forms/GSSubmitButton"; export class OrgConfig extends React.Component { @@ -45,8 +46,8 @@ export class OrgConfig extends React.Component { handleCloseTwilioDialog = () => this.setState({ twilioDialogOpen: false }); - handleSubmitTwilioAuthForm = async p => { - const { accountSid, authToken, messageServiceSid } = p; + handleSubmitTwilioAuthForm = async () => { + const { accountSid, authToken, messageServiceSid } = this.state; let twilioError; try { await this.props.onSubmit({ @@ -101,11 +102,12 @@ export class OrgConfig extends React.Component { style={inlineStyles.dialogButton} onClick={this.handleCloseTwilioDialog} />, - ]; @@ -163,26 +165,29 @@ export class OrgConfig extends React.Component { - From 177027d809c2ed63b624c1a898b9e85e0d64260d Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sat, 17 Apr 2021 15:42:36 -0400 Subject: [PATCH 063/191] remove only, fix test --- __test__/extensions/messaging_services/twilio.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__test__/extensions/messaging_services/twilio.test.js b/__test__/extensions/messaging_services/twilio.test.js index 4b2c31738..83419fd2b 100644 --- a/__test__/extensions/messaging_services/twilio.test.js +++ b/__test__/extensions/messaging_services/twilio.test.js @@ -464,7 +464,7 @@ describe("twilio", () => { const org2Auth = await cacheableData.organization.getMessageServiceConfig( org2 ); - expect(org2Auth.authToken).toBe("test_twilio_auth_token"); + expect(org2Auth.authToken).toBe(""); expect(org2Auth.accountSid).toBe("test_twilio_account_sid"); }); @@ -502,7 +502,7 @@ describe("twilio", () => { }); }); -describe.only("config functions", () => { +describe("config functions", () => { let twilioConfig; let encryptedTwilioConfig; let organization; From b373a3152450dc006b791d41eefd89ccc66a22eb Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sun, 18 Apr 2021 11:34:32 -0400 Subject: [PATCH 064/191] Remove twilio-specific resolvers and mutations --- __test__/test_helpers.js | 60 +++++++++++++++------------------- src/api/organization.js | 3 -- src/api/schema.js | 6 ---- src/server/api/organization.js | 32 ------------------ src/server/api/schema.js | 41 ----------------------- 5 files changed, 27 insertions(+), 115 deletions(-) diff --git a/__test__/test_helpers.js b/__test__/test_helpers.js index c14843f14..c7f350d77 100644 --- a/__test__/test_helpers.js +++ b/__test__/test_helpers.js @@ -9,6 +9,7 @@ import { r } from "../src/server/models/"; import { graphql } from "graphql"; +import gql from "graphql-tag"; // Cypress integration tests do not use jest but do use these helpers // They would benefit from mocking mail services, though, so something to look in to. @@ -232,49 +233,42 @@ export const ensureOrganizationTwilioWithMessagingService = async ( export async function setTwilioAuth(user, organization) { const rootValue = {}; - const accountSid = "test_twilio_account_sid"; - const authToken = "test_twilio_auth_token"; - const messageServiceSid = "test_message_service"; + const twilioAccountSid = "test_twilio_account_sid"; + const twilioAuthToken = "test_twilio_auth_token"; + const twilioMessageServiceSid = "test_message_service"; const orgId = organization.data.createOrganization.id; const context = getContext({ user }); - const twilioQuery = ` - mutation updateTwilioAuth( - $twilioAccountSid: String - $twilioAuthToken: String - $twilioMessageServiceSid: String - $organizationId: String! - ) { - updateTwilioAuth( - twilioAccountSid: $twilioAccountSid - twilioAuthToken: $twilioAuthToken - twilioMessageServiceSid: $twilioMessageServiceSid - organizationId: $organizationId - ) { - id - twilioAccountSid - twilioAuthToken - twilioMessageServiceSid - } - }`; + const query = ` + mutation updateMessageServiceConfig( + $organizationId: String! + $messageServiceName: String! + $config: JSON! + ) { + updateMessageServiceConfig( + organizationId: $organizationId + messageServiceName: $messageServiceName + config: $config + ) + } + `; + + const twilioConfig = { + twilioAccountSid, + twilioAuthToken, + twilioMessageServiceSid + }; const variables = { organizationId: orgId, - twilioAccountSid: accountSid, - twilioAuthToken: authToken, - twilioMessageServiceSid: messageServiceSid + messageServiceName: "twilio", + config: JSON.stringify(twilioConfig) }; - const result = await graphql( - mySchema, - twilioQuery, - rootValue, - context, - variables - ); + const result = await graphql(mySchema, query, rootValue, context, variables); if (result && result.errors) { - console.log("updateTwilioAuth failed " + JSON.stringify(result)); + console.log("updateMessageServiceConfig failed " + JSON.stringify(result)); } return result; } diff --git a/src/api/organization.js b/src/api/organization.js index 7dd55d44e..6991887bf 100644 --- a/src/api/organization.js +++ b/src/api/organization.js @@ -73,9 +73,6 @@ export const schema = gql` texterUIConfig: TexterUIConfig cacheable: Int tags(group: String): [Tag] - twilioAccountSid: String - twilioAuthToken: String - twilioMessageServiceSid: String messageService: MessageService fullyConfigured: Boolean emailEnabled: Boolean diff --git a/src/api/schema.js b/src/api/schema.js index f14559f3e..abe2784d0 100644 --- a/src/api/schema.js +++ b/src/api/schema.js @@ -292,12 +292,6 @@ const rootSchema = gql` messageServiceName: String! config: JSON! ): JSON - updateTwilioAuth( - organizationId: String! - twilioAccountSid: String - twilioAuthToken: String - twilioMessageServiceSid: String - ): Organization bulkSendMessages(assignmentId: Int!): [CampaignContact] sendMessage( message: MessageInput! diff --git a/src/server/api/organization.js b/src/server/api/organization.js index c462e9297..9237a0b7c 100644 --- a/src/server/api/organization.js +++ b/src/server/api/organization.js @@ -211,38 +211,6 @@ export const resolvers = { cacheable: (org, _, { user }) => // quanery logic. levels are 0, 1, 2 r.redis ? (getConfig("REDIS_CONTACT_CACHE", org) ? 2 : 1) : 0, - twilioAccountSid: async (organization, _, { user }) => { - try { - await accessRequired(user, organization.id, "OWNER"); - return organization.features.indexOf("TWILIO_ACCOUNT_SID") !== -1 - ? JSON.parse(organization.features).TWILIO_ACCOUNT_SID - : null; - } catch (err) { - return null; - } - }, - twilioAuthToken: async (organization, _, { user }) => { - try { - await accessRequired(user, organization.id, "OWNER"); - return JSON.parse(organization.features || "{}") - .TWILIO_AUTH_TOKEN_ENCRYPTED - ? "" - : null; - } catch (err) { - return null; - } - }, - twilioMessageServiceSid: async (organization, _, { user }) => { - try { - await accessRequired(user, organization.id, "OWNER"); - return organization.features.indexOf("TWILIO_MESSAGE_SERVICE_SID") !== - -1 - ? JSON.parse(organization.features).TWILIO_MESSAGE_SERVICE_SID - : null; - } catch (err) { - return null; - } - }, messageService: async (organization, _, { user }) => { try { await accessRequired(user, organization.id, "OWNER"); diff --git a/src/server/api/schema.js b/src/server/api/schema.js index 1540b5f20..7c1cdbdc1 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -48,8 +48,6 @@ import { resolvers as questionResponseResolvers } from "./question-response"; import { resolvers as tagResolvers } from "./tag"; import { getUsers, resolvers as userResolvers } from "./user"; import { change } from "../local-auth-helpers"; -import { symmetricEncrypt } from "./lib/crypto"; -import Twilio from "twilio"; import { bulkSendMessages, @@ -742,45 +740,6 @@ const rootMutations = { return await Organization.get(organizationId); }, - updateTwilioAuth: async ( - _, - { - organizationId, - twilioAccountSid, - twilioAuthToken, - twilioMessageServiceSid - }, - { user } - ) => { - await accessRequired(user, organizationId, "OWNER"); - - const organization = await Organization.get(organizationId); - const featuresJSON = getFeatures(organization); - featuresJSON.TWILIO_ACCOUNT_SID = twilioAccountSid.substr(0, 64); - featuresJSON.TWILIO_AUTH_TOKEN_ENCRYPTED = twilioAuthToken - ? symmetricEncrypt(twilioAuthToken).substr(0, 256) - : twilioAuthToken; - featuresJSON.TWILIO_MESSAGE_SERVICE_SID = twilioMessageServiceSid.substr( - 0, - 64 - ); - organization.features = JSON.stringify(featuresJSON); - - try { - if (twilioAuthToken && global.TEST_ENVIRONMENT !== "1") { - // Make sure Twilio credentials work. - const twilio = Twilio(twilioAccountSid, twilioAuthToken); - const accounts = await twilio.api.accounts.list(); - } - } catch (err) { - throw new GraphQLError("Invalid Twilio credentials"); - } - - await organization.save(); - await cacheableData.organization.clear(organizationId); - - return await Organization.get(organizationId); - }, createInvite: async (_, { invite }, { user }) => { if ( (user && user.is_superadmin) || From ed69105abbf00868f39c06c5018b6d021ca6d48f Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sun, 18 Apr 2021 11:48:31 -0400 Subject: [PATCH 065/191] remove reference to twilioMessageServiceSid --- src/containers/AdminPhoneNumberInventory.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/containers/AdminPhoneNumberInventory.js b/src/containers/AdminPhoneNumberInventory.js index fdcd42fda..dfc3ff00d 100644 --- a/src/containers/AdminPhoneNumberInventory.js +++ b/src/containers/AdminPhoneNumberInventory.js @@ -273,6 +273,9 @@ class AdminPhoneNumberInventory extends React.Component { } renderBuyNumbersForm() { + const messageService = this.props.data.organization.messageService; + const messageServiceName = messageService.name; + const messageServiceConfig = messageService.config || "{}"; return ( - {this.props.data.organization.twilioMessageServiceSid && + {messageServiceName === "twilio" && + messageServiceConfig.TWILIO_MESSAGE_SERVICE_SID && + messageServiceConfig.TWILIO_MESSAGE_SERVICE_SID.length > 0 && !this.props.data.organization.campaignPhoneNumbersEnabled ? ( Date: Sun, 18 Apr 2021 12:12:43 -0400 Subject: [PATCH 066/191] standardize the way we import message services --- .../messaging_services/fakeservice/index.js | 14 ++++++++++---- src/extensions/messaging_services/nexmo/index.js | 12 +++++++++--- src/extensions/messaging_services/service_map.js | 5 +++-- src/extensions/messaging_services/twilio/index.js | 2 +- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/extensions/messaging_services/fakeservice/index.js b/src/extensions/messaging_services/fakeservice/index.js index cd3c761aa..3fb32d740 100644 --- a/src/extensions/messaging_services/fakeservice/index.js +++ b/src/extensions/messaging_services/fakeservice/index.js @@ -18,7 +18,13 @@ export const getMetadata = () => ({ name: "fakeservice" }); -async function sendMessage(message, contact, trx, organization, campaign) { +export async function sendMessage( + message, + contact, + trx, + organization, + campaign +) { const errorCode = message.text.match(/error(\d+)/); const changes = { service: "fakeservice", @@ -110,7 +116,7 @@ async function convertMessagePartsToMessage(messageParts) { }); } -async function handleIncomingMessage(message) { +export async function handleIncomingMessage(message) { const { contact_number, user_number, service_id, text } = message; const pendingMessagePart = new PendingMessagePart({ service: "fakeservice", @@ -125,7 +131,7 @@ async function handleIncomingMessage(message) { return part.id; } -async function buyNumbersInAreaCode(organization, areaCode, limit) { +export async function buyNumbersInAreaCode(organization, areaCode, limit) { const rows = []; for (let i = 0; i < limit; i++) { const last4 = limit.toString().padStart(4, "0"); @@ -144,7 +150,7 @@ async function buyNumbersInAreaCode(organization, areaCode, limit) { return limit; } -async function deleteNumbersInAreaCode(organization, areaCode) { +export async function deleteNumbersInAreaCode(organization, areaCode) { const numbersToDelete = ( await r .knex("owned_phone_number") diff --git a/src/extensions/messaging_services/nexmo/index.js b/src/extensions/messaging_services/nexmo/index.js index d7dc78549..fa89b8589 100644 --- a/src/extensions/messaging_services/nexmo/index.js +++ b/src/extensions/messaging_services/nexmo/index.js @@ -117,7 +117,13 @@ async function rentNewCell() { throw new Error("Did not find any cell"); } -async function sendMessage(message, contact, trx, organization, campaign) { +export async function sendMessage( + message, + contact, + trx, + organization, + campaign +) { if (!nexmo) { const options = trx ? { transaction: trx } : {}; await Message.get(message.id).update({ send_status: "SENT" }, options); @@ -200,7 +206,7 @@ async function sendMessage(message, contact, trx, organization, campaign) { }); } -async function handleDeliveryReport(report) { +export async function handleDeliveryReport(report) { if (report.hasOwnProperty("client-ref")) { const message = await Message.get(report["client-ref"]); // FUTURE: insert log record with JSON.stringify(report) @@ -219,7 +225,7 @@ async function handleDeliveryReport(report) { } } -async function handleIncomingMessage(message) { +export async function handleIncomingMessage(message) { if ( !message.hasOwnProperty("to") || !message.hasOwnProperty("msisdn") || diff --git a/src/extensions/messaging_services/service_map.js b/src/extensions/messaging_services/service_map.js index 472163423..c15a6f1c6 100644 --- a/src/extensions/messaging_services/service_map.js +++ b/src/extensions/messaging_services/service_map.js @@ -1,8 +1,9 @@ -import nexmo from "./nexmo"; +import * as nexmo from "./nexmo"; import * as twilio from "./twilio"; -import fakeservice from "./fakeservice"; +import * as fakeservice from "./fakeservice"; import { getConfig } from "../../server/api/lib/config"; +// TODO this should be built dynamically const serviceMap = { nexmo, twilio, diff --git a/src/extensions/messaging_services/twilio/index.js b/src/extensions/messaging_services/twilio/index.js index 1745aa888..c6b8e5032 100644 --- a/src/extensions/messaging_services/twilio/index.js +++ b/src/extensions/messaging_services/twilio/index.js @@ -112,7 +112,7 @@ export const errorDescriptions = { "-167": "Internal: Initial message altered (initialtext-guard)" }; -function addServerEndpoints(expressApp) { +export function addServerEndpoints(expressApp) { expressApp.post( "/twilio/:orgId?", headerValidator( From 0dfb7707ca2e5f324ba7f8061c7de7f89aabbc73 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sun, 18 Apr 2021 12:44:01 -0400 Subject: [PATCH 067/191] generalized add server endpoints --- .../messaging_services/nexmo/index.js | 36 +++++++++++++++++-- .../messaging_services/service_map.js | 12 +++++++ .../messaging_services/twilio/index.js | 6 +--- src/server/index.js | 34 ++---------------- 4 files changed, 49 insertions(+), 39 deletions(-) diff --git a/src/extensions/messaging_services/nexmo/index.js b/src/extensions/messaging_services/nexmo/index.js index fa89b8589..b17e760d7 100644 --- a/src/extensions/messaging_services/nexmo/index.js +++ b/src/extensions/messaging_services/nexmo/index.js @@ -3,6 +3,7 @@ import { getFormattedPhoneNumber } from "../../../lib/phone-format"; import { Message, PendingMessagePart } from "../../../server/models"; import { getLastMessage } from "../message-sending"; import { log } from "../../../lib"; +import wrap from "../../../server/wrap"; // NEXMO error_codes: // If status is a number, then it will be the number @@ -28,6 +29,36 @@ if (process.env.NEXMO_API_KEY && process.env.NEXMO_API_SECRET) { }); } +export function addServerEndpoints(expressApp) { + if (process.env.NEXMO_API_KEY) { + expressApp.post( + "/nexmo", + wrap(async (req, res) => { + try { + const messageId = await nexmo.handleIncomingMessage(req.body); + res.send(messageId); + } catch (ex) { + log.error(ex); + res.send("done"); + } + }) + ); + + expressApp.post( + "/nexmo-message-report", + wrap(async (req, res) => { + try { + const body = req.body; + await nexmo.handleDeliveryReport(body); + } catch (ex) { + log.error(ex); + } + res.send("done"); + }) + ); + } +} + async function convertMessagePartsToMessage(messageParts) { const firstPart = messageParts[0]; const userNumber = firstPart.user_number; @@ -171,7 +202,7 @@ export async function sendMessage( messageToSave.error_code = Number(hasError) || hasError.charCodeAt(0); } - let options = { conflict: "update" }; + const options = { conflict: "update" }; if (trx) { options.transaction = trx; } @@ -187,7 +218,7 @@ export async function sendMessage( }); // FUTURE: insert log record with service response } else { - let options = { conflict: "update" }; + const options = { conflict: "update" }; if (trx) { options.transaction = trx; } @@ -264,6 +295,7 @@ export async function handleIncomingMessage(message) { } export default { + addServerEndpoints, convertMessagePartsToMessage, findNewCell, rentNewCell, diff --git a/src/extensions/messaging_services/service_map.js b/src/extensions/messaging_services/service_map.js index c15a6f1c6..858e86024 100644 --- a/src/extensions/messaging_services/service_map.js +++ b/src/extensions/messaging_services/service_map.js @@ -10,6 +10,18 @@ const serviceMap = { fakeservice }; +export const addServerEndpoints = app => { + Object.keys(serviceMap).forEach(serviceName => { + const serviceAddServerEndpoints = exports.tryGetFunctionFromService( + serviceName, + "addServerEndpoints" + ); + if (serviceAddServerEndpoints) { + serviceAddServerEndpoints(app); + } + }); +}; + export const getConfigKey = serviceName => `message_service_${serviceName}`; export const getService = serviceName => serviceMap[serviceName]; diff --git a/src/extensions/messaging_services/twilio/index.js b/src/extensions/messaging_services/twilio/index.js index c6b8e5032..3e01b84ca 100644 --- a/src/extensions/messaging_services/twilio/index.js +++ b/src/extensions/messaging_services/twilio/index.js @@ -72,11 +72,7 @@ const headerValidator = url => { url }; - return twilioLibrary.default.Twilio.webhook(authToken, options)( - req, - res, - next - ); + return twilioLibrary.webhook(authToken, options)(req, res, next); }; }; diff --git a/src/server/index.js b/src/server/index.js index c8120b078..3d1b8f251 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -11,11 +11,9 @@ import { schema } from "../api/schema"; import passport from "passport"; import cookieSession from "cookie-session"; import passportSetup from "./auth-passport"; -import wrap from "./wrap"; import { log } from "../lib"; import telemetry from "./telemetry"; -import nexmo from "../extensions/messaging_services/nexmo"; -import twilio from "../extensions/messaging_services/twilio"; +import { addServerEndpoints as messagingServicesAddServerEndpoints } from "../extensions/messaging_services/service_map"; import { getConfig } from "./api/lib/config"; import { seedZipCodes } from "./seeds/seed-zip-codes"; import { setupUserNotificationObservers } from "./notifications"; @@ -113,35 +111,7 @@ Object.keys(configuredIngestMethods).forEach(ingestMethodName => { } }); -twilio.addServerEndpoints(app); - -if (process.env.NEXMO_API_KEY) { - app.post( - "/nexmo", - wrap(async (req, res) => { - try { - const messageId = await nexmo.handleIncomingMessage(req.body); - res.send(messageId); - } catch (ex) { - log.error(ex); - res.send("done"); - } - }) - ); - - app.post( - "/nexmo-message-report", - wrap(async (req, res) => { - try { - const body = req.body; - await nexmo.handleDeliveryReport(body); - } catch (ex) { - log.error(ex); - } - res.send("done"); - }) - ); -} +messagingServicesAddServerEndpoints(app); app.get("/logout-callback", (req, res) => { req.logOut(); From 0e544b64c7793129884272be9155856f5770eb35 Mon Sep 17 00:00:00 2001 From: Larry Person Date: Sun, 18 Apr 2021 23:28:22 -0400 Subject: [PATCH 068/191] add messaging service handlers for downtime --- .../messaging_services/nexmo/index.js | 6 ++-- .../messaging_services/service_map.js | 11 ++++++-- .../messaging_services/twilio/index.js | 6 ++-- src/server/downtime.js | 28 ++++++++++++------- src/server/index.js | 7 ++++- 5 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/extensions/messaging_services/nexmo/index.js b/src/extensions/messaging_services/nexmo/index.js index b17e760d7..2929b8d62 100644 --- a/src/extensions/messaging_services/nexmo/index.js +++ b/src/extensions/messaging_services/nexmo/index.js @@ -29,9 +29,9 @@ if (process.env.NEXMO_API_KEY && process.env.NEXMO_API_SECRET) { }); } -export function addServerEndpoints(expressApp) { +export function addServerEndpoints(addPostRoute) { if (process.env.NEXMO_API_KEY) { - expressApp.post( + addPostRoute( "/nexmo", wrap(async (req, res) => { try { @@ -44,7 +44,7 @@ export function addServerEndpoints(expressApp) { }) ); - expressApp.post( + addPostRoute( "/nexmo-message-report", wrap(async (req, res) => { try { diff --git a/src/extensions/messaging_services/service_map.js b/src/extensions/messaging_services/service_map.js index 858e86024..dcb821e94 100644 --- a/src/extensions/messaging_services/service_map.js +++ b/src/extensions/messaging_services/service_map.js @@ -10,14 +10,21 @@ const serviceMap = { fakeservice }; -export const addServerEndpoints = app => { +export const addServerEndpoints = (app, adders) => { Object.keys(serviceMap).forEach(serviceName => { const serviceAddServerEndpoints = exports.tryGetFunctionFromService( serviceName, "addServerEndpoints" ); if (serviceAddServerEndpoints) { - serviceAddServerEndpoints(app); + serviceAddServerEndpoints( + (route, handler) => { + adders.post(app, route, handler); + }, + (route, handler) => { + adders.get(app, route, handler); + } + ); } }); }; diff --git a/src/extensions/messaging_services/twilio/index.js b/src/extensions/messaging_services/twilio/index.js index 3e01b84ca..21b7b8a7c 100644 --- a/src/extensions/messaging_services/twilio/index.js +++ b/src/extensions/messaging_services/twilio/index.js @@ -108,8 +108,8 @@ export const errorDescriptions = { "-167": "Internal: Initial message altered (initialtext-guard)" }; -export function addServerEndpoints(expressApp) { - expressApp.post( +export function addServerEndpoints(addPostRoute) { + addPostRoute( "/twilio/:orgId?", headerValidator( process.env.TWILIO_MESSAGE_CALLBACK_URL || @@ -153,7 +153,7 @@ export function addServerEndpoints(expressApp) { }) ); - expressApp.post("/twilio-message-report/:orgId?", ...messageReportHooks); + addPostRoute("/twilio-message-report/:orgId?", ...messageReportHooks); } async function convertMessagePartsToMessage(messageParts) { diff --git a/src/server/downtime.js b/src/server/downtime.js index 0049479a2..7ac71363b 100644 --- a/src/server/downtime.js +++ b/src/server/downtime.js @@ -3,15 +3,13 @@ import bodyParser from "body-parser"; import express from "express"; import { log } from "../lib"; import renderIndex from "./middleware/render-index"; -import fs from "fs"; -import { existsSync } from "fs"; +import fs, { existsSync } from "fs"; import path from "path"; +import { addServerEndpoints as messagingServicesAddServerEndpoints } from "../extensions/messaging_services/service_map"; // This server is for when it is in downtime mode and we just statically // serve the client app -const DEBUG = process.env.NODE_ENV === "development"; - const app = express(); const port = process.env.DEV_APP_PORT || process.env.PORT; // Don't rate limit heroku @@ -30,7 +28,7 @@ if (existsSync(process.env.ASSETS_DIR)) { ); } -let assetMap = { +const assetMap = { "bundle.js": "/assets/bundle.js" }; if (process.env.NODE_ENV === "production") { @@ -56,13 +54,23 @@ if (process.env.NODE_ENV === "production") { } } +const serverIsDown = handler => (req, res, next) => { + if (process.env.DOWNTIME_NO_DB) { + return res.status(500).send("Server is down"); + } + return handler(req, res, next); +}; + +const routeAdders = { + get: (_app, route, handler) => _app.get(route, serverIsDown(handler)), + post: (_app, route, handler) => _app.post(route, serverIsDown(handler)) +}; + +messagingServicesAddServerEndpoints(app, routeAdders); + app.use((req, res, next) => { if (req.path !== "/downtime") { - if (/twilio|nexmo/.test(req.path) && process.env.DOWNTIME_NO_DB) { - res.status(500).send("Server is down"); - } else { - res.redirect(302, "/downtime"); - } + res.redirect(302, "/downtime"); } else { res.send(renderIndex("", "", assetMap)); } diff --git a/src/server/index.js b/src/server/index.js index 3d1b8f251..7de6e0cf0 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -111,7 +111,12 @@ Object.keys(configuredIngestMethods).forEach(ingestMethodName => { } }); -messagingServicesAddServerEndpoints(app); +const routeAdders = { + get: (_app, route, handler) => _app.get(route, handler), + post: (_app, route, handler) => _app.post(route, handler) +}; + +messagingServicesAddServerEndpoints(app, routeAdders); app.get("/logout-callback", (req, res) => { req.logOut(); From a5e5778dc6220479f8fc8d394009c9647de215e4 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Fri, 23 Apr 2021 17:51:46 -0400 Subject: [PATCH 069/191] fix tests after lperson:lperson/pluggable_messaging_services --- __test__/test_helpers.js | 2 +- src/extensions/messaging_services/twilio/index.js | 10 ++++++++-- src/workers/jobs.js | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/__test__/test_helpers.js b/__test__/test_helpers.js index c7f350d77..5b661bb34 100644 --- a/__test__/test_helpers.js +++ b/__test__/test_helpers.js @@ -233,7 +233,7 @@ export const ensureOrganizationTwilioWithMessagingService = async ( export async function setTwilioAuth(user, organization) { const rootValue = {}; - const twilioAccountSid = "test_twilio_account_sid"; + const twilioAccountSid = "ACtest_twilio_account_sid"; const twilioAuthToken = "test_twilio_auth_token"; const twilioMessageServiceSid = "test_message_service"; const orgId = organization.data.createOrganization.id; diff --git a/src/extensions/messaging_services/twilio/index.js b/src/extensions/messaging_services/twilio/index.js index 64d94cc8b..1d2fae951 100644 --- a/src/extensions/messaging_services/twilio/index.js +++ b/src/extensions/messaging_services/twilio/index.js @@ -256,8 +256,14 @@ async function getOrganizationContactUserNumber(organization, contactNumber) { return null; } -async function sendMessage(message, contact, trx, organization, campaign) { - const twilio = await getTwilio(organization); +export async function sendMessage( + message, + contact, + trx, + organization, + campaign +) { + const twilio = await exports.getTwilio(organization); const APITEST = /twilioapitest/.test(message.text); if (!twilio && !APITEST) { log.warn( diff --git a/src/workers/jobs.js b/src/workers/jobs.js index 3202d4b96..0d8522089 100644 --- a/src/workers/jobs.js +++ b/src/workers/jobs.js @@ -966,7 +966,7 @@ export async function sendMessages(queryFunc, defaultStatus) { message.id ); } - message.service = message.service || process.env.DEFAULT_SERVICE; + message.service = message.service || getConfig("DEFAULT_SERVICE"); const service = serviceMap[message.service]; log.info( `Sending (${message.service}): ${message.user_number} -> ${message.contact_number}\nMessage: ${message.text}` From a9a4cb88d15c00ec4a10525f9f017686ffe4ae2a Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Wed, 28 Apr 2021 14:38:11 -0400 Subject: [PATCH 070/191] schema changes/adjustments for sticky sender and other models --- .../messaging_services/twilio.test.js | 2 +- migrations/20190207220000_init_db.js | 8 +- .../20200820144200_organization_contacts.js | 29 ----- .../20210220144200_organization_contacts.js | 63 +++++++++++ .../20210426112734_add_delivery_confirmed.js | 39 +++++++ .../messaging_services/twilio/index.js | 10 +- .../cacheable_queries/campaign-contact.js | 30 ++--- .../models/cacheable_queries/message.js | 103 ++++++------------ .../cacheable_queries/organization-contact.js | 7 +- src/server/models/message.js | 10 +- src/server/models/organization-contact.js | 28 ++++- 11 files changed, 196 insertions(+), 133 deletions(-) delete mode 100644 migrations/20200820144200_organization_contacts.js create mode 100644 migrations/20210220144200_organization_contacts.js create mode 100644 migrations/20210426112734_add_delivery_confirmed.js diff --git a/__test__/extensions/messaging_services/twilio.test.js b/__test__/extensions/messaging_services/twilio.test.js index 83419fd2b..07bb29305 100644 --- a/__test__/extensions/messaging_services/twilio.test.js +++ b/__test__/extensions/messaging_services/twilio.test.js @@ -465,7 +465,7 @@ describe("twilio", () => { org2 ); expect(org2Auth.authToken).toBe(""); - expect(org2Auth.accountSid).toBe("test_twilio_account_sid"); + expect(org2Auth.accountSid).toBe("ACtest_twilio_account_sid"); }); describe("Number buying", () => { diff --git a/migrations/20190207220000_init_db.js b/migrations/20190207220000_init_db.js index 85ea3fc4e..6dd304be3 100644 --- a/migrations/20190207220000_init_db.js +++ b/migrations/20190207220000_init_db.js @@ -350,7 +350,7 @@ const initialize = async knex => { t.text("service_id") .notNullable() .defaultTo(""); - t.enu("send_status", [ + const statuses = [ "QUEUED", "SENDING", "SENT", @@ -358,7 +358,11 @@ const initialize = async knex => { "ERROR", "PAUSED", "NOT_ATTEMPTED" - ]).notNullable(); + ]; + if (isSqlite) { + statuses.push("DELIVERED CONFIRMED"); + } + t.enu("send_status", statuses).notNullable(); t.timestamp("created_at") .defaultTo(knex.fn.now()) .notNullable(); diff --git a/migrations/20200820144200_organization_contacts.js b/migrations/20200820144200_organization_contacts.js deleted file mode 100644 index 57a76dd7d..000000000 --- a/migrations/20200820144200_organization_contacts.js +++ /dev/null @@ -1,29 +0,0 @@ -exports.up = async function up(knex, Promise) { - await knex.schema.alterTable("message", t => { - t.index(["contact_number", "user_number"], "cell_user_number_idx"); - }); - - if (!(await knex.schema.hasTable("organization_contact"))) { - await knex.schema.createTable("organization_contact", t => { - t.increments("id"); - t.text("organization_id"); - t.text("contact_number"); - t.text("user_number"); - - t.index( - ["organization_id", "contact_number"], - "organization_contact_organization_contact_number" - ); - - t.unique(["organization_id", "contact_number"]); - }); - } -}; - -exports.down = async function down(knex, Promise) { - await knex.schema.alterTable("message", t => { - t.dropIndex("cell_user_number_idx"); - }); - - await knex.schema.dropTableIfExists("organization_contact"); -}; diff --git a/migrations/20210220144200_organization_contacts.js b/migrations/20210220144200_organization_contacts.js new file mode 100644 index 000000000..782f32e49 --- /dev/null +++ b/migrations/20210220144200_organization_contacts.js @@ -0,0 +1,63 @@ +exports.up = async function up(knex) { + const isSqlite = /sqlite/.test(knex.client.config.client); + + if (!process.env.NO_INDEX_CHANGES) { + // For Postgres, consider concurrent creation with manual command: + // CREATE INDEX CONCURRENTLY cell_msgsvc_user_number_idx ON message (contact_number, messageservice_sid, user_number); + // DROP INDEX cell_messageservice_sid_idx; + await knex.schema.alterTable("message", t => { + // we need user_number indexed for when/if service has no messageservice_sid and only indexes by phone numbers + t.index( + ["contact_number", "messageservice_sid", "user_number"], + "cell_msgsvc_user_number_idx" + ); + // sqlite is not good at dropping indexes and index might not be named + // t.dropIndex("cell_messageservice_sid_idx"); + }); + } + + await knex.schema.createTable("organization_contact", t => { + t.increments("id").primary(); + t.integer("organization_id") + .references("id") + .inTable("organization"); + t.text("contact_number").notNullable(); + t.text("user_number"); + t.integer("subscribe_status").defaultTo(0); + t.text("carrier"); + t.timestamp("created_at").defaultTo(knex.fn.now()); + t.timestamp("last_lookup").defaultTo(); + t.text("lookup_name"); + + t.index( + // putting contact_number first, in-case queries can ever span organizations + ["contact_number", "organization_id"], + "organization_contact_organization_contact_number" + ); + + t.index( + ["organization_id", "user_number"], + "organization_contact_organization_user_number" + ); + t.unique(["contact_number", "organization_id"]); + }); +}; + +exports.down = async function down(knex) { + const isSqlite = /sqlite/.test(knex.client.config.client); + if (!isSqlite && !process.env.NO_INDEX_CHANGES) { + try { + await knex.schema.alterTable("message", t => { + t.index( + ["contact_number", "messageservice_sid"], + "cell_messageservice_sid_idx" + ); + t.dropIndex("cell_msgsvc_user_number_idx"); + }); + } catch (err) { + // pass if indexes exist and/or dropped + } + } + + await knex.schema.dropTableIfExists("organization_contact"); +}; diff --git a/migrations/20210426112734_add_delivery_confirmed.js b/migrations/20210426112734_add_delivery_confirmed.js new file mode 100644 index 000000000..c59e8b1ce --- /dev/null +++ b/migrations/20210426112734_add_delivery_confirmed.js @@ -0,0 +1,39 @@ +// Add DELIVERED CONFIRMED as a value in the `send_status` enumeration +exports.up = knex => { + const isSqlite = /sqlite/.test(knex.client.config.client); + if (isSqlite) { + return Promise.resolve(); + } + return knex.schema.raw(` + ALTER TABLE "message" DROP CONSTRAINT IF EXISTS "message_send_status_check"; + ALTER TABLE "message" ADD CONSTRAINT "message_send_status_check" CHECK (send_status IN ( + 'QUEUED'::text, + 'SENDING'::text, + 'SENT'::text, + 'DELIVERED'::text, + 'DELIVERED CONFIRMED'::text, + 'ERROR'::text, + 'PAUSED'::text, + 'NOT_ATTEMPTED'::text + )) + `); +}; + +exports.down = knex => { + const isSqlite = /sqlite/.test(knex.client.config.client); + if (isSqlite) { + return Promise.resolve(); + } + return knex.schema.raw(` + ALTER TABLE "message" DROP CONSTRAINT IF EXISTS "message_send_status_check"; + ALTER TABLE "message" ADD CONSTRAINT "message_send_status_check" CHECK (send_status IN ( + 'QUEUED'::text, + 'SENDING'::text, + 'SENT'::text, + 'DELIVERED'::text, + 'ERROR'::text, + 'PAUSED'::text, + 'NOT_ATTEMPTED'::text + )) + `); +}; diff --git a/src/extensions/messaging_services/twilio/index.js b/src/extensions/messaging_services/twilio/index.js index 1d2fae951..4e73a00a4 100644 --- a/src/extensions/messaging_services/twilio/index.js +++ b/src/extensions/messaging_services/twilio/index.js @@ -338,7 +338,8 @@ export async function sendMessage( if (userNumber) { changes.user_number = userNumber; - } else { + } + if (messagingServiceSid) { changes.messageservice_sid = messagingServiceSid; } @@ -348,11 +349,8 @@ export async function sendMessage( body: message.text, statusCallback: process.env.TWILIO_STATUS_CALLBACK_URL }, - userNumber - ? { from: userNumber } - : messagingServiceSid - ? { messagingServiceSid } - : {}, + userNumber ? { from: userNumber } : {}, + messagingServiceSid ? { messagingServiceSid } : {}, twilioValidityPeriod ? { validityPeriod: twilioValidityPeriod } : {}, parseMessageText(message) ); diff --git a/src/server/models/cacheable_queries/campaign-contact.js b/src/server/models/cacheable_queries/campaign-contact.js index 56b544857..cae699eda 100644 --- a/src/server/models/cacheable_queries/campaign-contact.js +++ b/src/server/models/cacheable_queries/campaign-contact.js @@ -392,25 +392,27 @@ const campaignContactCache = { return false; } } - - const assignmentFilter = messageServiceSid - ? { messageservice_sid: messageServiceSid } - : { user_number: userNumber }; let messageQuery = r .knex("message") .select("campaign_contact_id") - .where( - Object.assign( - { - is_from_contact: false, - contact_number: cell, - service - }, - assignmentFilter - ) - ) + .where({ + is_from_contact: false, + contact_number: cell, + service + }) .orderBy("message.created_at", "desc") .limit(1); + + if (messageServiceSid) { + messageQuery = messageQuery.where( + "messageservice_sid", + messageServiceSid + ); + } else { + messageQuery = messageQuery + .whereNull("messageservice_sid") + .where("user_number", userNumber); + } if (r.redis) { // we get the campaign_id so we can cache errorCount and needsResponseCount messageQuery = messageQuery diff --git a/src/server/models/cacheable_queries/message.js b/src/server/models/cacheable_queries/message.js index 8ba518ae1..3c1dce1fc 100644 --- a/src/server/models/cacheable_queries/message.js +++ b/src/server/models/cacheable_queries/message.js @@ -24,45 +24,6 @@ const dbQuery = ({ campaignContactId }) => { .orderBy("created_at"); }; -const contactIdFromOther = async ({ - campaignContactId, - assignmentId, - cell, - service, - messageServiceSid -}) => { - if (campaignContactId) { - return campaignContactId; - } - console.log( - "messageCache contactIdfromother hard", - campaignContactId, - assignmentId, - cell, - service - ); - - if (!assignmentId || !cell || !messageServiceSid) { - throw new Error(`campaignContactId required or assignmentId-cell-service-messageServiceSid triple required. - cell: ${cell}, messageServivceSid: ${messageServiceSid}, assignmentId: ${assignmentId} - `); - } - if (r.redis) { - const cellLookup = await campaignContactCache.lookupByCell( - cell, - service || "", - messageServiceSid, - null, // is this even used? - /* bailWithoutCache*/ true - ); - if (cellLookup) { - return cellLookup.campaign_contact_id; - } - } - // TODO: more ways and by db -- is this necessary if the active-campaign-postmigration edgecase goes away? - return null; -}; - const saveMessageCache = async (contactId, contactMessages, overwriteFull) => { if (r.redis && CONTACT_CACHE_ENABLED) { const key = cacheKey(contactId); @@ -108,7 +69,6 @@ const cacheDbResult = async dbResult => { const query = async ({ campaignContactId, justCache }) => { // queryObj ~ { campaignContactId, assignmentId, cell, service, messageServiceSid } if (r.redis && CONTACT_CACHE_ENABLED) { - // campaignContactId = await contactIdFromOther(queryObj); if (campaignContactId) { const [exists, messages] = await r.redis .multi() @@ -191,10 +151,11 @@ const deliveryReport = async ({ if (userNumber) { changes.user_number = userNumber; } + let lookup; if (newStatus === "ERROR") { changes.error_code = errorCode; - const lookup = await campaignContactCache.lookupByCell( + lookup = await campaignContactCache.lookupByCell( contactNumber, service || "", messageServiceSid, @@ -218,40 +179,46 @@ const deliveryReport = async ({ .limit(1) .update(changes); - if (process.env.EXPERIMENTAL_STICKY_SENDER && newStatus === "DELIVERED") { - const [message] = await r - .knex("message") - .where("service_id", messageSid) - .limit(1); - - // Assign user number to contact/organization - const campaignContact = await campaignContactCache.load( - message.campaign_contact_id - ); - - const organizationId = await campaignContactCache.orgId(campaignContact); - const organizationContact = await organizationContactCache.query({ - organizationId, - contactNumber - }); + // TODO: move the below into a test for service-strategies if there are onDeliveryReport impls + // which uses campaignContactCache.lookupByCell above + if ( + userNumber && + process.env.EXPERIMENTAL_STICKY_SENDER && + newStatus === "DELIVERED" + ) { + lookup = + lookup || + (await campaignContactCache.lookupByCell( + contactNumber, + service || "", + messageServiceSid, + userNumber + )); + // FUTURE: maybe don't do anything if userNumber existed before above save in message? + if (lookup) { + // Assign user number to contact/organization + const campaignContact = await campaignContactCache.load( + lookup.campaign_contact_id + ); - if (!organizationContact) { - await organizationContactCache.save({ - organizationId: organizationId, - contactNumber: contactNumber, - userNumber: userNumber + const organizationId = await campaignContactCache.orgId(campaignContact); + const organizationContact = await organizationContactCache.query({ + organizationId, + contactNumber }); + + if (!organizationContact) { + await organizationContactCache.save({ + organizationId, + contactNumber, + userNumber + }); + } } } }; const messageCache = { - clearQuery: async queryObj => { - if (r.redis) { - const contactId = await contactIdFromOther(queryObj); - await r.redis.delAsync(cacheKey(contactId)); - } - }, deliveryReport, query, save: async ({ diff --git a/src/server/models/cacheable_queries/organization-contact.js b/src/server/models/cacheable_queries/organization-contact.js index 123dca377..25c1f51c6 100644 --- a/src/server/models/cacheable_queries/organization-contact.js +++ b/src/server/models/cacheable_queries/organization-contact.js @@ -17,13 +17,12 @@ const organizationContactCache = { } const organizationContact = await r - .table("organization_contact") - .filter({ + .knex("organization_contact") + .where({ organization_id: organizationId, contact_number: contactNumber }) - .limit(1)(0) - .default(null); + .first(); if (r.redis) { await r.redis diff --git a/src/server/models/message.js b/src/server/models/message.js index ddcfe29c0..56e610de2 100644 --- a/src/server/models/message.js +++ b/src/server/models/message.js @@ -40,6 +40,7 @@ const Message = thinky.createModel( "SENDING", "SENT", "DELIVERED", + "DELIVERED CONFIRMED", "ERROR", "PAUSED", "NOT_ATTEMPTED" @@ -64,13 +65,10 @@ Message.ensureIndex("campaign_contact_id"); Message.ensureIndex("send_status"); //Message.ensureIndex("contact_number"); Message.ensureIndex("service_id"); -Message.ensureIndex("cell_messageservice_sid_idx", doc => [ - doc("contact_number"), - doc("messageservice_sid") -]); - -Message.ensureIndex("cell_user_number_idx", doc => [ +Message.ensureIndex("cell_msgsvc_user_number_idx", doc => [ doc("contact_number"), + doc("messageservice_sid"), + // for when/if there is no service messageservice, we then index by number doc("user_number") ]); diff --git a/src/server/models/organization-contact.js b/src/server/models/organization-contact.js index 92290413e..a04dfc524 100644 --- a/src/server/models/organization-contact.js +++ b/src/server/models/organization-contact.js @@ -1,6 +1,12 @@ import thinky from "./thinky"; const type = thinky.type; -import { optionalString, requiredString } from "./custom-types"; +import { + optionalString, + requiredString, + timestamp, + optionalTimestamp +} from "./custom-types"; +import Organization from "./organization"; // For documentation purposes only. Use knex queries instead of this model. const OrganizationContact = thinky.createModel( @@ -11,9 +17,25 @@ const OrganizationContact = thinky.createModel( id: type.string(), organization_id: requiredString(), contact_number: requiredString(), - user_number: optionalString() + user_number: optionalString(), + subscribe_status: type.integer().default(0), + carrier: optionalString(), + created_at: timestamp(), + last_lookup: optionalTimestamp(), + lookup_name: optionalString() }) - .allowExtra(false) + .allowExtra(false), + { noAutoCreation: true, dependencies: [Organization] } +); + +OrganizationContact.ensureIndex( + "organization_contact_organization_contact_number", + doc => [doc("contact_number"), doc("organization_id")] +); + +OrganizationContact.ensureIndex( + "organization_contact_organization_user_number", + doc => [doc("organization_id"), doc("user_number")] ); export default OrganizationContact; From 37f817438d4005d125fb1fca27383b959b80d190 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Wed, 28 Apr 2021 14:51:10 -0400 Subject: [PATCH 071/191] rename messaging_services => service-vendors to help distinguish between a vendor and a "messaging service" (internal abstraction in Twilio and some other vendors) --- .../extensions/message-handlers/profanity-tagger.test.js | 2 +- .../{messaging_services => service-vendors}/index.test.js | 6 +++--- .../service_map.test.js | 2 +- .../{messaging_services => service-vendors}/twilio.test.js | 4 ++-- __test__/lib.test.js | 2 +- __test__/server/api/campaign/campaign.test.js | 2 +- .../server/api/mutations/updateMessageServiceConfig.test.js | 6 +++--- __test__/server/api/organization.test.js | 4 ++-- .../server/models/cacheable_queries/organization.test.js | 2 +- docs/HOWTO-scale-spoke-plan.md | 2 +- src/containers/Settings.jsx | 2 +- .../fakeservice/index.js | 0 .../{messaging_services => service-vendors}/index.js | 0 .../message-sending.js | 0 .../{messaging_services => service-vendors}/nexmo/index.js | 0 .../react-components.js | 0 .../{messaging_services => service-vendors}/service_map.js | 0 .../{messaging_services => service-vendors}/twilio/index.js | 0 .../twilio/react-components/org-config.js | 0 src/server/api/campaign.js | 2 +- src/server/api/mutations/buyPhoneNumbers.js | 2 +- src/server/api/mutations/releaseCampaignNumbers.js | 2 +- src/server/api/mutations/startCampaign.js | 2 +- src/server/api/mutations/updateMessageServiceConfig.js | 2 +- src/server/api/organization.js | 2 +- src/server/api/schema.js | 2 +- src/server/downtime.js | 2 +- src/server/index.js | 2 +- src/server/models/cacheable_queries/organization.js | 2 +- src/workers/jobs.js | 6 +++--- src/workers/tasks.js | 2 +- 31 files changed, 31 insertions(+), 31 deletions(-) rename __test__/extensions/{messaging_services => service-vendors}/index.test.js (82%) rename __test__/extensions/{messaging_services => service-vendors}/service_map.test.js (98%) rename __test__/extensions/{messaging_services => service-vendors}/twilio.test.js (99%) rename src/extensions/{messaging_services => service-vendors}/fakeservice/index.js (100%) rename src/extensions/{messaging_services => service-vendors}/index.js (100%) rename src/extensions/{messaging_services => service-vendors}/message-sending.js (100%) rename src/extensions/{messaging_services => service-vendors}/nexmo/index.js (100%) rename src/extensions/{messaging_services => service-vendors}/react-components.js (100%) rename src/extensions/{messaging_services => service-vendors}/service_map.js (100%) rename src/extensions/{messaging_services => service-vendors}/twilio/index.js (100%) rename src/extensions/{messaging_services => service-vendors}/twilio/react-components/org-config.js (100%) diff --git a/__test__/extensions/message-handlers/profanity-tagger.test.js b/__test__/extensions/message-handlers/profanity-tagger.test.js index e46c80ce9..70533bede 100644 --- a/__test__/extensions/message-handlers/profanity-tagger.test.js +++ b/__test__/extensions/message-handlers/profanity-tagger.test.js @@ -1,5 +1,5 @@ import { r, cacheableData } from "../../../src/server/models"; -import serviceMap from "../../../src/extensions/messaging_services"; +import serviceMap from "../../../src/extensions/service-vendors"; import { available, DEFAULT_PROFANITY_REGEX_BASE64 diff --git a/__test__/extensions/messaging_services/index.test.js b/__test__/extensions/service-vendors/index.test.js similarity index 82% rename from __test__/extensions/messaging_services/index.test.js rename to __test__/extensions/service-vendors/index.test.js index 3539b73f8..e354abb1a 100644 --- a/__test__/extensions/messaging_services/index.test.js +++ b/__test__/extensions/service-vendors/index.test.js @@ -1,7 +1,7 @@ -import * as serviceMap from "../../../src/extensions/messaging_services/service_map"; -import * as messagingServices from "../../../src/extensions/messaging_services/index"; +import * as serviceMap from "../../../src/extensions/service-vendors/service_map"; +import * as messagingServices from "../../../src/extensions/service-vendors/index"; -describe("extensions/messaging_services index", () => { +describe("extensions/service-vendors index", () => { describe("fullyConfigured", () => { let fullyConfiguredFunction; let organization; diff --git a/__test__/extensions/messaging_services/service_map.test.js b/__test__/extensions/service-vendors/service_map.test.js similarity index 98% rename from __test__/extensions/messaging_services/service_map.test.js rename to __test__/extensions/service-vendors/service_map.test.js index 005ba59a0..24fb8e503 100644 --- a/__test__/extensions/messaging_services/service_map.test.js +++ b/__test__/extensions/service-vendors/service_map.test.js @@ -1,4 +1,4 @@ -import * as serviceMap from "../../../src/extensions/messaging_services/service_map"; +import * as serviceMap from "../../../src/extensions/service-vendors/service_map"; import * as config from "../../../src/server/api/lib/config"; describe("service_map", () => { diff --git a/__test__/extensions/messaging_services/twilio.test.js b/__test__/extensions/service-vendors/twilio.test.js similarity index 99% rename from __test__/extensions/messaging_services/twilio.test.js rename to __test__/extensions/service-vendors/twilio.test.js index 07bb29305..f1490807f 100644 --- a/__test__/extensions/messaging_services/twilio.test.js +++ b/__test__/extensions/service-vendors/twilio.test.js @@ -1,7 +1,7 @@ /* eslint-disable no-unused-expressions, consistent-return */ import * as twilioLibrary from "twilio"; -import { getLastMessage } from "../../../src/extensions/messaging_services/message-sending"; -import * as twilio from "../../../src/extensions/messaging_services/twilio"; +import { getLastMessage } from "../../../src/extensions/service-vendors/message-sending"; +import * as twilio from "../../../src/extensions/service-vendors/twilio"; import { getConfig } from "../../../src/server/api/lib/config"; // eslint-disable-line no-duplicate-imports, import/no-duplicates import * as configFunctions from "../../../src/server/api/lib/config"; // eslint-disable-line no-duplicate-imports, import/no-duplicates import crypto from "../../../src/server/api/lib/crypto"; diff --git a/__test__/lib.test.js b/__test__/lib.test.js index 174f00867..4800ee67a 100644 --- a/__test__/lib.test.js +++ b/__test__/lib.test.js @@ -1,6 +1,6 @@ import { resolvers } from "../src/server/api/schema"; import { schema } from "../src/api/schema"; -import twilio from "../src/extensions/messaging_services/twilio"; +import twilio from "../src/extensions/service-vendors/twilio"; import { getConfig, hasConfig } from "../src/server/api/lib/config"; import { makeExecutableSchema } from "graphql-tools"; diff --git a/__test__/server/api/campaign/campaign.test.js b/__test__/server/api/campaign/campaign.test.js index 53ed01b46..1bd458e9f 100644 --- a/__test__/server/api/campaign/campaign.test.js +++ b/__test__/server/api/campaign/campaign.test.js @@ -10,7 +10,7 @@ import { dataQuery as TexterTodoQuery } from "../../../../src/containers/TexterTodo"; import { dataQuery as TexterTodoListQuery } from "../../../../src/containers/TexterTodoList"; -import * as twilio from "../../../../src/extensions/messaging_services/twilio"; +import * as twilio from "../../../../src/extensions/service-vendors/twilio"; import { makeTree } from "../../../../src/lib"; import { getConfig } from "../../../../src/server/api/lib/config"; import { cacheableData, r } from "../../../../src/server/models"; diff --git a/__test__/server/api/mutations/updateMessageServiceConfig.test.js b/__test__/server/api/mutations/updateMessageServiceConfig.test.js index de2a6cff8..26225333f 100644 --- a/__test__/server/api/mutations/updateMessageServiceConfig.test.js +++ b/__test__/server/api/mutations/updateMessageServiceConfig.test.js @@ -1,7 +1,7 @@ import { updateMessageServiceConfigGql } from "../../../../src/containers/Settings"; -// import * as messagingServices from "../../../../src/extensions/messaging_services"; -import * as serviceMap from "../../../../src/extensions/messaging_services/service_map"; -import * as twilio from "../../../../src/extensions/messaging_services/twilio"; +// import * as messagingServices from "../../../../src/extensions/service-vendors"; +import * as serviceMap from "../../../../src/extensions/service-vendors/service_map"; +import * as twilio from "../../../../src/extensions/service-vendors/twilio"; import { r, Organization } from "../../../../src/server/models"; import orgCache from "../../../../src/server/models/cacheable_queries/organization"; import { diff --git a/__test__/server/api/organization.test.js b/__test__/server/api/organization.test.js index 63b91e4fa..56357b192 100644 --- a/__test__/server/api/organization.test.js +++ b/__test__/server/api/organization.test.js @@ -3,7 +3,7 @@ import { r } from "../../../src/server/models/"; import { getCampaignsQuery } from "../../../src/containers/AdminCampaignList"; import { GraphQLError } from "graphql/error"; import gql from "graphql-tag"; -import * as messagingServices from "../../../src/extensions/messaging_services"; +import * as messagingServices from "../../../src/extensions/service-vendors"; import { cleanupTest, @@ -18,7 +18,7 @@ import { } from "../../test_helpers"; import * as srcServerApiErrors from "../../../src/server/api/errors"; import * as orgCache from "../../../src/server/models/cacheable_queries/organization"; -import * as serviceMap from "../../../src/extensions/messaging_services/service_map"; +import * as serviceMap from "../../../src/extensions/service-vendors/service_map"; const ActionHandlerFramework = require("../../../src/extensions/action-handlers"); diff --git a/__test__/server/models/cacheable_queries/organization.test.js b/__test__/server/models/cacheable_queries/organization.test.js index d7ab94bd0..312aa1ac6 100644 --- a/__test__/server/models/cacheable_queries/organization.test.js +++ b/__test__/server/models/cacheable_queries/organization.test.js @@ -1,5 +1,5 @@ import orgCache from "../../../../src/server/models/cacheable_queries/organization"; -import * as serviceMap from "../../../../src/extensions/messaging_services/service_map"; +import * as serviceMap from "../../../../src/extensions/service-vendors/service_map"; describe("cacheable_queries.organization", () => { let serviceWith; diff --git a/docs/HOWTO-scale-spoke-plan.md b/docs/HOWTO-scale-spoke-plan.md index c43492ada..aadac344a 100644 --- a/docs/HOWTO-scale-spoke-plan.md +++ b/docs/HOWTO-scale-spoke-plan.md @@ -155,7 +155,7 @@ Here is the (proposed) structure of data in Redis to support the above data need 4. HSET `replies-` (using lookup) * Code points: - * [twilio backend codepoint](https://github.com/MoveOnOrg/Spoke/blob/main/src/server/extensions/messaging_services/twilio.js#L203) + * [twilio backend codepoint](https://github.com/MoveOnOrg/Spoke/blob/main/src/server/extensions/service-vendors/twilio.js#L203) * note that is called both from server/index.js and workers/jobs.js * In theory we can/should do this generically over services, but pending_message_part complicates this a bit much. A 'middle road' approach would also implement this in server/api/lib/fakeservice.js diff --git a/src/containers/Settings.jsx b/src/containers/Settings.jsx index c1dafd74c..636ec6d97 100644 --- a/src/containers/Settings.jsx +++ b/src/containers/Settings.jsx @@ -17,7 +17,7 @@ import Toggle from "material-ui/Toggle"; import moment from "moment"; import CampaignTexterUIForm from "../components/CampaignTexterUIForm"; import OrganizationFeatureSettings from "../components/OrganizationFeatureSettings"; -import getMessagingServiceConfigComponent from "../extensions/messaging_services/react-components"; +import getMessagingServiceConfigComponent from "../extensions/service-vendors/react-components"; import GSTextField from "../components/forms/GSTextField"; const styles = StyleSheet.create({ diff --git a/src/extensions/messaging_services/fakeservice/index.js b/src/extensions/service-vendors/fakeservice/index.js similarity index 100% rename from src/extensions/messaging_services/fakeservice/index.js rename to src/extensions/service-vendors/fakeservice/index.js diff --git a/src/extensions/messaging_services/index.js b/src/extensions/service-vendors/index.js similarity index 100% rename from src/extensions/messaging_services/index.js rename to src/extensions/service-vendors/index.js diff --git a/src/extensions/messaging_services/message-sending.js b/src/extensions/service-vendors/message-sending.js similarity index 100% rename from src/extensions/messaging_services/message-sending.js rename to src/extensions/service-vendors/message-sending.js diff --git a/src/extensions/messaging_services/nexmo/index.js b/src/extensions/service-vendors/nexmo/index.js similarity index 100% rename from src/extensions/messaging_services/nexmo/index.js rename to src/extensions/service-vendors/nexmo/index.js diff --git a/src/extensions/messaging_services/react-components.js b/src/extensions/service-vendors/react-components.js similarity index 100% rename from src/extensions/messaging_services/react-components.js rename to src/extensions/service-vendors/react-components.js diff --git a/src/extensions/messaging_services/service_map.js b/src/extensions/service-vendors/service_map.js similarity index 100% rename from src/extensions/messaging_services/service_map.js rename to src/extensions/service-vendors/service_map.js diff --git a/src/extensions/messaging_services/twilio/index.js b/src/extensions/service-vendors/twilio/index.js similarity index 100% rename from src/extensions/messaging_services/twilio/index.js rename to src/extensions/service-vendors/twilio/index.js diff --git a/src/extensions/messaging_services/twilio/react-components/org-config.js b/src/extensions/service-vendors/twilio/react-components/org-config.js similarity index 100% rename from src/extensions/messaging_services/twilio/react-components/org-config.js rename to src/extensions/service-vendors/twilio/react-components/org-config.js diff --git a/src/server/api/campaign.js b/src/server/api/campaign.js index 9422483cc..e10dcac2f 100644 --- a/src/server/api/campaign.js +++ b/src/server/api/campaign.js @@ -2,7 +2,7 @@ import { accessRequired } from "./errors"; import { mapFieldsToModel, mapFieldsOrNull } from "./lib/utils"; import twilio, { errorDescriptions -} from "../../extensions/messaging_services/twilio"; +} from "../../extensions/service-vendors/twilio"; import { Campaign, JobRequest, r, cacheableData } from "../models"; import { getUsers } from "./user"; import { getSideboxChoices } from "./organization"; diff --git a/src/server/api/mutations/buyPhoneNumbers.js b/src/server/api/mutations/buyPhoneNumbers.js index 103161f15..a9b8340e3 100644 --- a/src/server/api/mutations/buyPhoneNumbers.js +++ b/src/server/api/mutations/buyPhoneNumbers.js @@ -1,4 +1,4 @@ -import serviceMap from "../../../extensions/messaging_services"; +import serviceMap from "../../../extensions/service-vendors"; import { accessRequired } from "../errors"; import { getConfig } from "../lib/config"; import { cacheableData } from "../../models"; diff --git a/src/server/api/mutations/releaseCampaignNumbers.js b/src/server/api/mutations/releaseCampaignNumbers.js index ecf53231f..e421068c2 100644 --- a/src/server/api/mutations/releaseCampaignNumbers.js +++ b/src/server/api/mutations/releaseCampaignNumbers.js @@ -2,7 +2,7 @@ import { r } from "../../models"; import cacheableData from "../../models/cacheable_queries"; import { accessRequired } from "../errors"; import { getConfig } from "../lib/config"; -import twilio from "../../../extensions/messaging_services/twilio"; +import twilio from "../../../extensions/service-vendors/twilio"; import ownedPhoneNumber from "../lib/owned-phone-number"; export const releaseCampaignNumbers = async (_, { campaignId }, { user }) => { diff --git a/src/server/api/mutations/startCampaign.js b/src/server/api/mutations/startCampaign.js index 87bbeee9b..a446593f7 100644 --- a/src/server/api/mutations/startCampaign.js +++ b/src/server/api/mutations/startCampaign.js @@ -2,7 +2,7 @@ import cacheableData from "../../models/cacheable_queries"; import { r } from "../../models"; import { accessRequired } from "../errors"; import { Notifications, sendUserNotification } from "../../notifications"; -import * as twilio from "../../../extensions/messaging_services/twilio"; +import * as twilio from "../../../extensions/service-vendors/twilio"; import { getConfig } from "../lib/config"; import { jobRunner } from "../../../extensions/job-runners"; import { Tasks } from "../../../workers/tasks"; diff --git a/src/server/api/mutations/updateMessageServiceConfig.js b/src/server/api/mutations/updateMessageServiceConfig.js index 0946cd7f1..8ec8986e8 100644 --- a/src/server/api/mutations/updateMessageServiceConfig.js +++ b/src/server/api/mutations/updateMessageServiceConfig.js @@ -3,7 +3,7 @@ import { getConfigKey, getService, tryGetFunctionFromService -} from "../../../extensions/messaging_services"; +} from "../../../extensions/service-vendors"; import { getConfig } from "../../../server/api/lib/config"; import orgCache from "../../models/cacheable_queries/organization"; import { accessRequired } from "../errors"; diff --git a/src/server/api/organization.js b/src/server/api/organization.js index dcb00e473..39fcb4f6f 100644 --- a/src/server/api/organization.js +++ b/src/server/api/organization.js @@ -12,7 +12,7 @@ import { import { fullyConfigured, getServiceMetadata -} from "../../extensions/messaging_services"; +} from "../../extensions/service-vendors"; export const ownerConfigurable = { // ACTION_HANDLERS: 1, diff --git a/src/server/api/schema.js b/src/server/api/schema.js index 6eba865e9..da997b849 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -37,7 +37,7 @@ import { } from "./errors"; import { resolvers as interactionStepResolvers } from "./interaction-step"; import { resolvers as inviteResolvers } from "./invite"; -import { saveNewIncomingMessage } from "../../extensions/messaging_services/message-sending"; +import { saveNewIncomingMessage } from "../../extensions/service-vendors/message-sending"; import { getConfig, getFeatures } from "./lib/config"; import { resolvers as messageResolvers } from "./message"; import { resolvers as optOutResolvers } from "./opt-out"; diff --git a/src/server/downtime.js b/src/server/downtime.js index 7ac71363b..e42eddaa7 100644 --- a/src/server/downtime.js +++ b/src/server/downtime.js @@ -5,7 +5,7 @@ import { log } from "../lib"; import renderIndex from "./middleware/render-index"; import fs, { existsSync } from "fs"; import path from "path"; -import { addServerEndpoints as messagingServicesAddServerEndpoints } from "../extensions/messaging_services/service_map"; +import { addServerEndpoints as messagingServicesAddServerEndpoints } from "../extensions/service-vendors/service_map"; // This server is for when it is in downtime mode and we just statically // serve the client app diff --git a/src/server/index.js b/src/server/index.js index 7de6e0cf0..1f828bbc9 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -13,7 +13,7 @@ import cookieSession from "cookie-session"; import passportSetup from "./auth-passport"; import { log } from "../lib"; import telemetry from "./telemetry"; -import { addServerEndpoints as messagingServicesAddServerEndpoints } from "../extensions/messaging_services/service_map"; +import { addServerEndpoints as messagingServicesAddServerEndpoints } from "../extensions/service-vendors/service_map"; import { getConfig } from "./api/lib/config"; import { seedZipCodes } from "./seeds/seed-zip-codes"; import { setupUserNotificationObservers } from "./notifications"; diff --git a/src/server/models/cacheable_queries/organization.js b/src/server/models/cacheable_queries/organization.js index e741fd36a..2ec00e2c2 100644 --- a/src/server/models/cacheable_queries/organization.js +++ b/src/server/models/cacheable_queries/organization.js @@ -1,7 +1,7 @@ import { getMessageServiceConfig, tryGetFunctionFromService -} from "../../../extensions/messaging_services/service_map"; +} from "../../../extensions/service-vendors/service_map"; import { getConfig } from "../../api/lib/config"; import { r } from "../../models"; diff --git a/src/workers/jobs.js b/src/workers/jobs.js index 0d8522089..8773322cf 100644 --- a/src/workers/jobs.js +++ b/src/workers/jobs.js @@ -10,12 +10,12 @@ import { import telemetry from "../server/telemetry"; import { log, gunzip, zipToTimeZone, convertOffsetsToStrings } from "../lib"; import { sleep, updateJob } from "./lib"; -import serviceMap from "../extensions/messaging_services"; -import twilio from "../extensions/messaging_services/twilio"; +import serviceMap from "../extensions/service-vendors"; +import twilio from "../extensions/service-vendors/twilio"; import { getLastMessage, saveNewIncomingMessage -} from "../extensions/messaging_services/message-sending"; +} from "../extensions/service-vendors/message-sending"; import importScriptFromDocument from "../server/api/lib/import-script"; import { rawIngestMethod } from "../extensions/contact-loaders"; diff --git a/src/workers/tasks.js b/src/workers/tasks.js index 9c26bf293..99e6fd083 100644 --- a/src/workers/tasks.js +++ b/src/workers/tasks.js @@ -1,7 +1,7 @@ // Tasks are lightweight, fire-and-forget functions run in the background. // Unlike Jobs, tasks are not tracked in the database. // See src/extensions/job-runners/README.md for more details -import serviceMap from "../extensions/messaging_services"; +import serviceMap from "../extensions/service-vendors"; import * as ActionHandlers from "../extensions/action-handlers"; import { cacheableData } from "../server/models"; From 61b215d71df96b2e1734bbc29d980a874c65c450 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Thu, 29 Apr 2021 17:34:40 -0400 Subject: [PATCH 072/191] some deeper renaming. Still a little more cleanup but will wait until we can move campaign phone numbers around --- ...t.js => updateServiceVendorConfig.test.js} | 26 ++++++++-------- __test__/test_helpers.js | 6 ++-- src/api/message-service.js | 5 --- src/api/schema.js | 2 +- src/containers/Settings.jsx | 31 +++++++++---------- src/extensions/service-vendors/components.js | 14 +++++++++ .../service-vendors/fakeservice/index.js | 1 - .../service-vendors/react-components.js | 16 ---------- .../service-vendors/twilio/index.js | 1 - .../org-config.js => react-component.js} | 2 -- src/server/api/mutations/index.js | 2 +- ...Config.js => updateServiceVendorConfig.js} | 4 +-- src/server/api/schema.js | 4 +-- 13 files changed, 51 insertions(+), 63 deletions(-) rename __test__/server/api/mutations/{updateMessageServiceConfig.test.js => updateServiceVendorConfig.test.js} (91%) create mode 100644 src/extensions/service-vendors/components.js delete mode 100644 src/extensions/service-vendors/react-components.js rename src/extensions/service-vendors/twilio/{react-components/org-config.js => react-component.js} (99%) rename src/server/api/mutations/{updateMessageServiceConfig.js => updateServiceVendorConfig.js} (96%) diff --git a/__test__/server/api/mutations/updateMessageServiceConfig.test.js b/__test__/server/api/mutations/updateServiceVendorConfig.test.js similarity index 91% rename from __test__/server/api/mutations/updateMessageServiceConfig.test.js rename to __test__/server/api/mutations/updateServiceVendorConfig.test.js index 26225333f..d1e7ef160 100644 --- a/__test__/server/api/mutations/updateMessageServiceConfig.test.js +++ b/__test__/server/api/mutations/updateServiceVendorConfig.test.js @@ -1,4 +1,4 @@ -import { updateMessageServiceConfigGql } from "../../../../src/containers/Settings"; +import { updateServiceVendorConfigGql } from "../../../../src/containers/Settings"; // import * as messagingServices from "../../../../src/extensions/service-vendors"; import * as serviceMap from "../../../../src/extensions/service-vendors/service_map"; import * as twilio from "../../../../src/extensions/service-vendors/twilio"; @@ -14,7 +14,7 @@ import { setupTest } from "../../../test_helpers"; -describe("updateMessageServiceConfig", () => { +describe("updateServiceVendorConfig", () => { beforeEach(async () => { await setupTest(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); @@ -72,7 +72,7 @@ describe("updateMessageServiceConfig", () => { }); it("returns an error", async () => { - const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); + const gqlResult = await runGql(updateServiceVendorConfigGql, vars, user); expect(gqlResult.errors[0].message).toEqual( "Can't configure this will never be a message service name. It's not the configured message service" ); @@ -88,7 +88,7 @@ describe("updateMessageServiceConfig", () => { if (r.redis) r.redis.flushdb(); }); it("returns an error", async () => { - const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); + const gqlResult = await runGql(updateServiceVendorConfigGql, vars, user); expect(gqlResult.errors[0].message).toEqual( "Can't configure twilio. It's not the configured message service" ); @@ -102,7 +102,7 @@ describe("updateMessageServiceConfig", () => { }); it("returns an error", async () => { - const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); + const gqlResult = await runGql(updateServiceVendorConfigGql, vars, user); expect(gqlResult.errors[0].message).toEqual( "twilio is not a valid message service" ); @@ -116,7 +116,7 @@ describe("updateMessageServiceConfig", () => { }); it("returns an error", async () => { - const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); + const gqlResult = await runGql(updateServiceVendorConfigGql, vars, user); expect(gqlResult.errors[0].message).toEqual( "twilio does not support configuration" ); @@ -130,7 +130,7 @@ describe("updateMessageServiceConfig", () => { }); it("returns an error", async () => { - const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); + const gqlResult = await runGql(updateServiceVendorConfigGql, vars, user); expect(gqlResult.errors[0].message).toEqual("Config is not valid JSON"); expect(twilio.updateConfig).not.toHaveBeenCalled(); }); @@ -144,7 +144,7 @@ describe("updateMessageServiceConfig", () => { }); it("returns an error", async () => { - const gqlResult = await runGql(updateMessageServiceConfigGql, vars, user); + const gqlResult = await runGql(updateServiceVendorConfigGql, vars, user); expect(gqlResult.errors[0].message).toEqual("OH NO!"); }); }); @@ -176,7 +176,7 @@ describe("updateMessageServiceConfig", () => { }) ] ]); - expect(gqlResult.data.updateMessageServiceConfig).toEqual( + expect(gqlResult.data.updateServiceVendorConfig).toEqual( expect.objectContaining(expectedCacheConfig) ); @@ -193,7 +193,7 @@ describe("updateMessageServiceConfig", () => { describe("when features DOES NOT HAVE an existing config for the message service", () => { it("writes message service config in features.configKey", async () => { const gqlResult = await runGql( - updateMessageServiceConfigGql, + updateServiceVendorConfigGql, vars, user ); @@ -216,7 +216,7 @@ describe("updateMessageServiceConfig", () => { }); it("writes message service config in features.configKey", async () => { const gqlResult = await runGql( - updateMessageServiceConfigGql, + updateServiceVendorConfigGql, vars, user ); @@ -242,7 +242,7 @@ describe("updateMessageServiceConfig", () => { }); it("writes individual config components to the top level of features", async () => { const gqlResult = await runGql( - updateMessageServiceConfigGql, + updateServiceVendorConfigGql, vars, user ); @@ -278,7 +278,7 @@ describe("updateMessageServiceConfig", () => { .mockReturnValue(extremelyFakeService); }); it("writes the message service config to features.config_key", async () => { - await runGql(updateMessageServiceConfigGql, vars, user); + await runGql(updateServiceVendorConfigGql, vars, user); dbOrganization = await Organization.get(organization.id); expect(JSON.parse(dbOrganization.features)).toEqual( expect.objectContaining({ [configKey]: expectedConfig }) diff --git a/__test__/test_helpers.js b/__test__/test_helpers.js index 5b661bb34..197cdb9a8 100644 --- a/__test__/test_helpers.js +++ b/__test__/test_helpers.js @@ -241,12 +241,12 @@ export async function setTwilioAuth(user, organization) { const context = getContext({ user }); const query = ` - mutation updateMessageServiceConfig( + mutation updateServiceVendorConfig( $organizationId: String! $messageServiceName: String! $config: JSON! ) { - updateMessageServiceConfig( + updateServiceVendorConfig( organizationId: $organizationId messageServiceName: $messageServiceName config: $config @@ -268,7 +268,7 @@ export async function setTwilioAuth(user, organization) { const result = await graphql(mySchema, query, rootValue, context, variables); if (result && result.errors) { - console.log("updateMessageServiceConfig failed " + JSON.stringify(result)); + console.log("updateServiceVendorConfig failed " + JSON.stringify(result)); } return result; } diff --git a/src/api/message-service.js b/src/api/message-service.js index 385526efd..30efd9be8 100644 --- a/src/api/message-service.js +++ b/src/api/message-service.js @@ -1,13 +1,8 @@ import gql from "graphql-tag"; export const schema = gql` - enum MessageServiceType { - SMS - } - type MessageService { name: String! - type: MessageServiceType! config: JSON supportsOrgConfig: Boolean! supportsCampaignConfig: Boolean! diff --git a/src/api/schema.js b/src/api/schema.js index 010da1dde..f65ab7876 100644 --- a/src/api/schema.js +++ b/src/api/schema.js @@ -295,7 +295,7 @@ const rootSchema = gql` organizationId: String! optOutMessage: String! ): Organization - updateMessageServiceConfig( + updateServiceVendorConfig( organizationId: String! messageServiceName: String! config: JSON! diff --git a/src/containers/Settings.jsx b/src/containers/Settings.jsx index 636ec6d97..73ae5ed37 100644 --- a/src/containers/Settings.jsx +++ b/src/containers/Settings.jsx @@ -17,7 +17,7 @@ import Toggle from "material-ui/Toggle"; import moment from "moment"; import CampaignTexterUIForm from "../components/CampaignTexterUIForm"; import OrganizationFeatureSettings from "../components/OrganizationFeatureSettings"; -import getMessagingServiceConfigComponent from "../extensions/service-vendors/react-components"; +import { getServiceVendorComponent } from "../extensions/service-vendors/components"; import GSTextField from "../components/forms/GSTextField"; const styles = StyleSheet.create({ @@ -137,7 +137,7 @@ class Settings extends React.Component { ); } - renderMessageServiceConfig() { + renderServiceVendorConfig() { const { id: organizationId, messageService } = this.props.data.organization; if (!messageService) { return null; @@ -147,9 +147,9 @@ class Settings extends React.Component { if (!supportsOrgConfig) { return null; } - - const ConfigMessageService = getMessagingServiceConfigComponent(name); - if (!ConfigMessageService) { + const component = getServiceVendorComponent(name); + const ConfigServiceVendor = component.OrgConfig; + if (!ConfigServiceVendor) { return null; } @@ -158,22 +158,22 @@ class Settings extends React.Component { - { - return this.props.mutations.updateMessageServiceConfig(newConfig); + return this.props.mutations.updateServiceVendorConfig(newConfig); }} onAllSetChanged={allSet => { - this.setState({ messageServiceAllSet: allSet }); + this.setState({ serviceVendorAllSet: allSet }); }} requestRefetch={async () => { return this.props.data.refetch(); @@ -261,7 +261,7 @@ class Settings extends React.Component {
{this.renderTextingHoursForm()}
- {this.renderMessageServiceConfig()} + {this.renderServiceVendorConfig()} {this.props.data.organization && this.props.data.organization.texterUIConfig && this.props.data.organization.texterUIConfig.sideboxChoices.length ? ( @@ -379,7 +379,6 @@ const queries = { } messageService { name - type supportsOrgConfig supportsCampaignConfig config @@ -417,13 +416,13 @@ export const editOrganizationGql = gql` } `; -export const updateMessageServiceConfigGql = gql` - mutation updateMessageServiceConfig( +export const updateServiceVendorConfigGql = gql` + mutation updateServiceVendorConfig( $organizationId: String! $messageServiceName: String! $config: JSON! ) { - updateMessageServiceConfig( + updateServiceVendorConfig( organizationId: $organizationId messageServiceName: $messageServiceName config: $config @@ -506,9 +505,9 @@ const mutations = { optOutMessage } }), - updateMessageServiceConfig: ownProps => newConfig => { + updateServiceVendorConfig: ownProps => newConfig => { return { - mutation: updateMessageServiceConfigGql, + mutation: updateServiceVendorConfigGql, variables: { organizationId: ownProps.params.organizationId, messageServiceName: ownProps.data.organization.messageService.name, diff --git a/src/extensions/service-vendors/components.js b/src/extensions/service-vendors/components.js new file mode 100644 index 000000000..369d2b3c0 --- /dev/null +++ b/src/extensions/service-vendors/components.js @@ -0,0 +1,14 @@ +/* eslint no-console: 0 */ +export const getServiceVendorComponent = serviceName => { + try { + // eslint-disable-next-line global-require + const component = require(`./${serviceName}/react-component.js`); + return component; + } catch (caught) { + console.log("caught", caught); + console.error( + `SERVICE_VENDOR failed to load react component for ${serviceName}` + ); + return null; + } +}; diff --git a/src/extensions/service-vendors/fakeservice/index.js b/src/extensions/service-vendors/fakeservice/index.js index 092ed25f0..681fd5ee2 100644 --- a/src/extensions/service-vendors/fakeservice/index.js +++ b/src/extensions/service-vendors/fakeservice/index.js @@ -14,7 +14,6 @@ import uuid from "uuid"; export const getMetadata = () => ({ supportsOrgConfig: false, supportsCampaignConfig: false, - type: "SMS", name: "fakeservice" }); diff --git a/src/extensions/service-vendors/react-components.js b/src/extensions/service-vendors/react-components.js deleted file mode 100644 index 58329eff9..000000000 --- a/src/extensions/service-vendors/react-components.js +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint no-console: 0 */ -export const orgConfig = serviceName => { - try { - // eslint-disable-next-line global-require - const component = require(`./${serviceName}/react-components/org-config.js`); - return component.OrgConfig; - } catch (caught) { - console.log("caught", caught); - console.error( - `MESSAGING_SERVICES failed to load orgConfig reaction component for ${serviceName}` - ); - return null; - } -}; - -export default orgConfig; diff --git a/src/extensions/service-vendors/twilio/index.js b/src/extensions/service-vendors/twilio/index.js index 4e73a00a4..e82a7967a 100644 --- a/src/extensions/service-vendors/twilio/index.js +++ b/src/extensions/service-vendors/twilio/index.js @@ -38,7 +38,6 @@ const MAX_NUMBERS_PER_BUY_JOB = getConfig("MAX_NUMBERS_PER_BUY_JOB") || 100; export const getMetadata = () => ({ supportsOrgConfig: getConfig("TWILIO_MULTI_ORG", null, { truthy: true }), supportsCampaignConfig: false, - type: "SMS", name: "twilio" }); diff --git a/src/extensions/service-vendors/twilio/react-components/org-config.js b/src/extensions/service-vendors/twilio/react-component.js similarity index 99% rename from src/extensions/service-vendors/twilio/react-components/org-config.js rename to src/extensions/service-vendors/twilio/react-component.js index 39b5af916..01c30ab49 100644 --- a/src/extensions/service-vendors/twilio/react-components/org-config.js +++ b/src/extensions/service-vendors/twilio/react-component.js @@ -217,5 +217,3 @@ OrgConfig.propTypes = { onAllSetChanged: PropTypes.func, requestRefetch: PropTypes.func }; - -export default OrgConfig; diff --git a/src/server/api/mutations/index.js b/src/server/api/mutations/index.js index 19d970bbc..2b96af68f 100644 --- a/src/server/api/mutations/index.js +++ b/src/server/api/mutations/index.js @@ -12,4 +12,4 @@ export { updateQuestionResponses } from "./updateQuestionResponses"; export { releaseCampaignNumbers } from "./releaseCampaignNumbers"; export { clearCachedOrgAndExtensionCaches } from "./clearCachedOrgAndExtensionCaches"; export { updateFeedback } from "./updateFeedback"; -export { updateMessageServiceConfig } from "./updateMessageServiceConfig"; +export { updateServiceVendorConfig } from "./updateServiceVendorConfig"; diff --git a/src/server/api/mutations/updateMessageServiceConfig.js b/src/server/api/mutations/updateServiceVendorConfig.js similarity index 96% rename from src/server/api/mutations/updateMessageServiceConfig.js rename to src/server/api/mutations/updateServiceVendorConfig.js index 8ec8986e8..c2e146c6f 100644 --- a/src/server/api/mutations/updateMessageServiceConfig.js +++ b/src/server/api/mutations/updateServiceVendorConfig.js @@ -9,7 +9,7 @@ import orgCache from "../../models/cacheable_queries/organization"; import { accessRequired } from "../errors"; import { Organization } from "../../../server/models"; -export const updateMessageServiceConfig = async ( +export const updateServiceVendorConfig = async ( _, { organizationId, messageServiceName, config }, { user } @@ -87,7 +87,7 @@ export const updateMessageServiceConfig = async ( return orgCache.getMessageServiceConfig(updatedOrganization); }; -export const getMessageServiceConfig = async ( +export const getServiceVendorConfig = async ( serviceName, organization, options = {} diff --git a/src/server/api/schema.js b/src/server/api/schema.js index da997b849..bab455d34 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -65,7 +65,7 @@ import { releaseCampaignNumbers, clearCachedOrgAndExtensionCaches, updateFeedback, - updateMessageServiceConfig + updateServiceVendorConfig } from "./mutations"; import { jobRunner } from "../../extensions/job-runners"; @@ -500,7 +500,7 @@ const rootMutations = { startCampaign, releaseCampaignNumbers, clearCachedOrgAndExtensionCaches, - updateMessageServiceConfig, + updateServiceVendorConfig, userAgreeTerms: async (_, { userId }, { user }) => { // We ignore userId: you can only agree to terms for yourself await r From 2cdab5f3f6ea321ebec557443dcd35f0505e406a Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Thu, 29 Apr 2021 18:28:50 -0400 Subject: [PATCH 073/191] add service-vendor/bandwidth dependencies --- package.json | 2 ++ yarn.lock | 98 +++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 92 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index a10087eb7..0f183442b 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,8 @@ "homepage": "https://github.com/MoveOnOrg/Spoke/#readme", "dependencies": { "@aoberoi/passport-slack": "^1.0.5", + "@bandwidth/messaging": "^3.0.0", + "@bandwidth/numbers": "^1.7.0", "@trt2/gsm-charset-utils": "^1.0.13", "aphrodite": "^2.3.1", "apollo-cache-inmemory": "^1.6.6", diff --git a/yarn.lock b/yarn.lock index 2c2ea9bc6..f1409f08d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14,6 +14,13 @@ needle "^1.4.2" passport-oauth2 "^1.3.0" +"@apimatic/schema@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@apimatic/schema/-/schema-0.4.1.tgz#cb7a122895846638b01f402cc4ca1a32f656aa11" + integrity sha512-KdGp4GaC0sTlcwshahvqZ8OrB/QEM99lxm3sEAo5JgVQP3XH0y/+zeguV8OZhiXRsHERoVZvcI4rKBSHcL84gQ== + dependencies: + lodash.flatten "^4.4.0" + "@babel/code-frame@7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" @@ -1132,6 +1139,29 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@bandwidth/messaging@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@bandwidth/messaging/-/messaging-3.0.0.tgz#7ff19238e2d0d95122ccb29e5adeb375e97c859f" + integrity sha512-DeFOML9leEZOGggb4hSOdJ4mrHmktk+JVYkGCKjNkJPOn0wQsDmse0O4EJLudd71WgmHPWKWXEA4tMFWLPaIjg== + dependencies: + "@apimatic/schema" "^0.4.1" + "@types/node" "^14.14.27" + axios "^0.21.1" + detect-node "^2.0.4" + form-data "^3.0.0" + lodash.flatmap "^4.5.0" + tiny-warning "^1.0.3" + +"@bandwidth/numbers@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@bandwidth/numbers/-/numbers-1.7.0.tgz#0205d4835af11b129ccd615ef57f249b0066f54e" + integrity sha512-Puy2SOlAAyGFnlC4J4Zs/gcBergOqGSOGh4n0q5CvfactEBlCFP999r/rURji9XlPtSOTlZBLLg7lJJJ3Pa7vw== + dependencies: + bluebird "^3.7.2" + streamifier "^0.1.1" + superagent "^3.7.0" + xml2js "^0.4.4" + "@csstools/convert-colors@^1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" @@ -1383,6 +1413,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.20.tgz#f7974863edd21d1f8a494a73e8e2b3658615c340" integrity sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A== +"@types/node@^14.14.27": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -4408,7 +4443,7 @@ compare-versions@^3.6.0: resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== -component-emitter@^1.2.1, component-emitter@^1.3.0: +component-emitter@^1.2.0, component-emitter@^1.2.1, component-emitter@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== @@ -4534,7 +4569,7 @@ cookie@0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== -cookiejar@^2.1.2: +cookiejar@^2.1.0, cookiejar@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA== @@ -6954,7 +6989,7 @@ fork-ts-checker-webpack-plugin@1.0.0-alpha.6: semver "^5.6.0" tapable "^1.0.0" -form-data@^2.3.2: +form-data@^2.3.1, form-data@^2.3.2: version "2.5.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== @@ -6981,7 +7016,7 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -formidable@^1.2.2: +formidable@^1.2.0, formidable@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== @@ -10067,6 +10102,11 @@ lodash.findindex@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.findindex/-/lodash.findindex-4.6.0.tgz#a3245dee61fb9b6e0624b535125624bb69c11106" integrity sha1-oyRd7mH7m24GJLU1ElYku2nBEQY= +lodash.flatmap@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.flatmap/-/lodash.flatmap-4.5.0.tgz#ef8cbf408f6e48268663345305c6acc0b778702e" + integrity sha1-74y/QI9uSCaGYzRTBcaswLd4cC4= + lodash.flatten@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" @@ -10529,7 +10569,7 @@ merge@^1.2.0: resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== -methods@^1.1.2, methods@~1.1.2: +methods@^1.1.1, methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= @@ -10605,7 +10645,7 @@ mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: dependencies: mime-db "1.45.0" -mime@1.6.0, mime@^1.5.0: +mime@1.6.0, mime@^1.4.1, mime@^1.5.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -13068,6 +13108,13 @@ qs@6.7.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@^6.5.1: + version "6.10.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" + integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg== + dependencies: + side-channel "^1.0.4" + qs@^6.5.2, qs@^6.7.0, qs@^6.9.4: version "6.9.6" resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee" @@ -13512,7 +13559,7 @@ read-pkg@^2.0.0: normalize-package-data "^2.3.2" path-type "^2.0.0" -"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -14477,6 +14524,15 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -14839,6 +14895,11 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== +streamifier@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/streamifier/-/streamifier-0.1.1.tgz#97e98d8fa4d105d62a2691d1dc07e820db8dfc4f" + integrity sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8= + strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" @@ -15065,6 +15126,22 @@ stylehacks@^4.0.0: postcss "^7.0.0" postcss-selector-parser "^3.0.0" +superagent@^3.7.0: + version "3.8.3" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128" + integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA== + dependencies: + component-emitter "^1.2.0" + cookiejar "^2.1.0" + debug "^3.1.0" + extend "^3.0.0" + form-data "^2.3.1" + formidable "^1.2.0" + methods "^1.1.1" + mime "^1.4.1" + qs "^6.5.1" + readable-stream "^2.3.5" + superagent@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/superagent/-/superagent-5.3.1.tgz#d62f3234d76b8138c1320e90fa83dc1850ccabf1" @@ -15357,6 +15434,11 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= +tiny-warning@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + tinycolor2@^1.4.1: version "1.4.2" resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803" @@ -16692,7 +16774,7 @@ xml2js@0.4.19: sax ">=0.6.0" xmlbuilder "~9.0.1" -xml2js@^0.4.17: +xml2js@^0.4.17, xml2js@^0.4.4: version "0.4.23" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== From bf0ce1d854f4d72859cfdbb5e67d8bd103b36e64 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Sat, 1 May 2021 10:43:48 -0400 Subject: [PATCH 074/191] remove messageService.type and tweak sendMessage api and support errors across message services --- .../service-vendors/service_map.test.js | 12 ++--- .../extensions/service-vendors/twilio.test.js | 6 ++- __test__/server/api/organization.test.js | 3 +- .../service-vendors/fakeservice/index.js | 4 +- .../service-vendors/message-sending.js | 15 ++++++ src/extensions/service-vendors/nexmo/index.js | 5 +- src/extensions/service-vendors/service_map.js | 32 ++++++++++++- .../service-vendors/twilio/index.js | 46 ++++++------------- src/server/api/campaign.js | 23 +++++----- src/workers/jobs.js | 15 +++--- src/workers/tasks.js | 2 +- 11 files changed, 94 insertions(+), 69 deletions(-) diff --git a/__test__/extensions/service-vendors/service_map.test.js b/__test__/extensions/service-vendors/service_map.test.js index 24fb8e503..cad27eb8d 100644 --- a/__test__/extensions/service-vendors/service_map.test.js +++ b/__test__/extensions/service-vendors/service_map.test.js @@ -106,8 +106,7 @@ describe("service_map", () => { expect(metadata).toEqual({ name: "twilio", supportsCampaignConfig: false, - supportsOrgConfig: false, - type: "SMS" + supportsOrgConfig: false }); }); describe("when TWILIO_MULTI_ORG is true", () => { @@ -119,8 +118,7 @@ describe("service_map", () => { expect(metadata).toEqual({ name: "twilio", supportsCampaignConfig: false, - supportsOrgConfig: true, - type: "SMS" + supportsOrgConfig: true }); }); }); @@ -131,8 +129,7 @@ describe("service_map", () => { expect(metadata).toEqual({ name: "nexmo", supportsCampaignConfig: false, - supportsOrgConfig: false, - type: "SMS" + supportsOrgConfig: false }); }); }); @@ -142,8 +139,7 @@ describe("service_map", () => { expect(metadata).toEqual({ name: "fakeservice", supportsCampaignConfig: false, - supportsOrgConfig: false, - type: "SMS" + supportsOrgConfig: false }); }); }); diff --git a/__test__/extensions/service-vendors/twilio.test.js b/__test__/extensions/service-vendors/twilio.test.js index f1490807f..58bf6fe15 100644 --- a/__test__/extensions/service-vendors/twilio.test.js +++ b/__test__/extensions/service-vendors/twilio.test.js @@ -141,7 +141,11 @@ describe("twilio", () => { cb(null, { sid: "SM12345", error_code: null }); }); - await twilio.sendMessage(message, dbCampaignContact, null, org); + await twilio.sendMessage({ + message, + contact: dbCampaignContact, + organization: org + }); expect(mockMessageCreate).toHaveBeenCalledTimes(1); const arg = mockMessageCreate.mock.calls[0][0]; expect(arg).toMatchObject({ diff --git a/__test__/server/api/organization.test.js b/__test__/server/api/organization.test.js index 56357b192..d2b0210c3 100644 --- a/__test__/server/api/organization.test.js +++ b/__test__/server/api/organization.test.js @@ -332,7 +332,6 @@ describe("organization", async () => { fakeConfig = { fake: "faker_and_faker" }; fakeMetadata = { name: "super_fake", - type: "SMS", supportsOrgConfig: true, supportsCampaignConfig: false }; @@ -355,7 +354,6 @@ describe("organization", async () => { organization(id: $organizationId) { messageService { name - type supportsOrgConfig supportsCampaignConfig config @@ -370,6 +368,7 @@ describe("organization", async () => { }); it("calls functions and returns the result", async () => { const result = await runGql(gqlQuery, variables, testAdminUser); + console.log("result", result); expect(result.data.organization.messageService).toEqual({ ...fakeMetadata, config: fakeConfig diff --git a/src/extensions/service-vendors/fakeservice/index.js b/src/extensions/service-vendors/fakeservice/index.js index 681fd5ee2..3d3bcab41 100644 --- a/src/extensions/service-vendors/fakeservice/index.js +++ b/src/extensions/service-vendors/fakeservice/index.js @@ -17,13 +17,13 @@ export const getMetadata = () => ({ name: "fakeservice" }); -export async function sendMessage( +export async function sendMessage({ message, contact, trx, organization, campaign -) { +}) { const errorCode = message.text.match(/error(\d+)/); const changes = { service: "fakeservice", diff --git a/src/extensions/service-vendors/message-sending.js b/src/extensions/service-vendors/message-sending.js index d4b59f8af..84c6fcc52 100644 --- a/src/extensions/service-vendors/message-sending.js +++ b/src/extensions/service-vendors/message-sending.js @@ -18,3 +18,18 @@ export async function getLastMessage({ export async function saveNewIncomingMessage(messageInstance, contact) { await cacheableData.message.save({ messageInstance, contact }); } + +const mediaExtractor = new RegExp(/\[\s*(http[^\]\s]*)\s*\]/); + +export function parseMessageText(message) { + const text = message.text || ""; + const params = { + body: text.replace(mediaExtractor, "") + }; + // Image extraction + const results = text.match(mediaExtractor); + if (results) { + params.mediaUrl = results[1]; + } + return params; +} diff --git a/src/extensions/service-vendors/nexmo/index.js b/src/extensions/service-vendors/nexmo/index.js index 82575b0e2..e099fd858 100644 --- a/src/extensions/service-vendors/nexmo/index.js +++ b/src/extensions/service-vendors/nexmo/index.js @@ -16,7 +16,6 @@ import wrap from "../../../server/wrap"; export const getMetadata = () => ({ supportsOrgConfig: false, supportsCampaignConfig: false, - type: "SMS", name: "nexmo" }); @@ -149,13 +148,13 @@ async function rentNewCell() { throw new Error("Did not find any cell"); } -export async function sendMessage( +export async function sendMessage({ message, contact, trx, organization, campaign -) { +}) { if (!nexmo) { const options = trx ? { transaction: trx } : {}; await Message.get(message.id).update({ send_status: "SENT" }, options); diff --git a/src/extensions/service-vendors/service_map.js b/src/extensions/service-vendors/service_map.js index dcb821e94..55daed4c8 100644 --- a/src/extensions/service-vendors/service_map.js +++ b/src/extensions/service-vendors/service_map.js @@ -4,7 +4,7 @@ import * as fakeservice from "./fakeservice"; import { getConfig } from "../../server/api/lib/config"; // TODO this should be built dynamically -const serviceMap = { +export const serviceMap = { nexmo, twilio, fakeservice @@ -76,4 +76,34 @@ export const getMessageServiceConfig = async ( return getServiceConfig(config, organization, options); }; +export const internalErrors = { + "-1": "Spoke failed to send the message (1st attempt).", + "-2": "Spoke failed to send the message (2nd attempt).", + "-3": "Spoke failed to send the message (3rd attempt).", + "-4": "Spoke failed to send the message (4th attempt).", + "-5": "Spoke failed to send the message and will NOT try again.", + "-133": "Auto-optout (no error)", + "-166": + "Internal: Message blocked due to text match trigger (profanity-tagger)", + "-167": "Internal: Initial message altered (initialtext-guard)" +}; + +export const errorDescription = (errorCode, service) => { + if (internalErrors[errorCode]) { + return { + code: errorCode, + description: internalErrors[errorCode], + link: null + }; + } else if (serviceMap[service].errorDescription) { + return serviceMap[service].errorDescription(errorCode); + } else { + return { + code: errorCode, + description: "Message Error", + link: null + }; + } +}; + export default serviceMap; diff --git a/src/extensions/service-vendors/twilio/index.js b/src/extensions/service-vendors/twilio/index.js index e82a7967a..3c182186f 100644 --- a/src/extensions/service-vendors/twilio/index.js +++ b/src/extensions/service-vendors/twilio/index.js @@ -13,7 +13,7 @@ import { r } from "../../../server/models"; import wrap from "../../../server/wrap"; -import { saveNewIncomingMessage } from "../message-sending"; +import { saveNewIncomingMessage, parseMessageText } from "../message-sending"; import { getMessageServiceConfig, getConfigKey } from "../service_map"; import { symmetricDecrypt, @@ -30,7 +30,7 @@ import { const MAX_SEND_ATTEMPTS = 5; const MESSAGE_VALIDITY_PADDING_SECONDS = 30; const MAX_TWILIO_MESSAGE_VALIDITY = 14400; -const DISABLE_DB_LOG = getConfig("DISABLE_DB_LOG"); +const ENABLE_DB_LOG = getConfig("ENABLE_DB_LOG"); const TWILIO_SKIP_VALIDATION = getConfig("TWILIO_SKIP_VALIDATION"); const BULK_REQUEST_CONCURRENCY = 5; const MAX_NUMBERS_PER_BUY_JOB = getConfig("MAX_NUMBERS_PER_BUY_JOB") || 100; @@ -95,18 +95,17 @@ export const errorDescriptions = { 30005: "Unknown destination handset", 30006: "Landline or unreachable carrier", 30007: "Message Delivery - Carrier violation", - 30008: "Message Delivery - Unknown error", - "-1": "Spoke failed to send the message, usually due to a temporary issue.", - "-2": "Spoke failed to send the message and will try again.", - "-3": "Spoke failed to send the message and will try again.", - "-4": "Spoke failed to send the message and will try again.", - "-5": "Spoke failed to send the message and will NOT try again.", - "-133": "Auto-optout (no error)", - "-166": - "Internal: Message blocked due to text match trigger (profanity-tagger)", - "-167": "Internal: Initial message altered (initialtext-guard)" + 30008: "Message Delivery - Unknown error" }; +export function errorDescription(errorCode) { + return { + code: errorCode, + description: errorDescriptions[errorCode] || "Twilio error", + link: `https://www.twilio.com/docs/api/errors/${errorCode}` + }; +} + export function addServerEndpoints(addPostRoute) { addPostRoute( "/twilio/:orgId?", @@ -195,21 +194,6 @@ async function convertMessagePartsToMessage(messageParts) { }); } -const mediaExtractor = new RegExp(/\[\s*(http[^\]\s]*)\s*\]/); - -function parseMessageText(message) { - const text = message.text || ""; - const params = { - body: text.replace(mediaExtractor, "") - }; - // Image extraction - const results = text.match(mediaExtractor); - if (results) { - params.mediaUrl = results[1]; - } - return params; -} - async function getMessagingServiceSid( organization, contact, @@ -255,13 +239,13 @@ async function getOrganizationContactUserNumber(organization, contactNumber) { return null; } -export async function sendMessage( +export async function sendMessage({ message, contact, trx, organization, campaign -) { +}) { const twilio = await exports.getTwilio(organization); const APITEST = /twilioapitest/.test(message.text); if (!twilio && !APITEST) { @@ -512,7 +496,7 @@ export async function handleDeliveryReport(report) { return; } - if (!DISABLE_DB_LOG) { + if (ENABLE_DB_LOG) { await Log.save({ message_sid: report.MessageSid, body: JSON.stringify(report), @@ -580,7 +564,7 @@ export async function handleIncomingMessage(message) { } // store mediaurl data in Log, so it can be extracted manually - if (message.MediaUrl0 && (!DISABLE_DB_LOG || getConfig("LOG_MEDIA_URL"))) { + if (ENABLE_DB_LOG) { await Log.save({ message_sid: MessageSid, body: JSON.stringify(message), diff --git a/src/server/api/campaign.js b/src/server/api/campaign.js index e10dcac2f..0dba2f061 100644 --- a/src/server/api/campaign.js +++ b/src/server/api/campaign.js @@ -1,8 +1,10 @@ import { accessRequired } from "./errors"; import { mapFieldsToModel, mapFieldsOrNull } from "./lib/utils"; -import twilio, { - errorDescriptions -} from "../../extensions/service-vendors/twilio"; +import { + serviceMap, + getServiceNameFromOrganization, + errorDescription +} from "../../extensions/service-vendors"; import { Campaign, JobRequest, r, cacheableData } from "../models"; import { getUsers } from "./user"; import { getSideboxChoices } from "./organization"; @@ -253,15 +255,13 @@ export const resolvers = { .groupBy("error_code") .orderByRaw("count(*) DESC"); const organization = loaders.organization.load(campaign.organization_id); - const isTwilio = getConfig("DEFAULT_SERVICE", organization) === "twilio"; return errorCounts.map(e => ({ + ...errorDescription( + e.error_code, + getServiceNameFromOrganization(organization) + ), code: String(e.error_code), - count: e.error_count, - description: errorDescriptions[e.error_code] || null, - link: - e.error_code > 0 && isTwilio - ? `https://www.twilio.com/docs/api/errors/${e.error_code}` - : null + count: e.error_count })); } }, @@ -684,7 +684,6 @@ export const resolvers = { } return ""; }, - // TODO: rename to messagingServicePhoneNumbers phoneNumbers: async (campaign, _, { user }) => { await accessRequired( user, @@ -692,7 +691,7 @@ export const resolvers = { "SUPERVOLUNTEER", true ); - const phoneNumbers = await twilio.getPhoneNumbersForService( + const phoneNumbers = await serviceMap.twilio.getPhoneNumbersForService( campaign.organization, campaign.messageservice_sid ); diff --git a/src/workers/jobs.js b/src/workers/jobs.js index 8773322cf..b37e1bd20 100644 --- a/src/workers/jobs.js +++ b/src/workers/jobs.js @@ -972,27 +972,26 @@ export async function sendMessages(queryFunc, defaultStatus) { `Sending (${message.service}): ${message.user_number} -> ${message.contact_number}\nMessage: ${message.text}` ); try { - await service.sendMessage( + await service.sendMessage({ message, - { - // reconstruct contact + contact: { id: message.campaign_contact_id, message_status: message.message_status, campaign_id: message.campaign_id }, trx, - { - // organization + organization: { + // TODO: probably not enough -- need a organization.load() id: message.organization_id, features: message.features }, - { - // campaign + campaign: { + // TODO: probably not enough -- need a organization.load() id: message.campaign_id, organization_id: message.organization_id, messageservice_sid: message.messageservice_sid } - ); + }); pastMessages.push(message.id); pastMessages = pastMessages.slice(-100); // keep the last 100 } catch (err) { diff --git a/src/workers/tasks.js b/src/workers/tasks.js index 99e6fd083..8b164f4bb 100644 --- a/src/workers/tasks.js +++ b/src/workers/tasks.js @@ -24,7 +24,7 @@ const sendMessage = async ({ throw new Error(`Failed to find service for message ${message}`); } - await service.sendMessage(message, contact, trx, organization, campaign); + await service.sendMessage({ message, contact, trx, organization, campaign }); }; const questionResponseActionHandler = async ({ From a5a5e380dcc1ae7095ffdfa533cb4acfa9c3001a Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Sun, 2 May 2021 16:09:35 -0400 Subject: [PATCH 075/191] secret-manager extensions and support in Twilio --- .../updateServiceVendorConfig.test.js | 26 +++++--- .../secret-manager/default-encrypt/index.js | 13 ++++ src/extensions/secret-manager/index.js | 37 +++++++++++ .../service-vendors/twilio/index.js | 61 ++++++++++++------- src/server/api/lib/config.js | 1 + .../mutations/updateServiceVendorConfig.js | 8 ++- 6 files changed, 114 insertions(+), 32 deletions(-) create mode 100644 src/extensions/secret-manager/default-encrypt/index.js create mode 100644 src/extensions/secret-manager/index.js diff --git a/__test__/server/api/mutations/updateServiceVendorConfig.test.js b/__test__/server/api/mutations/updateServiceVendorConfig.test.js index d1e7ef160..7a69c43e5 100644 --- a/__test__/server/api/mutations/updateServiceVendorConfig.test.js +++ b/__test__/server/api/mutations/updateServiceVendorConfig.test.js @@ -191,15 +191,19 @@ describe("updateServiceVendorConfig", () => { }; }); describe("when features DOES NOT HAVE an existing config for the message service", () => { - it("writes message service config in features.configKey", async () => { + it("writes message service config in features.configKey no existing config", async () => { const gqlResult = await runGql( updateServiceVendorConfigGql, vars, user ); - expect(twilio.updateConfig.mock.calls).toEqual([ - [undefined, newConfig] + expect(twilio.updateConfig.mock.calls[0].slice(0, 2)).toEqual([ + undefined, + newConfig ]); + expect(twilio.updateConfig.mock.calls[0][2].id).toEqual( + Number(organization.id) + ); sharedExpectations(gqlResult, expectedFeatures); }); @@ -220,9 +224,13 @@ describe("updateServiceVendorConfig", () => { vars, user ); - expect(twilio.updateConfig.mock.calls).toEqual([ - ["it doesn't matter", newConfig] + expect(twilio.updateConfig.mock.calls[0].slice(0, 2)).toEqual([ + "it doesn't matter", + newConfig ]); + expect(twilio.updateConfig.mock.calls[0][2].id).toEqual( + Number(organization.id) + ); sharedExpectations(gqlResult, expectedFeatures); }); @@ -246,9 +254,13 @@ describe("updateServiceVendorConfig", () => { vars, user ); - expect(twilio.updateConfig.mock.calls).toEqual([ - [undefined, newConfig] + expect(twilio.updateConfig.mock.calls[0].slice(0, 2)).toEqual([ + undefined, + newConfig ]); + expect(twilio.updateConfig.mock.calls[0][2].id).toEqual( + Number(organization.id) + ); sharedExpectations(gqlResult, { service, ...expectedConfig }); }); diff --git a/src/extensions/secret-manager/default-encrypt/index.js b/src/extensions/secret-manager/default-encrypt/index.js new file mode 100644 index 000000000..ad1684cb0 --- /dev/null +++ b/src/extensions/secret-manager/default-encrypt/index.js @@ -0,0 +1,13 @@ +import { + symmetricDecrypt, + symmetricEncrypt +} from "../../../server/api/lib/crypto"; + +export async function getSecret(name, token, organization) { + return symmetricDecrypt(token); +} + +export async function convertSecret(name, organization, secretValue) { + // returns token, which the caller is still responsible for saving somewhere + symmetricEncrypt(secretValue); +} diff --git a/src/extensions/secret-manager/index.js b/src/extensions/secret-manager/index.js new file mode 100644 index 000000000..3ca711bbc --- /dev/null +++ b/src/extensions/secret-manager/index.js @@ -0,0 +1,37 @@ +import { symmetricDecrypt } from "../../server/api/lib/crypto"; + +const SECRET_MANAGER_NAME = + process.env.SECRET_MANAGER || global.SECRET_MANAGER || "default-encrypt"; +const SECRET_MANAGER_COMPONENT = getSetup(SECRET_MANAGER_NAME); + +function getSetup(name) { + try { + const c = require(`./${name}/index.js`); + return c; + } catch (err) { + console.error("SECRET_MANAGER failed to load", name, err); + } +} + +export async function getSecret(name, token, organization) { + if (token.startsWith(SECRET_MANAGER_NAME)) { + return await SECRET_MANAGER_COMPONENT.getSecret( + name, + token.slice(SECRET_MANAGER_NAME.length + 1), + organization + ); + } else { + // legacy fallback + return symmetricDecrypt(token); + } +} + +export async function convertSecret(name, organization, secretValue) { + // returns token, which the caller is still responsible for saving somewhere + const token = await SECRET_MANAGER_COMPONENT.convertSecret( + name, + organization, + secretValue + ); + return `${SECRET_MANAGER_NAME}|${token}`; +} diff --git a/src/extensions/service-vendors/twilio/index.js b/src/extensions/service-vendors/twilio/index.js index 3c182186f..03bf69662 100644 --- a/src/extensions/service-vendors/twilio/index.js +++ b/src/extensions/service-vendors/twilio/index.js @@ -4,7 +4,12 @@ import * as twilioLibrary from "twilio"; import urlJoin from "url-join"; import { log } from "../../../lib"; import { getFormattedPhoneNumber } from "../../../lib/phone-format"; -import { getConfig, hasConfig } from "../../../server/api/lib/config"; +import { + getConfig, + hasConfig, + getSecret, + convertSecret +} from "../../../server/api/lib/config"; import { cacheableData, Log, @@ -15,10 +20,6 @@ import { import wrap from "../../../server/wrap"; import { saveNewIncomingMessage, parseMessageText } from "../message-sending"; import { getMessageServiceConfig, getConfigKey } from "../service_map"; -import { - symmetricDecrypt, - symmetricEncrypt -} from "../../../server/api/lib/crypto"; // TWILIO error_codes: // > 1 (i.e. positive) error_codes are reserved for Twilio error codes @@ -244,7 +245,8 @@ export async function sendMessage({ contact, trx, organization, - campaign + campaign, + serviceManagerData }) { const twilio = await exports.getTwilio(organization); const APITEST = /twilioapitest/.test(message.text); @@ -267,15 +269,14 @@ export async function sendMessage({ } // Note organization won't always be available, so then contact can trace to it - const messagingServiceSid = await getMessagingServiceSid( - organization, - contact, - message, - campaign - ); - - let userNumber; - if (process.env.EXPERIMENTAL_STICKY_SENDER) { + const messagingServiceSid = + (serviceManagerData && serviceManagerData.messageservice_sid) || + (await getMessagingServiceSid(organization, contact, message, campaign)); + + let userNumber = + (serviceManagerData && serviceManagerData.user_number) || + message.user_number; + if (process.env.EXPERIMENTAL_STICKY_SENDER && !userNumber) { userNumber = await getOrganizationContactUserNumber( organization, contact.cell @@ -319,10 +320,13 @@ export async function sendMessage({ } const changes = {}; - if (userNumber) { + if (userNumber && message.user_number != userNumber) { changes.user_number = userNumber; } - if (messagingServiceSid) { + if ( + messagingServiceSid && + message.messageservice_sid != messagingServiceSid + ) { changes.messageservice_sid = messagingServiceSid; } @@ -860,7 +864,11 @@ export const getServiceConfig = async ( if (hasEncryptedToken) { authToken = obscureSensitiveInformation ? "" - : symmetricDecrypt(serviceConfig.TWILIO_AUTH_TOKEN_ENCRYPTED); + : await getSecret( + "TWILIO_AUTH_TOKEN_ENCRYPTED", + serviceConfig.TWILIO_AUTH_TOKEN_ENCRYPTED, + organization + ); } else { authToken = obscureSensitiveInformation ? "" @@ -887,12 +895,14 @@ export const getServiceConfig = async ( if (hasEncryptedToken) { authToken = obscureSensitiveInformation ? "" - : symmetricDecrypt( + : await getSecret( + "TWILIO_AUTH_TOKEN_ENCRYPTED", getConfig( "TWILIO_AUTH_TOKEN_ENCRYPTED", organization, getConfigOptions - ) + ), + organization ); } else { const hasUnencryptedToken = hasConfig( @@ -939,7 +949,7 @@ export const getMessageServiceSid = async ( return messageServiceSid; }; -export const updateConfig = async (oldConfig, config) => { +export const updateConfig = async (oldConfig, config, organization) => { const { twilioAccountSid, twilioAuthToken, twilioMessageServiceSid } = config; if (!twilioAccountSid || !twilioMessageServiceSid) { throw new Error( @@ -953,9 +963,14 @@ export const updateConfig = async (oldConfig, config) => { // TODO(lperson) is twilioAuthToken required? -- not for unencrypted newConfig.TWILIO_AUTH_TOKEN_ENCRYPTED = twilioAuthToken - ? symmetricEncrypt(twilioAuthToken).substr(0, 256) + ? await convertSecret( + "TWILIO_AUTH_TOKEN_ENCRYPTED", + organization, + twilioAuthToken + ) : twilioAuthToken; - newConfig.TWILIO_MESSAGE_SERVICE_SID = twilioMessageServiceSid.substr(0, 64); + newConfig.TWILIO_MESSAGE_SERVICE_SID = + twilioMessageServiceSid && twilioMessageServiceSid.substr(0, 64); try { if (twilioAuthToken && global.TEST_ENVIRONMENT !== "1") { diff --git a/src/server/api/lib/config.js b/src/server/api/lib/config.js index b9f80b988..a5574d970 100644 --- a/src/server/api/lib/config.js +++ b/src/server/api/lib/config.js @@ -1,4 +1,5 @@ import fs from "fs"; +export { getSecret, convertSecret } from "../../../extensions/secret-manager"; // This is for centrally loading config from different environment sources // Especially for large config values (or many) some environments (like AWS Lambda) limit diff --git a/src/server/api/mutations/updateServiceVendorConfig.js b/src/server/api/mutations/updateServiceVendorConfig.js index c2e146c6f..151faec26 100644 --- a/src/server/api/mutations/updateServiceVendorConfig.js +++ b/src/server/api/mutations/updateServiceVendorConfig.js @@ -54,7 +54,11 @@ export const updateServiceVendorConfig = async ( let newConfig; try { - newConfig = await serviceConfigFunction(existingConfig, configObject); + newConfig = await serviceConfigFunction( + existingConfig, + configObject, + organization + ); } catch (caught) { // eslint-disable-next-line no-console console.error( @@ -64,7 +68,7 @@ export const updateServiceVendorConfig = async ( ); throw new GraphQLError(caught.message); } - + // TODO: put this into a transaction (so read of features record doesn't get clobbered) const dbOrganization = await Organization.get(organizationId); const features = JSON.parse(dbOrganization.features || "{}"); const hadMessageServiceConfig = !!features[configKey]; From e41b2d20c6860097f69e4ff0930c9266e9b3aef8 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Tue, 4 May 2021 09:56:08 -0400 Subject: [PATCH 076/191] clean some import bugs: secrets cannot be in api/lib/config because that is loaded by the client which cannot have the SESSION_SECRET --- .../lib => extensions/secret-manager}/crypto.test.js | 4 ++-- __test__/extensions/service-vendors/twilio.test.js | 4 ++-- __test__/server/api/organization.test.js | 4 +--- src/api/message-service.js | 1 - src/containers/Settings.jsx | 1 - .../contact-loaders/test-fakedata/react-component.js | 1 - .../api/lib => extensions/secret-manager}/crypto.js | 0 .../secret-manager/default-encrypt/index.js | 7 ++----- src/extensions/secret-manager/index.js | 2 +- src/extensions/service-vendors/twilio/index.js | 11 ++++------- .../service-vendors/twilio/react-component.js | 8 ++++---- src/server/api/lib/config.js | 1 - src/server/models/cacheable_queries/organization.js | 3 ++- 13 files changed, 18 insertions(+), 29 deletions(-) rename __test__/{server/api/lib => extensions/secret-manager}/crypto.test.js (84%) rename src/{server/api/lib => extensions/secret-manager}/crypto.js (100%) diff --git a/__test__/server/api/lib/crypto.test.js b/__test__/extensions/secret-manager/crypto.test.js similarity index 84% rename from __test__/server/api/lib/crypto.test.js rename to __test__/extensions/secret-manager/crypto.test.js index 3bd244afd..f8e62f3a1 100644 --- a/__test__/server/api/lib/crypto.test.js +++ b/__test__/extensions/secret-manager/crypto.test.js @@ -1,4 +1,4 @@ -import crypto from "../../../../src/server/api/lib/crypto"; +import crypto from "../../../src/extensions/secret-manager/crypto"; beforeEach(() => { jest.resetModules(); @@ -20,7 +20,7 @@ it("decrypted value should match original", () => { it("session secret must exist", () => { function encrypt() { delete global.SESSION_SECRET; - const crypto2 = require("../../../../src/server/api/lib/crypto"); + const crypto2 = require("../../../src/extensions/secret-manager/crypto"); const plaintext = "foo"; const encrypted = crypto2.symmetricEncrypt(plaintext); } diff --git a/__test__/extensions/service-vendors/twilio.test.js b/__test__/extensions/service-vendors/twilio.test.js index 58bf6fe15..1da3fae95 100644 --- a/__test__/extensions/service-vendors/twilio.test.js +++ b/__test__/extensions/service-vendors/twilio.test.js @@ -4,7 +4,7 @@ import { getLastMessage } from "../../../src/extensions/service-vendors/message- import * as twilio from "../../../src/extensions/service-vendors/twilio"; import { getConfig } from "../../../src/server/api/lib/config"; // eslint-disable-line no-duplicate-imports, import/no-duplicates import * as configFunctions from "../../../src/server/api/lib/config"; // eslint-disable-line no-duplicate-imports, import/no-duplicates -import crypto from "../../../src/server/api/lib/crypto"; +import crypto from "../../../src/extensions/secret-manager/crypto"; import { cacheableData, Message, r } from "../../../src/server/models/"; import { erroredMessageSender } from "../../../src/workers/job-processes"; import { @@ -966,7 +966,7 @@ describe("config functions", () => { expectedConfig = { TWILIO_ACCOUNT_SID: fakeAccountSid, - TWILIO_AUTH_TOKEN_ENCRYPTED: encryptedFakeAuthToken, + TWILIO_AUTH_TOKEN_ENCRYPTED: `default-encrypt|${encryptedFakeAuthToken}`, TWILIO_MESSAGE_SERVICE_SID: fakeMessageServiceSid }; diff --git a/__test__/server/api/organization.test.js b/__test__/server/api/organization.test.js index d2b0210c3..196763dcb 100644 --- a/__test__/server/api/organization.test.js +++ b/__test__/server/api/organization.test.js @@ -332,8 +332,7 @@ describe("organization", async () => { fakeConfig = { fake: "faker_and_faker" }; fakeMetadata = { name: "super_fake", - supportsOrgConfig: true, - supportsCampaignConfig: false + supportsOrgConfig: true }; jest @@ -355,7 +354,6 @@ describe("organization", async () => { messageService { name supportsOrgConfig - supportsCampaignConfig config } } diff --git a/src/api/message-service.js b/src/api/message-service.js index 30efd9be8..a563da5c4 100644 --- a/src/api/message-service.js +++ b/src/api/message-service.js @@ -5,6 +5,5 @@ export const schema = gql` name: String! config: JSON supportsOrgConfig: Boolean! - supportsCampaignConfig: Boolean! } `; diff --git a/src/containers/Settings.jsx b/src/containers/Settings.jsx index 73ae5ed37..026a85a1f 100644 --- a/src/containers/Settings.jsx +++ b/src/containers/Settings.jsx @@ -380,7 +380,6 @@ const queries = { messageService { name supportsOrgConfig - supportsCampaignConfig config } } diff --git a/src/extensions/contact-loaders/test-fakedata/react-component.js b/src/extensions/contact-loaders/test-fakedata/react-component.js index bf310ae08..4fe41f2b3 100644 --- a/src/extensions/contact-loaders/test-fakedata/react-component.js +++ b/src/extensions/contact-loaders/test-fakedata/react-component.js @@ -51,7 +51,6 @@ export class CampaignContactsForm extends React.Component { diff --git a/src/server/api/lib/crypto.js b/src/extensions/secret-manager/crypto.js similarity index 100% rename from src/server/api/lib/crypto.js rename to src/extensions/secret-manager/crypto.js diff --git a/src/extensions/secret-manager/default-encrypt/index.js b/src/extensions/secret-manager/default-encrypt/index.js index ad1684cb0..a2a95073c 100644 --- a/src/extensions/secret-manager/default-encrypt/index.js +++ b/src/extensions/secret-manager/default-encrypt/index.js @@ -1,7 +1,4 @@ -import { - symmetricDecrypt, - symmetricEncrypt -} from "../../../server/api/lib/crypto"; +import { symmetricDecrypt, symmetricEncrypt } from "../crypto"; export async function getSecret(name, token, organization) { return symmetricDecrypt(token); @@ -9,5 +6,5 @@ export async function getSecret(name, token, organization) { export async function convertSecret(name, organization, secretValue) { // returns token, which the caller is still responsible for saving somewhere - symmetricEncrypt(secretValue); + return symmetricEncrypt(secretValue); } diff --git a/src/extensions/secret-manager/index.js b/src/extensions/secret-manager/index.js index 3ca711bbc..d254fb69b 100644 --- a/src/extensions/secret-manager/index.js +++ b/src/extensions/secret-manager/index.js @@ -1,4 +1,4 @@ -import { symmetricDecrypt } from "../../server/api/lib/crypto"; +import { symmetricDecrypt } from "./crypto"; const SECRET_MANAGER_NAME = process.env.SECRET_MANAGER || global.SECRET_MANAGER || "default-encrypt"; diff --git a/src/extensions/service-vendors/twilio/index.js b/src/extensions/service-vendors/twilio/index.js index 03bf69662..1a58c1535 100644 --- a/src/extensions/service-vendors/twilio/index.js +++ b/src/extensions/service-vendors/twilio/index.js @@ -4,12 +4,7 @@ import * as twilioLibrary from "twilio"; import urlJoin from "url-join"; import { log } from "../../../lib"; import { getFormattedPhoneNumber } from "../../../lib/phone-format"; -import { - getConfig, - hasConfig, - getSecret, - convertSecret -} from "../../../server/api/lib/config"; +import { getConfig, hasConfig } from "../../../server/api/lib/config"; import { cacheableData, Log, @@ -18,6 +13,8 @@ import { r } from "../../../server/models"; import wrap from "../../../server/wrap"; +import { getSecret, convertSecret } from "../../secret-manager"; + import { saveNewIncomingMessage, parseMessageText } from "../message-sending"; import { getMessageServiceConfig, getConfigKey } from "../service_map"; @@ -1014,7 +1011,7 @@ export const manualMessagingServicesEnabled = organization => ); export const fullyConfigured = async organization => { - const { authToken, accountSid } = await exports.getServiceConfig( + const { authToken, accountSid } = await getMessageServiceConfig( "twilio", organization ); diff --git a/src/extensions/service-vendors/twilio/react-component.js b/src/extensions/service-vendors/twilio/react-component.js index 01c30ab49..5a8c91a7b 100644 --- a/src/extensions/service-vendors/twilio/react-component.js +++ b/src/extensions/service-vendors/twilio/react-component.js @@ -8,10 +8,10 @@ import PropTypes from "prop-types"; import React from "react"; import Form from "react-formal"; import * as yup from "yup"; -import DisplayLink from "../../../../components/DisplayLink"; -import GSForm from "../../../../components/forms/GSForm"; -import GSTextField from "../../../../components/forms/GSTextField"; -import GSSubmitButton from "../../../../components/forms/GSSubmitButton"; +import DisplayLink from "../../../components/DisplayLink"; +import GSForm from "../../../components/forms/GSForm"; +import GSTextField from "../../../components/forms/GSTextField"; +import GSSubmitButton from "../../../components/forms/GSSubmitButton"; export class OrgConfig extends React.Component { constructor(props) { diff --git a/src/server/api/lib/config.js b/src/server/api/lib/config.js index a5574d970..b9f80b988 100644 --- a/src/server/api/lib/config.js +++ b/src/server/api/lib/config.js @@ -1,5 +1,4 @@ import fs from "fs"; -export { getSecret, convertSecret } from "../../../extensions/secret-manager"; // This is for centrally loading config from different environment sources // Especially for large config values (or many) some environments (like AWS Lambda) limit diff --git a/src/server/models/cacheable_queries/organization.js b/src/server/models/cacheable_queries/organization.js index 2ec00e2c2..5a674baed 100644 --- a/src/server/models/cacheable_queries/organization.js +++ b/src/server/models/cacheable_queries/organization.js @@ -8,7 +8,8 @@ import { r } from "../../models"; const cacheKey = orgId => `${process.env.CACHE_PREFIX || ""}org-${orgId}`; const getOrganizationMessageService = organization => - getConfig("service", organization) || getConfig("DEFAULT_SERVICE"); + getConfig("service", organization) || + getConfig("DEFAULT_SERVICE", organization); const organizationCache = { clear: async id => { From 31d9a5eba1fe883e4a1d27671eac4721043f8000 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Tue, 4 May 2021 09:59:05 -0400 Subject: [PATCH 077/191] draft untested service-vendor/bandwidth implementation --- .../service-vendors/bandwidth/index.js | 96 +++++ .../service-vendors/bandwidth/messaging.js | 222 ++++++++++++ .../bandwidth/react-component.js | 339 ++++++++++++++++++ .../bandwidth/setup-and-numbers.js | 294 +++++++++++++++ src/extensions/service-vendors/service_map.js | 2 + 5 files changed, 953 insertions(+) create mode 100644 src/extensions/service-vendors/bandwidth/index.js create mode 100644 src/extensions/service-vendors/bandwidth/messaging.js create mode 100644 src/extensions/service-vendors/bandwidth/react-component.js create mode 100644 src/extensions/service-vendors/bandwidth/setup-and-numbers.js diff --git a/src/extensions/service-vendors/bandwidth/index.js b/src/extensions/service-vendors/bandwidth/index.js new file mode 100644 index 000000000..916577b18 --- /dev/null +++ b/src/extensions/service-vendors/bandwidth/index.js @@ -0,0 +1,96 @@ +import AuthHasher from "passport-local-authenticate"; +import wrap from "../../../server/wrap"; +import { log } from "../../../lib"; +import { getConfig } from "../../../server/api/lib/config"; + +export { + getServiceConfig, + buyNumbersInAreaCode, + deleteNumbersInAreaCode, + createMessagingService, // Bandwidth calls these 'applications' + deleteMessagingService, + updateConfig, + fullyConfigured +} from "./setup-and-numbers"; + +import { + sendMessage, + handleIncomingMessage, + handleDeliveryReport, + errorDescription +} from "./messaging"; + +export { + sendMessage, + handleIncomingMessage, + handleDeliveryReport, + errorDescription +}; + +export const getMetadata = () => ({ + supportsOrgConfig: !getConfig("SERVICE_VENDOR_NO_ORGCONFIG", null, { + truthy: true + }), + name: "bandwidth" +}); + +const webhooks = { + "message-delivered": handleDeliveryReport, + "message-failed": handleDeliveryReport, + "message-received": handleIncomingMessage, + "message-sending": () => Promise.resolve() // ignore MMS intermediate state +}; + +function verifyBandwidthServer(hashPassword) { + return new Promise((resolve, reject) => { + // maybe create an hmac + const [salt, hash] = hashPassword.split(":"); + AuthHasher.verify( + `${getConfig("SESSION_SECRET")}:bandwidth.com`, + { salt, hash }, + { keylen: 64 }, + (err, verified) => resolve(verified && !err) + ); + }); +} + +export function addServerEndpoints(addPostRoute) { + // https://dev.bandwidth.com/messaging/callbacks/messageEvents.html + // Bandwidth has a 10 second timeout!! + addPostRoute( + "/bandwidth/:orgId?", + wrap(async (req, res) => { + // parse login and password from headers + const b64auth = (req.headers.authorization || "").split(" ")[1] || ""; + const [login, password] = Buffer.from(b64auth, "base64") + .toString() + .split(":"); + + // TODO: better login/password auto-creation/context + // await verifyBandwidthServer(password) + if (login !== "bandwidth.com" || password !== "testtest") { + res.set("WWW-Authenticate", 'Basic realm="401"'); + res.status(401).send("Authentication required."); + return; + } + + // req.body is JSON + if (req.body.length && req.body[0].type) { + for (let i = 0, l = req.body.length; i < l; i++) { + const payload = req.body[i]; + if (webhooks[payload.type]) { + try { + // FUTURE: turn into tasks to avoid timeout issues + await webhooks[payload.type](payload, req.params); + } catch (err) { + log.error(err); + } + } + } + } + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end('{"success": true}'); + }) + ); +} diff --git a/src/extensions/service-vendors/bandwidth/messaging.js b/src/extensions/service-vendors/bandwidth/messaging.js new file mode 100644 index 000000000..13d4c7472 --- /dev/null +++ b/src/extensions/service-vendors/bandwidth/messaging.js @@ -0,0 +1,222 @@ +import BandwidthMessaging from "@bandwidth/messaging"; + +import { log } from "../../../lib"; +import { getFormattedPhoneNumber } from "../../../lib/phone-format"; +import { getConfig, hasConfig } from "../../../server/api/lib/config"; +import { cacheableData, Log, Message } from "../../../server/models"; +import { saveNewIncomingMessage, parseMessageText } from "../message-sending"; +import { getMessageServiceConfig, getConfigKey } from "../service_map"; + +const ENABLE_DB_LOG = getConfig("ENABLE_DB_LOG"); + +// https://dev.bandwidth.com/messaging/errors/codes.html +const errorDescriptions = { + 4001: "Service not allowed (Catch-all error)", + 4702: "Contact is unavailable or unreachable", + 4720: "Invalid destination (possibly a landline)", + 4721: "Contact's carrier service deactivated", + 4750: "Carrier rejected message", + 4770: "Carrier rejected as SPAM", + 4775: "Carrier rejected due to user opt-out", + 4780: "Carrier rejected due to P2P volumetric violation", + 5600: "Carrier Service Unavailable", + 5610: "Carrier Service Failure", + 5630: "No response or ack received from the Carrier", + 5650: "Carrier Service reports a failure to send to destination", + 9902: "Timed out waiting for delivery receipt. (Reason unknown)" +}; + +export function errorDescription(errorCode) { + return { + code: errorCode, + description: errorDescriptions[errorCode] || "Bandwidth error", + link: `https://dev.bandwidth.com/messaging/errors/codes.html` + }; +} + +export async function getBandwidthController(organization, config) { + const password = await convertSecret( + "bandwidthPassword", + organization, + config.password + ); + const client = new BandwidthMessaging.Client({ + timeout: 0, + basicAuthUserName: config.userName, + basicAuthPassword: password + }); + return new BandwidthMessaging.ApiController(client); +} + +export async function sendMessage({ + message, + contact, + trx, + organization, + campaign, + serviceManagerData +}) { + const config = await getMessageServiceConfig( + "bandwidth", + organization, + serviceManagerData + ); + // applicationId will probably come from config, unless serviceManager is doing something fancy + const applicationId = + (serviceManagerData && serviceManagerData.messageservice_sid) || + message.messageservice_sid || + config.applicationId; + // userNumber will probably come from serviceManagerData; config is unlikely to have userNumber + const userNumber = + (serviceManagerData && serviceManagerData.user_number) || + message.user_number || + config.userNumber; + + const changes = {}; + if (applicationId && applicationId != message.messageservice_sid) { + changes.messageservice_sid = applicationId; + } + if (userNumber && userNumber != message.user_number) { + changes.user_number = userNumber; + } + + const parsedMessage = parseMessageText(message); + const tag = `${(organization && organization.id) || ""}|${(contact && + contact.campaign_id) || + ""}|${(contact && contact.id) || ""}`; + const bandwidthMessage = { + applicationId, + to: [message.contact_number], + from: userNumber, + text: parsedMessage.body, + tag + }; + if (parsedMessage.mediaUrl) { + bandwidthMessage.media = [parsedMessage.mediaUrl]; + } + + let response; + if (/bandwidthapitest/.test(message.text)) { + let err; + const response = { + // TODO FAKE DATA + response: {}, + statusCode: 202, //accepted + headers: {} + }; + await postMessageSend({ + response, + err, + message, + contact, + trx, + organization, + changes + }); + return; + } + try { + const messagingController = await getBandwidthController( + organization, + config + ); + const response = await messagingController.createMessage( + config.accountId, + bandwidthMessage + ); + } catch (err) { + await postMessageSend({ + err, + message, + contact, + trx, + organization, + changes + }); + return; + } + await postMessageSend({ + response, + message, + contact, + trx, + organization, + changes + }); +} + +export async function postMessageSend({ + message, + contact, + trx, + err, + response, + organization, + changes +}) { + let changesToSave = changes + ? { + ...changes + } + : {}; +} + +export async function handleIncomingMessage(message, { orgId }) { + // https://dev.bandwidth.com/messaging/callbacks/incomingSingle.html + if ( + !message.hasOwnProperty("message") || + !message.hasOwnProperty("to") || + message.type !== "message-received" + ) { + log.error(`This is not an incoming message: ${JSON.stringify(message)}`); + return; + } + const { id, from, text, applicationId, media } = message.message; + const contactNumber = getFormattedPhoneNumber(from); + const userNumber = message.to ? getFormattedPhoneNumber(message.to) : ""; + const finalMessage = new Message({ + contact_number: contactNumber, + user_number: userNumber, + is_from_contact: true, + text, + media: media + ? media.map(m => ({ url: m, type: `image/${m.split(".").pop()}` })) + : null, + error_code: null, + service_id: id, + messageservice_sid: applicationId, + service: "bandwidth", + send_status: "DELIVERED", + user_id: null + }); + await saveNewIncomingMessage(finalMessage); + + if (ENABLE_DB_LOG) { + await Log.save({ + message_sid: id, + body: JSON.stringify(message), + error_code: -101, + from_num: from || null, + to_num: message.to || null + }); + } +} + +export async function handleDeliveryReport(report, { orgId }) { + // https://dev.bandwidth.com/messaging/callbacks/msgDelivered.html + // https://dev.bandwidth.com/messaging/callbacks/messageFailed.html + + const { id, from, applicationId, tag } = report.message; + const contactNumber = getFormattedPhoneNumber(report.to); + const userNumber = from ? getFormattedPhoneNumber(from) : ""; + // FUTURE: tag should have: "||" + await cacheableData.message.deliveryReport({ + contactNumber, + userNumber, + messageSid: id, + service: "bandwidth", + messageServiceSid: applicationId, + newStatus: report.type === "message-failed" ? "ERROR" : "DELIVERED", + errorCode: Number(report.errorCode || 0) || 0 + }); +} diff --git a/src/extensions/service-vendors/bandwidth/react-component.js b/src/extensions/service-vendors/bandwidth/react-component.js new file mode 100644 index 000000000..06ebc4443 --- /dev/null +++ b/src/extensions/service-vendors/bandwidth/react-component.js @@ -0,0 +1,339 @@ +/* eslint no-console: 0 */ +import { css } from "aphrodite"; +import { CardText, Card, CardHeader } from "material-ui/Card"; +import Dialog from "material-ui/Dialog"; +import FlatButton from "material-ui/FlatButton"; +import { Table, TableBody, TableRow, TableRowColumn } from "material-ui/Table"; +import PropTypes from "prop-types"; +import React from "react"; +import Form from "react-formal"; +import * as yup from "yup"; +import theme from "../../../styles/theme"; +import DisplayLink from "../../../components/DisplayLink"; +import GSForm from "../../../components/forms/GSForm"; +import GSTextField from "../../../components/forms/GSTextField"; +import GSSubmitButton from "../../../components/forms/GSSubmitButton"; + +export class OrgConfig extends React.Component { + constructor(props) { + super(props); + const { + userName, + password, + accountId, + siteId, + sipPeerId, + applicationId + } = this.props.config; + const allSet = + userName && password && accountId && siteId && sipPeerId && applicationId; + this.state = { allSet, ...this.props.config, country: "United States" }; + this.props.onAllSetChanged(allSet); + } + /* + componentDidUpdate(prevProps) { + const { + accountSid: prevAccountSid, + authToken: prevAuthToken, + messageServiceSid: prevMessageServiceSid + } = prevProps.config; + const prevAllSet = prevAccountSid && prevAuthToken && prevMessageServiceSid; + + const { accountSid, authToken, messageServiceSid } = this.props.config; + const allSet = accountSid && authToken && messageServiceSid; + + if (!!prevAllSet !== !!allSet) { + this.props.onAllSetChanged(allSet); + } + } + */ + onFormChange = value => { + this.setState(value); + }; + + handleOpenDialog = () => this.setState({ dialogOpen: true }); + + handleCloseDialog = () => this.setState({ dialogOpen: false }); + + handleSubmitAuthForm = async () => { + const { password, dialogOpen, error, ...config } = this.state; + if (password !== "") { + config.password = password; + } + let newError; + try { + await this.props.onSubmit(config); + await this.props.requestRefetch(); + this.setState({ + error: undefined + }); + } catch (caught) { + console.log("Error submitting Bandwidth settings", caught); + if (caught.graphQLErrors && caught.graphQLErrors.length > 0) { + const errors = caught.graphQLErrors.map(error => error.message); + newError = errors.join(","); + } else { + newError = caught.message; + } + this.setState({ error: newError }); + } + this.handleCloseDialog(); + }; + + render() { + const { organizationId, inlineStyles, styles, config } = this.props; + const { accountSid, authToken, messageServiceSid } = config; + const allSet = accountSid && authToken && messageServiceSid; + let baseUrl = "http://base"; + if (typeof window !== "undefined") { + baseUrl = window.location.origin; + } + const formSchema = yup.object({ + accountId: yup + .string() + .nullable() + .max(64), + userName: yup + .string() + .nullable() + .max(64), + password: yup + .string() + .nullable() + .max(64), + siteId: yup + .string() + .nullable() + .max(64), + sipPeerId: yup + .string() + .nullable() + .max(64), + applicationId: yup + .string() + .nullable() + .max(64), + houseNumber: yup.string().nullable(), + streetName: yup.string().nullable(), + city: yup.string().nullable(), + stateCode: yup.string().nullable(), + zip: yup.string().nullable(), + country: yup.string().nullable() + }); + + const dialogActions = [ + , + + ]; + + return ( +
+ {allSet && ( + + Settings for this organization: + + + + + Account ID + + {this.props.config.accountId} + + + + Username + + {this.props.config.userName} + + + + Password + + {this.props.config.password} + + {this.props.config.siteId ? ( + + + Application Info + + + Site (or Sub-account) id: {this.props.config.siteId} +
+ Location (or Sip-peer): {this.props.config.sipPeerId} +
+ Application Id: {this.props.config.applicationId} +
+
+ ) : null} + {this.props.config.streetName ? ( + + + Address + + + {this.props.config.houseNumber}{" "} + {this.props.config.streetName} +
+ {this.props.config.city}, {this.props.config.stateCode}{" "} + {this.props.config.zip} +
+ {this.props.config.country || ""} +
+
+ ) : null} +
+
+
+ )} + {this.state.error && ( + {this.state.error} + )} + +
+ + You can set Twilio API credentials specifically for this + Organization by entering them here. + + + + + +
+ {this.props.config.sipPeerId ? null : ( + + + +
+ In order to setup your Bandwidth account we need your + organization’s billing address. If you have + already created a subaccount (Bandwidth sometimes calls + this a 'Site') and a 'Location' (also called a + 'SipPeer'), then click Advanced and you can fill + in the information. Otherwise, just fill out the address + and we’ll set it all up for you. +
+ + + + + + +
+
+ )} + + + +

+ Anything not filled out, we will auto-create for you. If + you do not provide a Location Id, then you need to + fill in the address fields above. +

+ + + + +
+
+
+ + + Changing information here will break any campaigns that are + currently running. Do you want to contunue? + +
+
+
+
+ ); + } +} + +OrgConfig.propTypes = { + organizationId: PropTypes.string, + config: PropTypes.object, + inlineStyles: PropTypes.object, + styles: PropTypes.object, + saveLabel: PropTypes.string, + onSubmit: PropTypes.func, + onAllSetChanged: PropTypes.func, + requestRefetch: PropTypes.func +}; diff --git a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js new file mode 100644 index 000000000..0c08d40b2 --- /dev/null +++ b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js @@ -0,0 +1,294 @@ +import BandwidthNumbers from "@bandwidth/numbers"; +import BandwidthMessaging from "@bandwidth/messaging"; + +import { log } from "../../../lib"; +import { getFormattedPhoneNumber } from "../../../lib/phone-format"; +import { sleep } from "../../../workers/lib"; + +import { getConfig } from "../../../server/api/lib/config"; +import { getSecret, convertSecret } from "../../secret-manager"; +import { getMessageServiceConfig, getConfigKey } from "../service_map"; + +export async function getNumbersClient(organization, options) { + const config = + (options && options.serviceConfig) || + (await getMessageServiceConfig("bandwidth", organization)); + const password = await convertSecret( + "bandwidthPassword", + organization, + config.password + ); + const client = new BandwidthNumbers.Client({ + userName: config.userName, + password: password, + accountId: config.accountId + }); + return { client, config }; +} + +export const getServiceConfig = async ( + serviceConfig, + organization, + options = {} +) => { + // serviceConfig should have username, password, applicationId, and accountId + const { + restrictToOrgFeatures = false, + obscureSensitiveInformation = true + } = options; + let password; + if (serviceConfig) { + // Note, allows unencrypted auth tokens to be (manually) stored in the db + // @todo: decide if this is necessary, or if UI/envars is sufficient. + if (serviceConfig.password) { + password = obscureSensitiveInformation + ? "" + : await getSecret( + "bandwidthPassword", + organization, + serviceConfig.password + ); + } else { + password = obscureSensitiveInformation + ? "" + : serviceConfig.password; + } + } + // FUTURE: should it be possible for a universal setting? maybe + return { + ...serviceConfig, + password + }; +}; + +export async function fullyConfigured(organization) { + const config = await getMessageServiceConfig("bandwidth", organization); + if ( + !config.password || + !config.userName || + !config.accountId || + !config.applicationId + ) { + return false; + } + // TODO: also needs some number to send with + return true; +} + +export async function updateConfig(oldConfig, config, organization) { + // console.log('bandwidth.updateConfig', oldConfig, config, organization); + let changes = { ...config }; + if (config.password) { + changes.password = await convertSecret( + "bandwdithPassword", + organization, + config.password + ); + } + const finalConfig = { + ...oldConfig, + ...changes + }; + // console.log('bandwdith finalConfig', finalConfig); + + // TODO: test credentials with login + + try { + if ( + !config.siteId || + (!config.sipPeerId && + config.streetName && + config.city && + config.zip && + config.houseNumber && + config.stateCode) + ) { + const newAccountObjects = await createAccountBaseline(organization, { + serviceConfig: finalConfig + }); + Object.assign(finalConfig, newAccountObjects); + } + if (!config.applicationId) { + finalConfig.applicationId = await createMessagingService( + organization, + `Spoke app, org Id=${organization.id}`, + finalConfig + ); + } + delete finalConfig.autoConfigError; + } catch (err) { + console.log("bandwidth.updateConfig autoconfigure Error", err); + finalConfig.autoConfigError = "Auto-configuration failed"; + } + + return finalConfig; +} + +export async function buyNumbersInAreaCode( + organization, + areaCode, + limit, + opts +) { + let totalPurchased = 0; + const { client, config } = await getNumbersClient(organization); + let orderId; + if (areaCode === "800") { + // tollFree + const order = await BandwidthNumbers.Order.createAsync(client, { + name: `Spoke ${limit} order for org ${organization.id}`, + quantity: limit || 1, + siteId: config.siteId, + peerId: config.sipPeerId, + tollFreeWildCardPattern: "8**" + }); + orderId = order.id; + console.log("bandwidth order details", JSON.stringify(order)); + } + if (orderId) { + let result; + // poll + // sleep.... + for (let i = 0; i < 5; i++) { + result = await BandwidthNumbers.Order.getAsync(client, orderId); + if (result.orderStatus === "COMPLETE") { + const newNumbers = result.completedNumbers.map(cn => ({ + organization_id: organization.id, + area_code: cn.telephoneNumber.fullNumber.slice(0, 3), + phone_number: getFormattedPhoneNumber(cn.telephoneNumber.fullNumber), + service: "bandwidth", + allocated_to_id: config.sipPeerId, + service_id: cn.telephoneNumber.fullNumber, + allocated_at: new Date() + })); + await r.knex("owned_phone_number").insert(newNumbers); + totalPurchased = newNumbers.length; + break; + } else { + // TODO: is this reasonable? + await sleep(2000); + } + } + } + return totalPurchased; +} + +export async function deleteNumbersInAreaCode(organization, areaCode) { + // TODO + // disconnect (i.e. delete/drop) numbers + //await numbers.Disconnect.createAsync("Disconnect Order Name", ["9195551212", ...]) + const { client, config } = await getNumbersClient(organization); +} + +export async function createAccountBaseline(organization, options) { + // Does most of the things in Getting Started section of: + // https://dev.bandwidth.com/account/guides/programmaticApplicationSetup.html + // except creating an 'application' (messaging service in Spoke parlance) + // These pieces don't need to be done per-application/messagingservice + const configChanges = {}; + const { client, config } = await getNumbersClient(organization, options); + // 1. create sub-account/Site + if (!config.siteId || (options && options.newEverything)) { + const site = await BandwidthNumbers.Site.createAsync(client, { + name: `Spoke ${organization.name} (Subaccount)`, + address: { + houseNumber: config.houseNumber, + streetName: config.streetName, + city: config.city, + stateCode: config.stateCode, + zip: config.zip, + addressType: config.addressType || "Billing", + country: config.country || "United States" + } + }); + configChanges.siteId = site.id; + } + const siteId = configChanges.siteId || config.siteId; + // 2. create location/sippeer (w/ address) + let location; + if (!config.sipPeerId || (options && options.newEverything)) { + location = await BandwidthNumbers.SipPeer.createAsync(client, { + siteId, + peerName: `Spoke ${organization.name} (Location)`, + isDefaultPeer: true + }); + configChanges.sipPeerId = location.id; + } else { + location = await BandwidthNumbers.SipPeer.getAsync( + client, + siteId, + config.sipPeerId + ); + } + // 3. Enable SMS and MMS on location + // Note: create your own if you want different parameters (enforced) + await location.createSmsSettingsAsync({ + tollFree: true, + protocol: "http", + zone1: true, + zone2: false, + zone3: false, + zone4: false, + zone5: false + }); + await location.createMmsSettingsAsync({ + protocol: "http" + }); + return configChanges; +} + +export async function createMessagingService( + organization, + friendlyName, + serviceConfig +) { + const baseUrl = getConfig("BASE_URL", organization); + if (!baseUrl) { + return; + } + const { client, config } = await getNumbersClient(organization, { + serviceConfig + }); + // 1. create application + const application = await BandwidthNumbers.Application.createMessagingApplicationAsync( + client, + { + appName: friendlyName || "Spoke app", + msgCallbackUrl: `${baseUrl}/bandwidth/${(organization && + organization.id) || + ""}`, + callbackCreds: { + userId: "bandwidth.com", + password: "testtest" // TODO: see index.js + }, + requestedCallbackTypes: [ + "message-delivered", + "message-failed", + "message-sending" + ].map(c => ({ callbackType: c })) + } + ); + console.log("bandwidth createMessagingService", JSON.stringify(result)); + // 2. assign application to subaccount|site and location|sippeer + const location = await BandwidthNumbers.SipPeer.getAsync( + client, + config.siteId, + config.sipPeerId + ); + await location.editApplicationAsync({ + httpMessagingV2AppId: application.applicationId + }); + return application.applicationId; +} + +export async function deleteMessagingService( + organization, + messagingServiceSid +) { + const { client } = await getNumbersClient(organization); + const application = await BandwidthNumbers.Application.getAsync( + client, + messagingServiceSid + ); + await application.deleteAsync(); +} diff --git a/src/extensions/service-vendors/service_map.js b/src/extensions/service-vendors/service_map.js index 55daed4c8..629fbfa0c 100644 --- a/src/extensions/service-vendors/service_map.js +++ b/src/extensions/service-vendors/service_map.js @@ -1,10 +1,12 @@ import * as nexmo from "./nexmo"; import * as twilio from "./twilio"; import * as fakeservice from "./fakeservice"; +import * as bandwidth from "./bandwidth"; import { getConfig } from "../../server/api/lib/config"; // TODO this should be built dynamically export const serviceMap = { + bandwidth, nexmo, twilio, fakeservice From 974df89ae15c3820dac8096ff5f2d78c65c0c87b Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Wed, 5 May 2021 17:08:15 -0400 Subject: [PATCH 078/191] bugfixes: Twilio was not working as a library in production with the import-style. Also support multiple handlers as twilio uses, and calls to getMessageService for prod need to pass obscure...=false --- .../service-vendors/bandwidth/messaging.js | 9 +++--- .../bandwidth/setup-and-numbers.js | 4 ++- src/extensions/service-vendors/service_map.js | 8 ++--- .../service-vendors/twilio/index.js | 29 ++++++++++--------- src/server/index.js | 4 +-- 5 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/extensions/service-vendors/bandwidth/messaging.js b/src/extensions/service-vendors/bandwidth/messaging.js index 13d4c7472..dbf2c7a1c 100644 --- a/src/extensions/service-vendors/bandwidth/messaging.js +++ b/src/extensions/service-vendors/bandwidth/messaging.js @@ -56,11 +56,10 @@ export async function sendMessage({ campaign, serviceManagerData }) { - const config = await getMessageServiceConfig( - "bandwidth", - organization, - serviceManagerData - ); + const config = await getMessageServiceConfig("bandwidth", organization, { + obscureSensitiveInformation: false, + ...serviceManagerData + }); // applicationId will probably come from config, unless serviceManager is doing something fancy const applicationId = (serviceManagerData && serviceManagerData.messageservice_sid) || diff --git a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js index 0c08d40b2..538dac345 100644 --- a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js +++ b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js @@ -12,7 +12,9 @@ import { getMessageServiceConfig, getConfigKey } from "../service_map"; export async function getNumbersClient(organization, options) { const config = (options && options.serviceConfig) || - (await getMessageServiceConfig("bandwidth", organization)); + (await getMessageServiceConfig("bandwidth", organization, { + obscureSensitiveInformation: false + })); const password = await convertSecret( "bandwidthPassword", organization, diff --git a/src/extensions/service-vendors/service_map.js b/src/extensions/service-vendors/service_map.js index 629fbfa0c..bd0398885 100644 --- a/src/extensions/service-vendors/service_map.js +++ b/src/extensions/service-vendors/service_map.js @@ -20,11 +20,11 @@ export const addServerEndpoints = (app, adders) => { ); if (serviceAddServerEndpoints) { serviceAddServerEndpoints( - (route, handler) => { - adders.post(app, route, handler); + (route, ...handlers) => { + adders.post(app, route, ...handlers); }, - (route, handler) => { - adders.get(app, route, handler); + (route, ...handlers) => { + adders.get(app, route, ...handlers); } ); } diff --git a/src/extensions/service-vendors/twilio/index.js b/src/extensions/service-vendors/twilio/index.js index 1a58c1535..ad4475ac2 100644 --- a/src/extensions/service-vendors/twilio/index.js +++ b/src/extensions/service-vendors/twilio/index.js @@ -1,6 +1,6 @@ /* eslint-disable no-use-before-define, no-console */ import _ from "lodash"; -import * as twilioLibrary from "twilio"; +import Twilio, { twiml } from "twilio"; import urlJoin from "url-join"; import { log } from "../../../lib"; import { getFormattedPhoneNumber } from "../../../lib/phone-format"; @@ -39,16 +39,17 @@ export const getMetadata = () => ({ name: "twilio" }); -export async function getTwilio(organization) { +export const getTwilio = async organization => { const { authToken, accountSid } = await getMessageServiceConfig( "twilio", - organization + organization, + { obscureSensitiveInformation: false } ); if (accountSid && authToken) { - return twilioLibrary.default.Twilio(accountSid, authToken); // eslint-disable-line new-cap + return Twilio(accountSid, authToken); // eslint-disable-line new-cap } return null; -} +}; /** * Validate that the message came from Twilio before proceeding. @@ -62,14 +63,18 @@ const headerValidator = url => { const organization = req.params.orgId ? await cacheableData.organization.load(req.params.orgId) : null; - const { authToken } = await getMessageServiceConfig("twilio", organization); + const { authToken } = await getMessageServiceConfig( + "twilio", + organization, + { obscureSensitiveInformation: false } + ); const options = { validate: true, protocol: "https", url }; - return twilioLibrary.webhook(authToken, options)(req, res, next); + return Twilio.webhook(authToken, options)(req, res, next); }; }; @@ -117,7 +122,7 @@ export function addServerEndpoints(addPostRoute) { } catch (ex) { log.error(ex); } - const resp = new twilioLibrary.default.twiml.MessagingResponse(); + const resp = new twiml.MessagingResponse(); res.writeHead(200, { "Content-Type": "text/xml" }); res.end(resp.toString()); }) @@ -143,7 +148,7 @@ export function addServerEndpoints(addPostRoute) { } catch (ex) { log.error(ex); } - const resp = new twilioLibrary.default.twiml.MessagingResponse(); + const resp = new twiml.MessagingResponse(); res.writeHead(200, { "Content-Type": "text/xml" }); res.end(resp.toString()); }) @@ -879,7 +884,6 @@ export const getServiceConfig = async ( messageServiceSid = serviceConfig.TWILIO_MESSAGE_SERVICE_SID; } else { // for backward compatibility - const getConfigOptions = { onlyLocal: Boolean(restrictToOrgFeatures) }; const hasEncryptedToken = hasConfig( @@ -973,10 +977,7 @@ export const updateConfig = async (oldConfig, config, organization) => { if (twilioAuthToken && global.TEST_ENVIRONMENT !== "1") { // Make sure Twilio credentials work. // eslint-disable-next-line new-cap - const twilio = twilioLibrary.default.Twilio( - twilioAccountSid, - twilioAuthToken - ); + const twilio = Twilio(twilioAccountSid, twilioAuthToken); await twilio.api.accounts.list(); } } catch (err) { diff --git a/src/server/index.js b/src/server/index.js index 1f828bbc9..70fc7ed6a 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -112,8 +112,8 @@ Object.keys(configuredIngestMethods).forEach(ingestMethodName => { }); const routeAdders = { - get: (_app, route, handler) => _app.get(route, handler), - post: (_app, route, handler) => _app.post(route, handler) + get: (_app, route, ...handlers) => _app.get(route, ...handlers), + post: (_app, route, ...handlers) => _app.post(route, ...handlers) }; messagingServicesAddServerEndpoints(app, routeAdders); From 312bde8d7426e6a604b1b4272bc73811b7208eba Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Wed, 5 May 2021 17:53:29 -0400 Subject: [PATCH 079/191] fix tests for twilio library --- __test__/extensions/service-vendors/twilio.test.js | 10 +++++----- src/extensions/service-vendors/twilio/index.js | 13 ++++++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/__test__/extensions/service-vendors/twilio.test.js b/__test__/extensions/service-vendors/twilio.test.js index 1da3fae95..69b053d54 100644 --- a/__test__/extensions/service-vendors/twilio.test.js +++ b/__test__/extensions/service-vendors/twilio.test.js @@ -1,5 +1,5 @@ /* eslint-disable no-unused-expressions, consistent-return */ -import * as twilioLibrary from "twilio"; +import { twilioLibrary } from "../../../src/extensions/service-vendors/twilio"; import { getLastMessage } from "../../../src/extensions/service-vendors/message-sending"; import * as twilio from "../../../src/extensions/service-vendors/twilio"; import { getConfig } from "../../../src/server/api/lib/config"; // eslint-disable-line no-duplicate-imports, import/no-duplicates @@ -971,7 +971,7 @@ describe("config functions", () => { }; twilioApiAccountsListMock = jest.fn().mockResolvedValue({}); - jest.spyOn(twilioLibrary.default, "Twilio").mockReturnValue({ + jest.spyOn(twilioLibrary, "Twilio").mockReturnValue({ api: { accounts: { list: twilioApiAccountsListMock } } }); }); @@ -986,7 +986,7 @@ describe("config functions", () => { expect(crypto.symmetricEncrypt.mock.calls).toEqual([ ["fake_twilio_auth_token"] ]); - expect(twilioLibrary.default.Twilio.mock.calls).toEqual([ + expect(twilioLibrary.Twilio.mock.calls).toEqual([ [fakeAccountSid, fakeAuthToken] ]); expect(twilioApiAccountsListMock.mock.calls).toEqual([[]]); @@ -1006,7 +1006,7 @@ describe("config functions", () => { "twilioAccountSid and twilioMessageServiceSid are required" ); expect(crypto.symmetricEncrypt).not.toHaveBeenCalled(); - expect(twilioLibrary.default.Twilio).not.toHaveBeenCalled(); + expect(twilioLibrary.Twilio).not.toHaveBeenCalled(); expect(twilioApiAccountsListMock).not.toHaveBeenCalled(); }); }); @@ -1015,7 +1015,7 @@ describe("config functions", () => { twilioApiAccountsListMock = jest.fn().mockImplementation(() => { throw new Error("OH NO!"); }); - jest.spyOn(twilioLibrary.default, "Twilio").mockReturnValue({ + jest.spyOn(twilioLibrary, "Twilio").mockReturnValue({ api: { accounts: { list: twilioApiAccountsListMock } } }); }); diff --git a/src/extensions/service-vendors/twilio/index.js b/src/extensions/service-vendors/twilio/index.js index ad4475ac2..8d42a35b9 100644 --- a/src/extensions/service-vendors/twilio/index.js +++ b/src/extensions/service-vendors/twilio/index.js @@ -32,6 +32,7 @@ const ENABLE_DB_LOG = getConfig("ENABLE_DB_LOG"); const TWILIO_SKIP_VALIDATION = getConfig("TWILIO_SKIP_VALIDATION"); const BULK_REQUEST_CONCURRENCY = 5; const MAX_NUMBERS_PER_BUY_JOB = getConfig("MAX_NUMBERS_PER_BUY_JOB") || 100; +export const twilioLibrary = { Twilio, twiml }; export const getMetadata = () => ({ supportsOrgConfig: getConfig("TWILIO_MULTI_ORG", null, { truthy: true }), @@ -46,7 +47,7 @@ export const getTwilio = async organization => { { obscureSensitiveInformation: false } ); if (accountSid && authToken) { - return Twilio(accountSid, authToken); // eslint-disable-line new-cap + return twilioLibrary.Twilio(accountSid, authToken); // eslint-disable-line new-cap } return null; }; @@ -122,7 +123,7 @@ export function addServerEndpoints(addPostRoute) { } catch (ex) { log.error(ex); } - const resp = new twiml.MessagingResponse(); + const resp = new twilioLibrary.twiml.MessagingResponse(); res.writeHead(200, { "Content-Type": "text/xml" }); res.end(resp.toString()); }) @@ -148,7 +149,7 @@ export function addServerEndpoints(addPostRoute) { } catch (ex) { log.error(ex); } - const resp = new twiml.MessagingResponse(); + const resp = new twilioLibrary.twiml.MessagingResponse(); res.writeHead(200, { "Content-Type": "text/xml" }); res.end(resp.toString()); }) @@ -977,10 +978,11 @@ export const updateConfig = async (oldConfig, config, organization) => { if (twilioAuthToken && global.TEST_ENVIRONMENT !== "1") { // Make sure Twilio credentials work. // eslint-disable-next-line new-cap - const twilio = Twilio(twilioAccountSid, twilioAuthToken); + const twilio = twilioLibrary.Twilio(twilioAccountSid, twilioAuthToken); await twilio.api.accounts.list(); } } catch (err) { + console.log("twilio.updateConfig client error", err); throw new Error("Invalid Twilio credentials"); } @@ -1050,5 +1052,6 @@ export default { getServiceConfig, getMessageServiceSid, updateConfig, - getMetadata + getMetadata, + fullyConfigured }; From ca8bb97ca3823344a5cb6c44d428d36b2eddce4e Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Thu, 6 May 2021 15:43:31 -0400 Subject: [PATCH 080/191] service-managers draft and some service-vendors tweaks --- .../updateServiceVendorConfig.test.js | 10 +- __test__/server/api/organization.test.js | 8 +- __test__/test_helpers.js | 6 +- src/api/campaign.js | 2 + src/api/message-service.js | 9 -- src/api/organization.js | 3 +- src/api/schema.js | 15 ++- src/api/service.js | 22 ++++ src/containers/AdminCampaignEdit.jsx | 10 +- src/containers/AdminCampaignStats.jsx | 10 +- src/containers/Settings.jsx | 124 ++++++++++++++++-- src/extensions/service-managers/components.js | 14 ++ src/extensions/service-managers/index.js | 77 +++++++++++ .../test-fake-example/index.js | 118 +++++++++++++++++ .../test-fake-example/react-component.js | 66 ++++++++++ .../bandwidth/react-component.js | 3 +- src/server/api/campaign.js | 32 +++++ src/server/api/mutations/index.js | 1 + .../mutations/updateServiceVendorConfig.js | 38 +++--- src/server/api/organization.js | 31 ++++- src/server/api/schema.js | 2 + src/workers/jobs.js | 20 ++- src/workers/tasks.js | 27 +++- 23 files changed, 588 insertions(+), 60 deletions(-) delete mode 100644 src/api/message-service.js create mode 100644 src/api/service.js create mode 100644 src/extensions/service-managers/components.js create mode 100644 src/extensions/service-managers/index.js create mode 100644 src/extensions/service-managers/test-fake-example/index.js create mode 100644 src/extensions/service-managers/test-fake-example/react-component.js diff --git a/__test__/server/api/mutations/updateServiceVendorConfig.test.js b/__test__/server/api/mutations/updateServiceVendorConfig.test.js index 7a69c43e5..455954853 100644 --- a/__test__/server/api/mutations/updateServiceVendorConfig.test.js +++ b/__test__/server/api/mutations/updateServiceVendorConfig.test.js @@ -34,6 +34,10 @@ describe("updateServiceVendorConfig", () => { user = await createUser(); const invite = await createInvite(); const createOrganizationResult = await createOrganization(user, invite); + console.log( + "updateServiceVendorconfig test beforeEach createOrgRsul", + createOrganizationResult + ); organization = createOrganizationResult.data.createOrganization; await ensureOrganizationTwilioWithMessagingService( createOrganizationResult @@ -61,14 +65,14 @@ describe("updateServiceVendorConfig", () => { vars = { organizationId: organization.id, - messageServiceName: "twilio", + serviceName: "twilio", config: JSON.stringify(newConfig) }; }); describe("when it's not the configured message service name", () => { beforeEach(async () => { - vars.messageServiceName = "this will never be a message service name"; + vars.serviceName = "this will never be a message service name"; }); it("returns an error", async () => { @@ -271,7 +275,7 @@ describe("updateServiceVendorConfig", () => { beforeEach(async () => { service = "extremely_fake_service"; configKey = serviceMap.getConfigKey(service); - vars.messageServiceName = service; + vars.serviceName = service; dbOrganization.features = JSON.stringify({ service, TWILIO_ACCOUNT_SID: "the former_fake_account_sid", diff --git a/__test__/server/api/organization.test.js b/__test__/server/api/organization.test.js index 196763dcb..6a2075a14 100644 --- a/__test__/server/api/organization.test.js +++ b/__test__/server/api/organization.test.js @@ -323,7 +323,7 @@ describe("organization", async () => { }); }); - describe(".messageService", () => { + describe(".serviceVendor", () => { let gqlQuery; let variables; let fakeConfig; @@ -349,9 +349,9 @@ describe("organization", async () => { .mockReturnValue(fakeMetadata); gqlQuery = gql` - query messageService($organizationId: String!) { + query serviceVendor($organizationId: String!) { organization(id: $organizationId) { - messageService { + serviceVendor { name supportsOrgConfig config @@ -367,7 +367,7 @@ describe("organization", async () => { it("calls functions and returns the result", async () => { const result = await runGql(gqlQuery, variables, testAdminUser); console.log("result", result); - expect(result.data.organization.messageService).toEqual({ + expect(result.data.organization.serviceVendor).toEqual({ ...fakeMetadata, config: fakeConfig }); diff --git a/__test__/test_helpers.js b/__test__/test_helpers.js index 197cdb9a8..6425f3bda 100644 --- a/__test__/test_helpers.js +++ b/__test__/test_helpers.js @@ -243,12 +243,12 @@ export async function setTwilioAuth(user, organization) { const query = ` mutation updateServiceVendorConfig( $organizationId: String! - $messageServiceName: String! + $serviceName: String! $config: JSON! ) { updateServiceVendorConfig( organizationId: $organizationId - messageServiceName: $messageServiceName + serviceName: $serviceName config: $config ) } @@ -262,7 +262,7 @@ export async function setTwilioAuth(user, organization) { const variables = { organizationId: orgId, - messageServiceName: "twilio", + serviceName: "twilio", config: JSON.stringify(twilioConfig) }; diff --git a/src/api/campaign.js b/src/api/campaign.js index 862b01ebf..e941cc796 100644 --- a/src/api/campaign.js +++ b/src/api/campaign.js @@ -129,6 +129,8 @@ export const schema = gql` textingHoursStart: Int textingHoursEnd: Int timezone: String + serviceManagers(fromCampaignStatsPage: Boolean): [ServiceManager] + messageserviceSid: String useOwnMessagingService: Boolean phoneNumbers: [String] diff --git a/src/api/message-service.js b/src/api/message-service.js deleted file mode 100644 index a563da5c4..000000000 --- a/src/api/message-service.js +++ /dev/null @@ -1,9 +0,0 @@ -import gql from "graphql-tag"; - -export const schema = gql` - type MessageService { - name: String! - config: JSON - supportsOrgConfig: Boolean! - } -`; diff --git a/src/api/organization.js b/src/api/organization.js index 6991887bf..96adb81e7 100644 --- a/src/api/organization.js +++ b/src/api/organization.js @@ -73,7 +73,8 @@ export const schema = gql` texterUIConfig: TexterUIConfig cacheable: Int tags(group: String): [Tag] - messageService: MessageService + serviceVendor: ServiceVendor + serviceManagers: [ServiceManager] fullyConfigured: Boolean emailEnabled: Boolean phoneInventoryEnabled: Boolean! diff --git a/src/api/schema.js b/src/api/schema.js index f65ab7876..0a00c4f7a 100644 --- a/src/api/schema.js +++ b/src/api/schema.js @@ -14,7 +14,7 @@ import { schema as campaignContactSchema } from "./campaign-contact"; import { schema as cannedResponseSchema } from "./canned-response"; import { schema as inviteSchema } from "./invite"; import { schema as tagSchema } from "./tag"; -import { schema as messageServiceSchema } from "./message-service"; +import { schema as serviceSchema } from "./service"; const rootSchema = gql` input CampaignContactInput { @@ -297,9 +297,16 @@ const rootSchema = gql` ): Organization updateServiceVendorConfig( organizationId: String! - messageServiceName: String! + serviceName: String! config: JSON! - ): JSON + ): ServiceVendor + updateServiceManager( + organizationId: String! + campaignId: String + serviceManagerName: String! + updateData: JSON! + fromCampaignStatsPage: Boolean + ): ServiceManager bulkSendMessages(assignmentId: Int!): [CampaignContact] sendMessage( message: MessageInput! @@ -409,5 +416,5 @@ export const schema = [ inviteSchema, conversationSchema, tagSchema, - messageServiceSchema + serviceSchema ]; diff --git a/src/api/service.js b/src/api/service.js new file mode 100644 index 000000000..c34deee48 --- /dev/null +++ b/src/api/service.js @@ -0,0 +1,22 @@ +import gql from "graphql-tag"; + +export const schema = gql` + type ServiceVendor { + id: String! + name: String! + config: JSON + supportsOrgConfig: Boolean! + } + + type ServiceManager { + id: String! + name: String! + displayName: String! + data: JSON + supportsOrgConfig: Boolean! + supportsCampaignConfig: Boolean! + fullyConfigured: Boolean + organization: Organization + campaign: Campaign + } +`; diff --git a/src/containers/AdminCampaignEdit.jsx b/src/containers/AdminCampaignEdit.jsx index cbe2dd042..bdaa9a63b 100644 --- a/src/containers/AdminCampaignEdit.jsx +++ b/src/containers/AdminCampaignEdit.jsx @@ -47,8 +47,6 @@ const campaignInfoFragment = ` logoImageUrl introHtml primaryColor - useOwnMessagingService - messageserviceSid overrideOrganizationTextingHours textingHoursEnforced textingHoursStart @@ -107,6 +105,14 @@ const campaignInfoFragment = ` status resultMessage } + serviceManagers { + id + name + supportsOrgConfig + data + } + useOwnMessagingService + messageserviceSid inventoryPhoneNumberCounts { areaCode count diff --git a/src/containers/AdminCampaignStats.jsx b/src/containers/AdminCampaignStats.jsx index 3adac78e8..549df5236 100644 --- a/src/containers/AdminCampaignStats.jsx +++ b/src/containers/AdminCampaignStats.jsx @@ -440,6 +440,7 @@ const queries = { $contactsFilter: ContactsFilter! $needsResponseFilter: ContactsFilter! $assignmentsFilter: AssignmentsFilter + $fromCampaignStatsPage: Boolean ) { campaign(id: $campaignId) { id @@ -496,6 +497,12 @@ const queries = { } } cacheable + serviceManagers(fromCampaignStatsPage: $fromCampaignStatsPage) { + id + name + supportsCampaignConfig + data + } } } `, @@ -512,7 +519,8 @@ const queries = { needsResponseFilter: { messageStatus: "needsResponse", isOptedOut: false - } + }, + fromCampaignStatsPage: true }, pollInterval: 5000 }) diff --git a/src/containers/Settings.jsx b/src/containers/Settings.jsx index 026a85a1f..1c9737950 100644 --- a/src/containers/Settings.jsx +++ b/src/containers/Settings.jsx @@ -18,6 +18,7 @@ import moment from "moment"; import CampaignTexterUIForm from "../components/CampaignTexterUIForm"; import OrganizationFeatureSettings from "../components/OrganizationFeatureSettings"; import { getServiceVendorComponent } from "../extensions/service-vendors/components"; +import { getServiceManagerComponent } from "../extensions/service-managers/components"; import GSTextField from "../components/forms/GSTextField"; const styles = StyleSheet.create({ @@ -137,13 +138,79 @@ class Settings extends React.Component { ); } + renderServiceManagers() { + const { + id: organizationId, + serviceManagers + } = this.props.data.organization; + if (!serviceManagers.length) { + return null; + } + const allFullyConfigured = serviceManagers + .map(sm => sm.fullyConfigured !== false) + .reduce((a, b) => a && b, true); + return ( + + + + {serviceManagers.map(sm => { + const ServiceManagerComp = getServiceManagerComponent( + sm.name, + "OrgConfig" + ); + const serviceManagerName = sm.name; + return ( + + + +
FOO BAR {sm.name}
+ { + console.log("onSubmit updateData: ", updateData); + return this.props.mutations.updateServiceManager( + serviceManagerName, + updateData + ); + }} + /> +
+
+ ); + })} +
+
+ ); + } + renderServiceVendorConfig() { - const { id: organizationId, messageService } = this.props.data.organization; - if (!messageService) { + const { id: organizationId, serviceVendor } = this.props.data.organization; + if (!serviceVendor) { return null; } - const { name, supportsOrgConfig, config } = messageService; + const { name, supportsOrgConfig, config } = serviceVendor; if (!supportsOrgConfig) { return null; } @@ -261,6 +328,7 @@ class Settings extends React.Component {
{this.renderTextingHoursForm()}
+ {this.renderServiceManagers()} {this.renderServiceVendorConfig()} {this.props.data.organization && this.props.data.organization.texterUIConfig && @@ -377,11 +445,20 @@ const queries = { options sideboxChoices } - messageService { + serviceVendor { + id name supportsOrgConfig config } + serviceManagers { + id + name + displayName + supportsOrgConfig + data + fullyConfigured + } } } `, @@ -418,14 +495,35 @@ export const editOrganizationGql = gql` export const updateServiceVendorConfigGql = gql` mutation updateServiceVendorConfig( $organizationId: String! - $messageServiceName: String! + $serviceName: String! $config: JSON! ) { updateServiceVendorConfig( organizationId: $organizationId - messageServiceName: $messageServiceName + serviceName: $serviceName config: $config - ) + ) { + id + config + } + } +`; + +export const updateServiceManagerGql = gql` + mutation updateServiceManager( + $organizationId: String! + $serviceManagerName: String! + $updateData: JSON! + ) { + updateServiceManager( + organizationId: $organizationId + serviceManagerName: $serviceManagerName + updateData: $updateData + ) { + id + data + fullyConfigured + } } `; @@ -509,11 +607,21 @@ const mutations = { mutation: updateServiceVendorConfigGql, variables: { organizationId: ownProps.params.organizationId, - messageServiceName: ownProps.data.organization.messageService.name, + serviceName: ownProps.data.organization.serviceVendor.name, config: JSON.stringify(newConfig) } }; }, + updateServiceManager: ownProps => (serviceManagerName, updateData) => { + return { + mutation: updateServiceManagerGql, + variables: { + serviceManagerName, + updateData, + organizationId: ownProps.params.organizationId + } + }; + }, clearCachedOrgAndExtensionCaches: ownProps => () => ({ mutation: gql` mutation clearCachedOrgAndExtensionCaches($organizationId: String!) { diff --git a/src/extensions/service-managers/components.js b/src/extensions/service-managers/components.js new file mode 100644 index 000000000..82b5477e0 --- /dev/null +++ b/src/extensions/service-managers/components.js @@ -0,0 +1,14 @@ +/* eslint no-console: 0 */ +export const getServiceManagerComponent = (serviceName, componentName) => { + try { + // eslint-disable-next-line global-require + const component = require(`./${serviceName}/react-component.js`); + return component && component[componentName]; + } catch (caught) { + console.log("caught", caught); + console.error( + `SERVICE_VENDOR failed to load react component for ${serviceName}` + ); + return null; + } +}; diff --git a/src/extensions/service-managers/index.js b/src/extensions/service-managers/index.js new file mode 100644 index 000000000..8e1900bb5 --- /dev/null +++ b/src/extensions/service-managers/index.js @@ -0,0 +1,77 @@ +import { getConfig } from "../../server/api/lib/config"; + +export function getServiceManagers(organization) { + const handlerKey = "SERVICE_MANAGERS"; + const configuredHandlers = getConfig(handlerKey, organization); + const enabledHandlers = + (configuredHandlers && configuredHandlers.split(",")) || []; + + const handlers = []; + enabledHandlers.forEach(name => { + try { + const c = require(`./${name}/index.js`); + handlers.push(c); + } catch (err) { + console.error( + `${handlerKey} failed to load service manager ${name} -- ${err}` + ); + } + }); + return handlers; +} + +export function serviceManagersHaveImplementation(funcName, organization) { + // in-case funcArgs or organization is an additional 'hit' in a fast-path + // then call this method first to make sure it's worth loading extra context + // before calling processServiceManagers + const managers = getServiceManagers(organization); + return managers.filter(m => typeof m[funcName] === "function").length; +} + +export async function processServiceManagers( + funcName, + organization, + funcArgs, + specificServiceManagerName +) { + const managers = getServiceManagers(organization); + const funkyManagers = managers.filter( + m => + typeof m[funcName] === "function" && + (!specificServiceManagerName || m.name === specificServiceManagerName) + ); + console.log("service-managers.processServiceManagers", funkyManagers); + const resultArray = []; + // explicitly process these in order in case the order matters + for (let i = 0, l = funkyManagers.length; i < l; i++) { + resultArray.push( + await funkyManagers[i][funcName]({ organization, ...funcArgs }) + ); + } + // NOTE: some methods pass a shared modifiable object, e.g. 'saveData' + // that might be modified in-place, rather than the resultArray + // being important. + return resultArray; +} + +export async function getServiceManagerData( + funcName, + organization, + funcArgs, + specificServiceManagerName +) { + const managers = getServiceManagers(organization); + const funkyManagers = managers.filter( + m => + typeof m[funcName] === "function" && + (!specificServiceManagerName || m.name === specificServiceManagerName) + ); + const resultArray = await Promise.all( + funkyManagers.map(async sm => ({ + name: sm.name, + ...sm.metadata(), + ...(await sm[funcName]({ organization, ...funcArgs })) + })) + ); + return resultArray; +} diff --git a/src/extensions/service-managers/test-fake-example/index.js b/src/extensions/service-managers/test-fake-example/index.js new file mode 100644 index 000000000..a7c0551a2 --- /dev/null +++ b/src/extensions/service-managers/test-fake-example/index.js @@ -0,0 +1,118 @@ +/// All functions are OPTIONAL EXCEPT metadata() and const name=. +/// DO NOT IMPLEMENT ANYTHING YOU WILL NOT USE -- the existence of a function adds behavior/UI (sometimes costly) + +export const name = "test-fake-example"; + +export const metadata = () => ({ + // set canSpendMoney=true, if this extension can lead to (additional) money being spent + // if it can, which operations below can trigger money being spent? + displayName: "Test Fake Service Manager Example", + canSpendMoney: false, + moneySpendingOperations: ["onCampaignStart"], + supportsOrgConfig: true, + supportsCampaignConfig: true +}); + +export async function onMessageSend({ + message, + contact, + organization, + campaign +}) {} + +// maybe also with organization (only looked up if an enabled hook supports onDeliveryReport +export async function onDeliveryReport({ + contactNumber, + userNumber, + messageSid, + service, + messageServiceSid, + newStatus, + errorCode +}) {} + +export async function isCampaignReady({ + organization, + campaign, + user, + loaders +}) { + // if NOT ready, must return object in form of { ready: false, code: "", message: ""} +} + +export async function getCampaignData({ + organization, + campaign, + user, + loaders, + fromCampaignStatsPage +}) { + // MUST NOT RETURN SECRETS! + // called both from edit and stats contexts: editMode==true for edit page + return { + data: { + foo: "bar" + }, + fullyConfigured: true + }; +} + +export async function onCampaignUpdateSignal({ + organization, + campaign, + user, + updateData, + fromCampaignStatsPage +}) {} + +export async function onCampaignContactLoad({ + organization, + campaign, + ingestResult, + ingestDataReference, + finalContactCount, + deleteOptOutCells +}) { + console.log( + "service-managers.test-fake-example.OnCampaignContactLoad 11", + organization.id, + campaign.id, + ingestResult, + ingestDataReference + ); +} + +export async function getOrganizationData({ organization, user, loaders }) { + // MUST NOT RETURN SECRETS! + return { + // data is any JSON-able data that you want to send. + // This can/should map to the return value if you implement onOrganizationUpdateSignal() + // which will then get updated data in the Settings component on-save + data: { foo: "bar" }, + // fullyConfigured: null means (more) configuration is optional -- maybe not required to be enabled + // fullyConfigured: true means it is fully enabled and configured for operation + // fullyConfigured: false means more configuration is REQUIRED (i.e. manager is necessary and needs more configuration for Spoke campaigns to run) + fullyConfigured: null + }; +} + +export async function onOrganizationServiceSetup({ + organization, + user, + service +}) {} + +export async function onOrganizationUpdateSignal({ + organization, + user, + updateData +}) { + return { + data: updateData, + fullyConfigured: true + }; +} + +export async function onCampaignStart({}) {} + +export async function onCampaignArchive({}) {} diff --git a/src/extensions/service-managers/test-fake-example/react-component.js b/src/extensions/service-managers/test-fake-example/react-component.js new file mode 100644 index 000000000..615a30921 --- /dev/null +++ b/src/extensions/service-managers/test-fake-example/react-component.js @@ -0,0 +1,66 @@ +/* eslint no-console: 0 */ +import { css } from "aphrodite"; +import { CardText } from "material-ui/Card"; +import Dialog from "material-ui/Dialog"; +import FlatButton from "material-ui/FlatButton"; +import { Table, TableBody, TableRow, TableRowColumn } from "material-ui/Table"; +import PropTypes from "prop-types"; +import React from "react"; +import Form from "react-formal"; +import * as yup from "yup"; +import DisplayLink from "../../../components/DisplayLink"; +import GSForm from "../../../components/forms/GSForm"; +import GSTextField from "../../../components/forms/GSTextField"; +import GSSubmitButton from "../../../components/forms/GSSubmitButton"; + +export class OrgConfig extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + console.log("testfakedata OrgConfig", this.props); + const formSchema = yup.object({ + savedText: yup + .string() + .nullable() + .max(64) + }); + return ( +
+ THIS IS TEST_FAKE_DATA + { + console.log("onSubmit", x); + this.props.onSubmit(x); + }} + > + + + +
+ ); + } +} + +OrgConfig.propTypes = { + organizationId: PropTypes.string, + serviceManagerInfo: PropTypes.object, + inlineStyles: PropTypes.object, + styles: PropTypes.object, + saveLabel: PropTypes.string, + onSubmit: PropTypes.func +}; diff --git a/src/extensions/service-vendors/bandwidth/react-component.js b/src/extensions/service-vendors/bandwidth/react-component.js index 06ebc4443..b6043f414 100644 --- a/src/extensions/service-vendors/bandwidth/react-component.js +++ b/src/extensions/service-vendors/bandwidth/react-component.js @@ -30,8 +30,8 @@ export class OrgConfig extends React.Component { this.state = { allSet, ...this.props.config, country: "United States" }; this.props.onAllSetChanged(allSet); } + /* - componentDidUpdate(prevProps) { const { accountSid: prevAccountSid, authToken: prevAuthToken, @@ -63,7 +63,6 @@ export class OrgConfig extends React.Component { let newError; try { await this.props.onSubmit(config); - await this.props.requestRefetch(); this.setState({ error: undefined }); diff --git a/src/server/api/campaign.js b/src/server/api/campaign.js index 0dba2f061..a31a814bb 100644 --- a/src/server/api/campaign.js +++ b/src/server/api/campaign.js @@ -5,6 +5,7 @@ import { getServiceNameFromOrganization, errorDescription } from "../../extensions/service-vendors"; +import { getServiceManagerData } from "../../extensions/service-managers"; import { Campaign, JobRequest, r, cacheableData } from "../models"; import { getUsers } from "./user"; import { getSideboxChoices } from "./organization"; @@ -684,6 +685,37 @@ export const resolvers = { } return ""; }, + serviceManagers: async ( + campaign, + { fromCampaignStatsPage }, + { user, loaders } + ) => { + await accessRequired( + user, + campaign.organization_id, + "SUPERVOLUNTEER", + true + ); + const organization = await loaders.organization.load( + campaign.organization_id + ); + const result = await getServiceManagerData( + "getCampaignData", + organization, + { organization, campaign, user, loaders, fromCampaignStatsPage } + ); + return result.map(r => ({ + id: `${r.name}-org${campaign.organization_id}-${campaign.id}${ + fromCampaignStatsPage ? "stats" : "" + }`, + campaign, + organization, + // defaults + fullyConfigured: null, + data: null, + ...r + })); + }, phoneNumbers: async (campaign, _, { user }) => { await accessRequired( user, diff --git a/src/server/api/mutations/index.js b/src/server/api/mutations/index.js index 2b96af68f..92d3069af 100644 --- a/src/server/api/mutations/index.js +++ b/src/server/api/mutations/index.js @@ -12,4 +12,5 @@ export { updateQuestionResponses } from "./updateQuestionResponses"; export { releaseCampaignNumbers } from "./releaseCampaignNumbers"; export { clearCachedOrgAndExtensionCaches } from "./clearCachedOrgAndExtensionCaches"; export { updateFeedback } from "./updateFeedback"; +export { updateServiceManager } from "./updateServiceManager"; export { updateServiceVendorConfig } from "./updateServiceVendorConfig"; diff --git a/src/server/api/mutations/updateServiceVendorConfig.js b/src/server/api/mutations/updateServiceVendorConfig.js index 151faec26..7bbbebfb4 100644 --- a/src/server/api/mutations/updateServiceVendorConfig.js +++ b/src/server/api/mutations/updateServiceVendorConfig.js @@ -11,33 +11,29 @@ import { Organization } from "../../../server/models"; export const updateServiceVendorConfig = async ( _, - { organizationId, messageServiceName, config }, + { organizationId, serviceName, config }, { user } ) => { await accessRequired(user, organizationId, "OWNER"); const organization = await orgCache.load(organizationId); - const configuredMessageServiceName = orgCache.getMessageService(organization); - if (configuredMessageServiceName !== messageServiceName) { + const configuredServiceName = orgCache.getMessageService(organization); + if (configuredServiceName !== serviceName) { throw new GraphQLError( - `Can't configure ${messageServiceName}. It's not the configured message service` + `Can't configure ${serviceName}. It's not the configured message service` ); } - const service = getService(messageServiceName); + const service = getService(serviceName); if (!service) { - throw new GraphQLError( - `${messageServiceName} is not a valid message service` - ); + throw new GraphQLError(`${serviceName} is not a valid message service`); } const serviceConfigFunction = tryGetFunctionFromService( - messageServiceName, + serviceName, "updateConfig" ); if (!serviceConfigFunction) { - throw new GraphQLError( - `${messageServiceName} does not support configuration` - ); + throw new GraphQLError(`${serviceName} does not support configuration`); } let configObject; @@ -47,7 +43,7 @@ export const updateServiceVendorConfig = async ( throw new GraphQLError("Config is not valid JSON"); } - const configKey = getConfigKey(messageServiceName); + const configKey = getConfigKey(serviceName); const existingConfig = getConfig(configKey, organization, { onlyLocal: true }); @@ -62,9 +58,7 @@ export const updateServiceVendorConfig = async ( } catch (caught) { // eslint-disable-next-line no-console console.error( - `Error updating config for ${messageServiceName}: ${JSON.stringify( - caught - )}` + `Error updating config for ${serviceName}: ${JSON.stringify(caught)}` ); throw new GraphQLError(caught.message); } @@ -74,7 +68,7 @@ export const updateServiceVendorConfig = async ( const hadMessageServiceConfig = !!features[configKey]; const newConfigKeys = new Set(Object.keys(newConfig)); const legacyTwilioConfig = - messageServiceName === "twilio" && + serviceName === "twilio" && !hadMessageServiceConfig && Object.keys(features).some(k => newConfigKeys.has(k)); @@ -88,7 +82,13 @@ export const updateServiceVendorConfig = async ( await orgCache.clear(organization.id); const updatedOrganization = await orgCache.load(organization.id); - return orgCache.getMessageServiceConfig(updatedOrganization); + return { + id: `org${organization.id}-${serviceName}`, + config: await orgCache.getMessageServiceConfig(updatedOrganization, { + restrictToOrgFeatures: true, + obscureSensitiveInformation: true + }) + }; }; export const getServiceVendorConfig = async ( @@ -107,5 +107,5 @@ export const getServiceVendorConfig = async ( const config = getConfig(configKey, organization, { onlyLocal: options.restrictToOrgFeatures }); - return getServiceConfig(config, organization, options); + getServiceConfig(config, organization, options); }; diff --git a/src/server/api/organization.js b/src/server/api/organization.js index 39fcb4f6f..780eccb84 100644 --- a/src/server/api/organization.js +++ b/src/server/api/organization.js @@ -13,6 +13,7 @@ import { fullyConfigured, getServiceMetadata } from "../../extensions/service-vendors"; +import { getServiceManagerData } from "../../extensions/service-managers"; export const ownerConfigurable = { // ACTION_HANDLERS: 1, @@ -211,14 +212,20 @@ export const resolvers = { cacheable: (org, _, { user }) => // quanery logic. levels are 0, 1, 2 r.redis ? (getConfig("REDIS_CONTACT_CACHE", org) ? 2 : 1) : 0, - messageService: async (organization, _, { user }) => { + serviceVendor: async (organization, _, { user }) => { try { await accessRequired(user, organization.id, "OWNER"); const serviceName = cacheableData.organization.getMessageService( organization ); const serviceMetadata = getServiceMetadata(serviceName); + console.log( + "organization.messageService", + serviceName, + serviceMetadata + ); return { + id: `org${organization.id}-${serviceName}`, ...serviceMetadata, config: cacheableData.organization.getMessageServiceConfig( organization, @@ -226,9 +233,31 @@ export const resolvers = { ) }; } catch (caught) { + console.log("organization.messageService error", caught); return null; } }, + serviceManagers: async (organization, _, { user, loaders }) => { + try { + await accessRequired(user, organization.id, "OWNER", true); + const result = await getServiceManagerData( + "getOrganizationData", + organization, + { organization, user, loaders } + ); + return result.map(r => ({ + id: `${r.name}-org${organization.id}-`, + organization, + // defaults + fullyConfigured: null, + data: null, + ...r + })); + } catch (err) { + console.log("orgaization.serviceManagers error", err); + return []; + } + }, fullyConfigured: async organization => { return fullyConfigured(organization); }, diff --git a/src/server/api/schema.js b/src/server/api/schema.js index bab455d34..d790ac118 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -65,6 +65,7 @@ import { releaseCampaignNumbers, clearCachedOrgAndExtensionCaches, updateFeedback, + updateServiceManager, updateServiceVendorConfig } from "./mutations"; @@ -500,6 +501,7 @@ const rootMutations = { startCampaign, releaseCampaignNumbers, clearCachedOrgAndExtensionCaches, + updateServiceManager, updateServiceVendorConfig, userAgreeTerms: async (_, { userId }, { user }) => { // We ignore userId: you can only agree to terms for yourself diff --git a/src/workers/jobs.js b/src/workers/jobs.js index b37e1bd20..1d3c1d782 100644 --- a/src/workers/jobs.js +++ b/src/workers/jobs.js @@ -16,6 +16,7 @@ import { getLastMessage, saveNewIncomingMessage } from "../extensions/service-vendors/message-sending"; +import { serviceManagersHaveImplementation } from "../extensions/service-managers"; import importScriptFromDocument from "../server/api/lib/import-script"; import { rawIngestMethod } from "../extensions/contact-loaders"; @@ -26,6 +27,7 @@ import { sendEmail } from "../server/mail"; import { Notifications, sendUserNotification } from "../server/notifications"; import { getConfig } from "../server/api/lib/config"; import { invokeTaskFunction, Tasks } from "./tasks"; + import fs from "fs"; import path from "path"; @@ -278,7 +280,7 @@ export async function completeContactLoad( ingestResult ) { const campaignId = job.campaign_id; - const campaign = await Campaign.get(campaignId); + const campaign = await cacheableData.campaign.load(campaignId); const organization = await Organization.get(campaign.organization_id); let deleteOptOutCells = null; @@ -349,6 +351,22 @@ export async function completeContactLoad( deleteDuplicateCells, ingestResult }); + + if ( + serviceManagersHaveImplementation("onCampaignContactLoad", organization) + ) { + await invokeTaskFunction(Tasks.SERVICE_MANAGER_TRIGGER, { + functionName: "onCampaignContactLoad", + organizationId: organization.id, + data: { + campaign, + ingestResult, + ingestDataReference, + finalContactCount, + deleteOptOutCells + } + }); + } } export async function unzipPayload(job) { diff --git a/src/workers/tasks.js b/src/workers/tasks.js index 8b164f4bb..4288f3ac1 100644 --- a/src/workers/tasks.js +++ b/src/workers/tasks.js @@ -4,14 +4,36 @@ import serviceMap from "../extensions/service-vendors"; import * as ActionHandlers from "../extensions/action-handlers"; import { cacheableData } from "../server/models"; +import { processServiceManagers } from "../extensions/service-managers"; export const Tasks = Object.freeze({ SEND_MESSAGE: "send_message", ACTION_HANDLER_QUESTION_RESPONSE: "action_handler:question_response", ACTION_HANDLER_TAG_UPDATE: "action_handler:tag_update", - CAMPAIGN_START_CACHE: "campaign_start_cache" + CAMPAIGN_START_CACHE: "campaign_start_cache", + SERVICE_MANAGER_TRIGGER: "service_manager_trigger" }); +const serviceManagerTrigger = async ({ + functionName, + organizationId, + data +}) => { + console.log( + "serviceManagerTrigger", + functionName, + organizationId, + Object.keys(data) + ); + let organization, campaign; + if (organizationId) { + organization = await cacheableData.organization.load(organizationId); + console.log("serviceManagerTrigger org", organization.name); + } + + await processServiceManagers(functionName, organization, data); +}; + const sendMessage = async ({ message, contact, @@ -105,7 +127,8 @@ const taskMap = Object.freeze({ [Tasks.SEND_MESSAGE]: sendMessage, [Tasks.ACTION_HANDLER_QUESTION_RESPONSE]: questionResponseActionHandler, [Tasks.ACTION_HANDLER_TAG_UPDATE]: tagUpdateActionHandler, - [Tasks.CAMPAIGN_START_CACHE]: startCampaignCache + [Tasks.CAMPAIGN_START_CACHE]: startCampaignCache, + [Tasks.SERVICE_MANAGER_TRIGGER]: serviceManagerTrigger }); export const invokeTaskFunction = async (taskName, payload) => { From 8ac50f73f91c3d3a72b7bab979400baccda51d01 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Thu, 6 May 2021 19:09:11 -0400 Subject: [PATCH 081/191] fix tests --- .../api/mutations/updateServiceVendorConfig.test.js | 9 ++++----- __test__/test_helpers.js | 5 ++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/__test__/server/api/mutations/updateServiceVendorConfig.test.js b/__test__/server/api/mutations/updateServiceVendorConfig.test.js index 455954853..015794b3c 100644 --- a/__test__/server/api/mutations/updateServiceVendorConfig.test.js +++ b/__test__/server/api/mutations/updateServiceVendorConfig.test.js @@ -34,10 +34,6 @@ describe("updateServiceVendorConfig", () => { user = await createUser(); const invite = await createInvite(); const createOrganizationResult = await createOrganization(user, invite); - console.log( - "updateServiceVendorconfig test beforeEach createOrgRsul", - createOrganizationResult - ); organization = createOrganizationResult.data.createOrganization; await ensureOrganizationTwilioWithMessagingService( createOrganizationResult @@ -177,10 +173,13 @@ describe("updateServiceVendorConfig", () => { [ expect.objectContaining({ id: 1 + }), + expect.objectContaining({ + obscureSensitiveInformation: true }) ] ]); - expect(gqlResult.data.updateServiceVendorConfig).toEqual( + expect(gqlResult.data.updateServiceVendorConfig.config).toEqual( expect.objectContaining(expectedCacheConfig) ); diff --git a/__test__/test_helpers.js b/__test__/test_helpers.js index 6425f3bda..7da6f9c52 100644 --- a/__test__/test_helpers.js +++ b/__test__/test_helpers.js @@ -250,7 +250,10 @@ export async function setTwilioAuth(user, organization) { organizationId: $organizationId serviceName: $serviceName config: $config - ) + ) { + id + config + } } `; From 0959285fac6bd9f387700a7ef24228af91ed8779 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Thu, 6 May 2021 19:14:10 -0400 Subject: [PATCH 082/191] cleanup console.logs --- __test__/server/api/organization.test.js | 1 - src/containers/Settings.jsx | 9 ++++----- src/extensions/service-managers/index.js | 1 - src/server/api/organization.js | 5 ----- src/workers/tasks.js | 10 +--------- 5 files changed, 5 insertions(+), 21 deletions(-) diff --git a/__test__/server/api/organization.test.js b/__test__/server/api/organization.test.js index 6a2075a14..a54411c7d 100644 --- a/__test__/server/api/organization.test.js +++ b/__test__/server/api/organization.test.js @@ -366,7 +366,6 @@ describe("organization", async () => { }); it("calls functions and returns the result", async () => { const result = await runGql(gqlQuery, variables, testAdminUser); - console.log("result", result); expect(result.data.organization.serviceVendor).toEqual({ ...fakeMetadata, config: fakeConfig diff --git a/src/containers/Settings.jsx b/src/containers/Settings.jsx index 1c9737950..d8bdda22e 100644 --- a/src/containers/Settings.jsx +++ b/src/containers/Settings.jsx @@ -187,13 +187,12 @@ class Settings extends React.Component { inlineStyles={inlineStyles} styles={styles} saveLabel={this.props.saveLabel} - onSubmit={updateData => { - console.log("onSubmit updateData: ", updateData); - return this.props.mutations.updateServiceManager( + onSubmit={updateData => + this.props.mutations.updateServiceManager( serviceManagerName, updateData - ); - }} + ) + } />
diff --git a/src/extensions/service-managers/index.js b/src/extensions/service-managers/index.js index 8e1900bb5..31e0e3d7e 100644 --- a/src/extensions/service-managers/index.js +++ b/src/extensions/service-managers/index.js @@ -40,7 +40,6 @@ export async function processServiceManagers( typeof m[funcName] === "function" && (!specificServiceManagerName || m.name === specificServiceManagerName) ); - console.log("service-managers.processServiceManagers", funkyManagers); const resultArray = []; // explicitly process these in order in case the order matters for (let i = 0, l = funkyManagers.length; i < l; i++) { diff --git a/src/server/api/organization.js b/src/server/api/organization.js index 780eccb84..ce7c14711 100644 --- a/src/server/api/organization.js +++ b/src/server/api/organization.js @@ -219,11 +219,6 @@ export const resolvers = { organization ); const serviceMetadata = getServiceMetadata(serviceName); - console.log( - "organization.messageService", - serviceName, - serviceMetadata - ); return { id: `org${organization.id}-${serviceName}`, ...serviceMetadata, diff --git a/src/workers/tasks.js b/src/workers/tasks.js index 4288f3ac1..c63629506 100644 --- a/src/workers/tasks.js +++ b/src/workers/tasks.js @@ -19,18 +19,10 @@ const serviceManagerTrigger = async ({ organizationId, data }) => { - console.log( - "serviceManagerTrigger", - functionName, - organizationId, - Object.keys(data) - ); - let organization, campaign; + let organization; if (organizationId) { organization = await cacheableData.organization.load(organizationId); - console.log("serviceManagerTrigger org", organization.name); } - await processServiceManagers(functionName, organization, data); }; From 9f1a0c65d1c00d88ea2798a90efbd4b691817546 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Fri, 7 May 2021 13:47:56 -0400 Subject: [PATCH 083/191] service-managers in AdminCamapignEdit --- src/components/CampaignServiceManagers.jsx | 82 +++++++++++++++++++ src/containers/AdminCampaignEdit.jsx | 53 ++++++++++++ src/containers/AdminCampaignStats.jsx | 2 +- src/containers/Settings.jsx | 1 - .../test-fake-example/index.js | 46 +++++++---- .../test-fake-example/react-component.js | 56 +++++++++++++ .../api/mutations/updateServiceManager.js | 81 ++++++++++++++++++ .../models/cacheable_queries/message.js | 32 ++++++++ 8 files changed, 333 insertions(+), 20 deletions(-) create mode 100644 src/components/CampaignServiceManagers.jsx create mode 100644 src/server/api/mutations/updateServiceManager.js diff --git a/src/components/CampaignServiceManagers.jsx b/src/components/CampaignServiceManagers.jsx new file mode 100644 index 000000000..ad9c825e5 --- /dev/null +++ b/src/components/CampaignServiceManagers.jsx @@ -0,0 +1,82 @@ +/* eslint no-console: 0 */ +import type from "prop-types"; +import React from "react"; +import gql from "graphql-tag"; +import GSForm from "../components/forms/GSForm"; +import Form from "react-formal"; +import Dialog from "material-ui/Dialog"; +import GSSubmitButton from "../components/forms/GSSubmitButton"; +import FlatButton from "material-ui/FlatButton"; +import RaisedButton from "material-ui/RaisedButton"; +import * as yup from "yup"; +import { Card, CardText, CardActions, CardHeader } from "material-ui/Card"; +import { StyleSheet, css } from "aphrodite"; +import theme from "../styles/theme"; +import Toggle from "material-ui/Toggle"; +import moment from "moment"; +import CampaignTexterUIForm from "../components/CampaignTexterUIForm"; +import OrganizationFeatureSettings from "../components/OrganizationFeatureSettings"; +import { getServiceVendorComponent } from "../extensions/service-vendors/components"; +import { getServiceManagerComponent } from "../extensions/service-managers/components"; +import GSTextField from "../components/forms/GSTextField"; + +export class CampaignServiceVendors extends React.Component { + static propTypes = { + formValues: type.object, + onChange: type.func, + customFields: type.array, + saveLabel: type.string, + campaign: type.object, + onSubmit: type.func, + saveDisabled: type.bool, + isStarted: type.bool + }; + + render() { + const { campaign } = this.props; + if (!campaign.serviceManagers.length) { + return null; + } + const allFullyConfigured = campaign.serviceManagers + .map(sm => sm.fullyConfigured !== false) + .reduce((a, b) => a && b, true); + return ( +
+ {campaign.serviceManagers.map(sm => { + const ServiceManagerComp = getServiceManagerComponent( + sm.name, + "CampaignConfig" + ); + const serviceManagerName = sm.name; + return ( + + + + + this.props.onSubmit(serviceManagerName, updateData) + } + /> + + + ); + })} +
+ ); + } +} + +export default CampaignServiceVendors; diff --git a/src/containers/AdminCampaignEdit.jsx b/src/containers/AdminCampaignEdit.jsx index bdaa9a63b..55e4b664a 100644 --- a/src/containers/AdminCampaignEdit.jsx +++ b/src/containers/AdminCampaignEdit.jsx @@ -23,6 +23,7 @@ import CampaignCannedResponsesForm from "../components/CampaignCannedResponsesFo import CampaignDynamicAssignmentForm from "../components/CampaignDynamicAssignmentForm"; import CampaignTexterUIForm from "../components/CampaignTexterUIForm"; import CampaignPhoneNumbersForm from "../components/CampaignPhoneNumbersForm"; +import CampaignServiceManagers from "../components/CampaignServiceManagers"; import { dataTest, camelCase } from "../lib/attributes"; import CampaignTextingHoursForm from "../components/CampaignTextingHoursForm"; import { css } from "aphrodite"; @@ -108,8 +109,10 @@ const campaignInfoFragment = ` serviceManagers { id name + displayName supportsOrgConfig data + fullyConfigured } useOwnMessagingService messageserviceSid @@ -532,6 +535,29 @@ export class AdminCampaignEdit extends React.Component { expandableBySuperVolunteers: false } ]; + if ( + this.props.campaignData.campaign.serviceManagers && + this.props.campaignData.campaign.serviceManagers.length + ) { + finalSections.push({ + title: "Service Management", + content: CampaignServiceManagers, + keys: [], + checkCompleted: () => + // fullyConfigured can be true or null, but if false, then it blocks + this.props.campaignData.campaign.serviceManagers + .map(sm => sm.fullyConfigured !== false) + .reduce((a, b) => a && b), + blocksStarting: true, + expandAfterCampaignStarts: true, + expandableBySuperVolunteers: false, + extraProps: { + campaign: this.props.campaignData.campaign, + organization: this.props.organizationData.organization, + onSubmit: this.props.mutations.updateServiceManager + } + }); + } if (window.EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE) { finalSections.push({ title: "Messaging Service", @@ -1084,6 +1110,33 @@ const mutations = { campaignId, url } + }), + updateServiceManager: ownProps => (serviceManagerName, updateData) => ({ + mutation: gql` + mutation updateServiceManager( + $organizationId: String! + $campaignId: String! + $serviceManagerName: String! + $updateData: JSON! + ) { + updateServiceManager( + organizationId: $organizationId + campaignId: $campaignId + serviceManagerName: $serviceManagerName + updateData: $updateData + ) { + id + data + fullyConfigured + } + } + `, + variables: { + organizationId: ownProps.organizationData.organization.id, + campaignId: ownProps.campaignData.campaign.id, + serviceManagerName, + updateData + } }) }; diff --git a/src/containers/AdminCampaignStats.jsx b/src/containers/AdminCampaignStats.jsx index 549df5236..6b188cc40 100644 --- a/src/containers/AdminCampaignStats.jsx +++ b/src/containers/AdminCampaignStats.jsx @@ -500,7 +500,7 @@ const queries = { serviceManagers(fromCampaignStatsPage: $fromCampaignStatsPage) { id name - supportsCampaignConfig + displayName data } } diff --git a/src/containers/Settings.jsx b/src/containers/Settings.jsx index d8bdda22e..b4f8c0142 100644 --- a/src/containers/Settings.jsx +++ b/src/containers/Settings.jsx @@ -180,7 +180,6 @@ class Settings extends React.Component { }} /> -
FOO BAR {sm.name}
", message: ""} -} + campaignContact, + lookup +}) {} export async function getCampaignData({ organization, @@ -49,12 +44,20 @@ export async function getCampaignData({ }) { // MUST NOT RETURN SECRETS! // called both from edit and stats contexts: editMode==true for edit page - return { - data: { - foo: "bar" - }, - fullyConfigured: true - }; + if (fromCampaignStatsPage) { + return { + data: { + foo: "statsPage Info!!!" + } + }; + } else { + return { + data: { + foo: "bar" + }, + fullyConfigured: campaign.is_started + }; + } } export async function onCampaignUpdateSignal({ @@ -63,7 +66,14 @@ export async function onCampaignUpdateSignal({ user, updateData, fromCampaignStatsPage -}) {} +}) { + return { + data: { + foo: "xyz" + }, + fullyConfigured: true + }; +} export async function onCampaignContactLoad({ organization, diff --git a/src/extensions/service-managers/test-fake-example/react-component.js b/src/extensions/service-managers/test-fake-example/react-component.js index 615a30921..b31a75d4c 100644 --- a/src/extensions/service-managers/test-fake-example/react-component.js +++ b/src/extensions/service-managers/test-fake-example/react-component.js @@ -64,3 +64,59 @@ OrgConfig.propTypes = { saveLabel: PropTypes.string, onSubmit: PropTypes.func }; + +export class CampaignConfig extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + console.log("testfakedata CampaignConfig", this.props); + const formSchema = yup.object({ + savedText: yup + .string() + .nullable() + .max(64) + }); + return ( +
+ THIS IS TEST_FAKE_DATA + {!this.props.campaign.isStarted ? ( + { + console.log("onSubmit", x); + this.props.onSubmit(x); + }} + > + + + + ) : ( +
+ Campaign is now started! {this.props.serviceManagerInfo.data.foo} +
+ )} +
+ ); + } +} + +CampaignConfig.propTypes = { + user: PropTypes.object, + campaign: PropTypes.object, + serviceManagerInfo: PropTypes.object, + saveLabel: PropTypes.string, + onSubmit: PropTypes.func +}; diff --git a/src/server/api/mutations/updateServiceManager.js b/src/server/api/mutations/updateServiceManager.js new file mode 100644 index 000000000..16d2beca7 --- /dev/null +++ b/src/server/api/mutations/updateServiceManager.js @@ -0,0 +1,81 @@ +import { GraphQLError } from "graphql/error"; +import { getConfig } from "../../../server/api/lib/config"; +import { cacheableData } from "../../../server/models"; +import { processServiceManagers } from "../../../extensions/service-managers"; +import { accessRequired } from "../errors"; + +export const updateServiceManager = async ( + _, + { + organizationId, + serviceManagerName, + updateData, + campaignId, + fromCampaignStatsPage + }, + { user } +) => { + const organization = await cacheableData.organization.load(organizationId); + let campaign; + let result = {}; + if (campaignId) { + // FUTURE: maybe with specific metadata, this could be made lower + // which could be useful in complement to texter-sideboxes + await accessRequired(user, organizationId, "SUPERVOLUNTEER", true); + campaign = await cacheableData.campaign.load(campaignId); + const response = await processServiceManagers( + "onCampaignUpdateSignal", + organization, + { organization, campaign, user, updateData, fromCampaignStatsPage }, + serviceManagerName + ); + if (response && response.length && response[0]) { + result = response[0]; + } + } else { + // organization + await accessRequired(user, organizationId, "OWNER", true); + const response = await processServiceManagers( + "onOrganizationUpdateSignal", + organization, + { organization, user, updateData }, + serviceManagerName + ); + console.log("updateServiceManager organization", response); + if (response && response.length && response[0]) { + result = response[0]; + } + } + console.log("updateServiceManager", result); + return { + id: `${serviceManagerName}-org${organizationId}-${campaignId || ""}${ + fromCampaignStatsPage ? "stats" : "" + }`, + name: serviceManagerName, + organization, + campaign, + // defaults for result to override + data: null, + fullyConfigured: null, + ...result + }; +}; + +export const getServiceVendorConfig = async ( + serviceName, + organization, + options = {} +) => { + const getServiceConfig = exports.tryGetFunctionFromService( + serviceName, + "getServiceConfig" + ); + if (!getServiceConfig) { + return null; + } + const configKey = exports.getConfigKey(serviceName); + const config = getConfig(configKey, organization, { + onlyLocal: options.restrictToOrgFeatures + }); + return getServiceConfig(config, organization, options); +}; diff --git a/src/server/models/cacheable_queries/message.js b/src/server/models/cacheable_queries/message.js index 3c1dce1fc..7d699e675 100644 --- a/src/server/models/cacheable_queries/message.js +++ b/src/server/models/cacheable_queries/message.js @@ -1,8 +1,14 @@ import { r, Message } from "../../models"; import campaignCache from "./campaign"; import campaignContactCache from "./campaign-contact"; +import orgCache from "./organization"; import organizationContactCache from "./organization-contact"; import { getMessageHandlers } from "../../../extensions/message-handlers"; +import { + serviceManagersHaveImplementation, + processServiceManagers +} from "../../../extensions/service-managers"; + // QUEUE // messages- // Expiration: 24 hours after last message added @@ -179,6 +185,32 @@ const deliveryReport = async ({ .limit(1) .update(changes); + if (serviceManagersHaveImplementation("onDeliveryReport")) { + lookup = + lookup || + (await campaignContactCache.lookupByCell( + contactNumber, + service || "", + messageServiceSid, + userNumber + )); + const campaignContact = await campaignContactCache.load( + lookup.campaign_contact_id + ); + const organizationId = await campaignContactCache.orgId(campaignContact); + const organization = await orgCache.load(organizationId); + await processServiceManagers("onDeliveryReport", organization, { + campaignContact, + lookup, + contactNumber, + userNumber, + messageSid, + service, + messageServiceSid, + newStatus, + errorCode + }); + } // TODO: move the below into a test for service-strategies if there are onDeliveryReport impls // which uses campaignContactCache.lookupByCell above if ( From a99c780f1b593e448513bc3f76777b73203ada75 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Mon, 10 May 2021 12:14:46 -0400 Subject: [PATCH 084/191] service-managers events: onCampaignArchive, onCampaignUnarchive, .... --- src/api/service.js | 1 + src/components/CampaignServiceManagers.jsx | 36 +++++++++------ src/containers/AdminCampaignEdit.jsx | 3 +- src/containers/AdminCampaignStats.jsx | 46 ++++++++++++++++++- src/containers/AdminPhoneNumberInventory.js | 14 +++--- .../test-fake-example/index.js | 24 ++++++++-- .../test-fake-example/react-component.js | 44 ++++++++++++++++++ src/server/api/campaign.js | 1 + src/server/api/mutations/startCampaign.js | 12 +++++ .../api/mutations/updateServiceManager.js | 19 -------- .../mutations/updateServiceVendorConfig.js | 12 +++++ src/server/api/schema.js | 25 ++++++++++ 12 files changed, 189 insertions(+), 48 deletions(-) diff --git a/src/api/service.js b/src/api/service.js index c34deee48..5e391eb3b 100644 --- a/src/api/service.js +++ b/src/api/service.js @@ -16,6 +16,7 @@ export const schema = gql` supportsOrgConfig: Boolean! supportsCampaignConfig: Boolean! fullyConfigured: Boolean + unArchiveable: Boolean organization: Organization campaign: Campaign } diff --git a/src/components/CampaignServiceManagers.jsx b/src/components/CampaignServiceManagers.jsx index ad9c825e5..725aac553 100644 --- a/src/components/CampaignServiceManagers.jsx +++ b/src/components/CampaignServiceManagers.jsx @@ -27,13 +27,15 @@ export class CampaignServiceVendors extends React.Component { customFields: type.array, saveLabel: type.string, campaign: type.object, + organization: type.object, onSubmit: type.func, saveDisabled: type.bool, - isStarted: type.bool + isStarted: type.bool, + serviceManagerComponentName: type.string }; render() { - const { campaign } = this.props; + const { campaign, organization, serviceManagerComponentName } = this.props; if (!campaign.serviceManagers.length) { return null; } @@ -45,26 +47,32 @@ export class CampaignServiceVendors extends React.Component { {campaign.serviceManagers.map(sm => { const ServiceManagerComp = getServiceManagerComponent( sm.name, - "CampaignConfig" + serviceManagerComponentName ); const serviceManagerName = sm.name; + if (!ServiceManagerComp) { + return null; + } return ( - + {serviceManagerComponentName === "CampaignConfig" ? ( + + ) : null} this.props.onSubmit(serviceManagerName, updateData) diff --git a/src/containers/AdminCampaignEdit.jsx b/src/containers/AdminCampaignEdit.jsx index 55e4b664a..fe0751d2b 100644 --- a/src/containers/AdminCampaignEdit.jsx +++ b/src/containers/AdminCampaignEdit.jsx @@ -554,7 +554,8 @@ export class AdminCampaignEdit extends React.Component { extraProps: { campaign: this.props.campaignData.campaign, organization: this.props.organizationData.organization, - onSubmit: this.props.mutations.updateServiceManager + onSubmit: this.props.mutations.updateServiceManager, + serviceManagerComponentName: "CampaignConfig" } }); } diff --git a/src/containers/AdminCampaignStats.jsx b/src/containers/AdminCampaignStats.jsx index 6b188cc40..a6908a435 100644 --- a/src/containers/AdminCampaignStats.jsx +++ b/src/containers/AdminCampaignStats.jsx @@ -6,6 +6,7 @@ import { Card, CardTitle, CardText } from "material-ui/Card"; import LinearProgress from "material-ui/LinearProgress"; import TexterStats from "../components/TexterStats"; import OrganizationJoinLink from "../components/OrganizationJoinLink"; +import CampaignServiceManagers from "../components/CampaignServiceManagers"; import AdminCampaignCopy from "./AdminCampaignCopy"; import Snackbar from "material-ui/Snackbar"; import { withRouter, Link } from "react-router"; @@ -266,7 +267,12 @@ class AdminCampaignStats extends React.Component { campaign.isArchived ? ( sm.unArchiveable) + .reduce((a, b) => a && b) + } onClick={async () => await this.props.mutations.unarchiveCampaign( campaignId @@ -358,7 +364,12 @@ class AdminCampaignStats extends React.Component { campaignId={campaignId} /> ) : null} - +
@@ -502,6 +513,7 @@ const queries = { name displayName data + unArchiveable } } } @@ -597,6 +609,36 @@ const mutations = { `, variables: { campaignId }, refetchQueries: () => ["getOrganizationData"] + }), + updateServiceManager: ownProps => (serviceManagerName, updateData) => ({ + mutation: gql` + mutation updateServiceManager( + $organizationId: String! + $campaignId: String! + $serviceManagerName: String! + $updateData: JSON! + $fromCampaignStatsPage: Boolean + ) { + updateServiceManager( + organizationId: $organizationId + campaignId: $campaignId + serviceManagerName: $serviceManagerName + updateData: $updateData + fromCampaignStatsPage: $fromCampaignStatsPage + ) { + id + data + unArchiveable + } + } + `, + variables: { + organizationId: ownProps.organizationData.organization.id, + campaignId: ownProps.data.campaign.id, + serviceManagerName, + updateData, + fromCampaignStatsPage: true + } }) }; diff --git a/src/containers/AdminPhoneNumberInventory.js b/src/containers/AdminPhoneNumberInventory.js index dfc3ff00d..ddf6ea7c0 100644 --- a/src/containers/AdminPhoneNumberInventory.js +++ b/src/containers/AdminPhoneNumberInventory.js @@ -273,9 +273,9 @@ class AdminPhoneNumberInventory extends React.Component { } renderBuyNumbersForm() { - const messageService = this.props.data.organization.messageService; - const messageServiceName = messageService.name; - const messageServiceConfig = messageService.config || "{}"; + const service = this.props.data.organization.serviceVendor; + const serviceName = service.name; + const serviceConfig = service.config || "{}"; return ( - {messageServiceName === "twilio" && - messageServiceConfig.TWILIO_MESSAGE_SERVICE_SID && - messageServiceConfig.TWILIO_MESSAGE_SERVICE_SID.length > 0 && + {serviceName === "twilio" && + serviceConfig.TWILIO_MESSAGE_SERVICE_SID && + serviceConfig.TWILIO_MESSAGE_SERVICE_SID.length > 0 && !this.props.data.organization.campaignPhoneNumbersEnabled ? ( + THIS IS TEST_FAKE_DATA {this.props.serviceManagerInfo.data.foo} + {!this.props.campaign.isStarted ? ( + { + console.log("onSubmit", x); + this.props.onSubmit({ a: "b" }); + }} + > + + + ) : ( +
+ Campaign is now started! {this.props.serviceManagerInfo.data.foo} +
+ )} +
+ ); + } +} + +CampaignStats.propTypes = { + user: PropTypes.object, + campaign: PropTypes.object, + serviceManagerInfo: PropTypes.object, + saveLabel: PropTypes.string, + onSubmit: PropTypes.func +}; diff --git a/src/server/api/campaign.js b/src/server/api/campaign.js index a31a814bb..7329a4bc9 100644 --- a/src/server/api/campaign.js +++ b/src/server/api/campaign.js @@ -712,6 +712,7 @@ export const resolvers = { organization, // defaults fullyConfigured: null, + unArchiveable: null, data: null, ...r })); diff --git a/src/server/api/mutations/startCampaign.js b/src/server/api/mutations/startCampaign.js index a446593f7..32c06dbb3 100644 --- a/src/server/api/mutations/startCampaign.js +++ b/src/server/api/mutations/startCampaign.js @@ -2,6 +2,7 @@ import cacheableData from "../../models/cacheable_queries"; import { r } from "../../models"; import { accessRequired } from "../errors"; import { Notifications, sendUserNotification } from "../../notifications"; +import { serviceManagersHaveImplementation } from "../../../extensions/service-managers"; import * as twilio from "../../../extensions/service-vendors/twilio"; import { getConfig } from "../lib/config"; import { jobRunner } from "../../../extensions/job-runners"; @@ -70,5 +71,16 @@ export const startCampaign = async ( organization }); } + + if (serviceManagersHaveImplementation("onCampaignStart", organization)) { + await jobRunner.dispatchTask(Tasks.SERVICE_MANAGER_TRIGGER, { + functionName: "onCampaignStart", + organizationId: organization.id, + data: { + campaign: campaignRefreshed, + user + } + }); + } return campaignRefreshed; }; diff --git a/src/server/api/mutations/updateServiceManager.js b/src/server/api/mutations/updateServiceManager.js index 16d2beca7..241a43a1a 100644 --- a/src/server/api/mutations/updateServiceManager.js +++ b/src/server/api/mutations/updateServiceManager.js @@ -60,22 +60,3 @@ export const updateServiceManager = async ( ...result }; }; - -export const getServiceVendorConfig = async ( - serviceName, - organization, - options = {} -) => { - const getServiceConfig = exports.tryGetFunctionFromService( - serviceName, - "getServiceConfig" - ); - if (!getServiceConfig) { - return null; - } - const configKey = exports.getConfigKey(serviceName); - const config = getConfig(configKey, organization, { - onlyLocal: options.restrictToOrgFeatures - }); - return getServiceConfig(config, organization, options); -}; diff --git a/src/server/api/mutations/updateServiceVendorConfig.js b/src/server/api/mutations/updateServiceVendorConfig.js index 7bbbebfb4..619e60f36 100644 --- a/src/server/api/mutations/updateServiceVendorConfig.js +++ b/src/server/api/mutations/updateServiceVendorConfig.js @@ -4,6 +4,7 @@ import { getService, tryGetFunctionFromService } from "../../../extensions/service-vendors"; +import { processServiceManagers } from "../../../extensions/service-managers"; import { getConfig } from "../../../server/api/lib/config"; import orgCache from "../../models/cacheable_queries/organization"; import { accessRequired } from "../errors"; @@ -82,6 +83,17 @@ export const updateServiceVendorConfig = async ( await orgCache.clear(organization.id); const updatedOrganization = await orgCache.load(organization.id); + await processServiceManagers( + "onOrganizationServiceVendorSetup", + updatedOrganization, + { + user, + serviceName, + oldConfig: existingConfig, + newConfig + } + ); + return { id: `org${organization.id}-${serviceName}`, config: await orgCache.getMessageServiceConfig(updatedOrganization, { diff --git a/src/server/api/schema.js b/src/server/api/schema.js index d790ac118..6e1227895 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -38,6 +38,10 @@ import { import { resolvers as interactionStepResolvers } from "./interaction-step"; import { resolvers as inviteResolvers } from "./invite"; import { saveNewIncomingMessage } from "../../extensions/service-vendors/message-sending"; +import { + processServiceManagers, + serviceManagersHaveImplementation +} from "../../extensions/service-managers"; import { getConfig, getFeatures } from "./lib/config"; import { resolvers as messageResolvers } from "./message"; import { resolvers as optOutResolvers } from "./opt-out"; @@ -924,6 +928,17 @@ const rootMutations = { throw new Error("Cannot archive permanently archived campaign"); } campaign.is_archived = false; + const organization = await cacheableData.organization.load( + campaign.organization_id + ); + const serviceManagerResults = await processServiceManagers( + "onCampaignUnarchive", + organization, + { + campaign, + user + } + ); await campaign.save(); await cacheableData.campaign.clear(id); return campaign; @@ -934,6 +949,16 @@ const rootMutations = { campaign.is_archived = true; await campaign.save(); await cacheableData.campaign.clear(id); + if (serviceManagersHaveImplementation("onCampaignArchive")) { + await jobRunner.dispatchTask(Tasks.SERVICE_MANAGER_TRIGGER, { + functionName: "onCampaignArchive", + organizationId: campaign.organization_id, + data: { + campaign, + user + } + }); + } return campaign; }, archiveCampaigns: async (_, { ids }, { user, loaders }) => { From b9a69f28d5e9cac3e55d1be70f9f050b0bd19dd4 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Wed, 2 Jun 2021 10:37:08 -0400 Subject: [PATCH 085/191] service-managers: per-campaign-messageservices STUB todo: 1. remove old stuff 2. also support EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE (diff component) 3. auto-add SERVICE_MANAGERS when EXPERIMENTALS are set --- src/containers/AdminCampaignEdit.jsx | 1 + src/containers/AdminCampaignStats.jsx | 2 +- src/extensions/service-managers/index.js | 2 +- .../per-campaign-messageservices/index.js | 321 ++++++ .../react-component.js | 937 ++++++++++++++++++ .../test-fake-example/index.js | 2 + src/extensions/service-vendors/index.js | 12 +- .../service-vendors/twilio/index.js | 36 +- src/server/api/campaign.js | 1 + src/server/api/lib/owned-phone-number.js | 37 +- src/server/api/mutations/startCampaign.js | 86 +- .../api/mutations/updateServiceManager.js | 14 +- .../mutations/updateServiceVendorConfig.js | 22 +- src/server/api/organization.js | 25 +- src/server/api/schema.js | 37 +- src/workers/jobs.js | 107 -- src/workers/tasks.js | 48 +- 17 files changed, 1408 insertions(+), 282 deletions(-) create mode 100644 src/extensions/service-managers/per-campaign-messageservices/index.js create mode 100644 src/extensions/service-managers/per-campaign-messageservices/react-component.js diff --git a/src/containers/AdminCampaignEdit.jsx b/src/containers/AdminCampaignEdit.jsx index fe0751d2b..3802cb74f 100644 --- a/src/containers/AdminCampaignEdit.jsx +++ b/src/containers/AdminCampaignEdit.jsx @@ -577,6 +577,7 @@ export class AdminCampaignEdit extends React.Component { content: CampaignPhoneNumbersForm, keys: ["inventoryPhoneNumberCounts"], checkCompleted: () => { + // logic to move to the component itself or backend const { contactsCount, inventoryPhoneNumberCounts diff --git a/src/containers/AdminCampaignStats.jsx b/src/containers/AdminCampaignStats.jsx index a6908a435..4ed19cbb8 100644 --- a/src/containers/AdminCampaignStats.jsx +++ b/src/containers/AdminCampaignStats.jsx @@ -457,9 +457,9 @@ const queries = { id title isArchived - isArchivedPermanently joinToken useDynamicAssignment + isArchivedPermanently useOwnMessagingService messageserviceSid assignments(assignmentsFilter: $assignmentsFilter) { diff --git a/src/extensions/service-managers/index.js b/src/extensions/service-managers/index.js index 31e0e3d7e..dbcf0f460 100644 --- a/src/extensions/service-managers/index.js +++ b/src/extensions/service-managers/index.js @@ -50,7 +50,7 @@ export async function processServiceManagers( // NOTE: some methods pass a shared modifiable object, e.g. 'saveData' // that might be modified in-place, rather than the resultArray // being important. - return resultArray; + return resultArray.reduce((a, b) => Object.assign(a, b), {}); } export async function getServiceManagerData( diff --git a/src/extensions/service-managers/per-campaign-messageservices/index.js b/src/extensions/service-managers/per-campaign-messageservices/index.js new file mode 100644 index 000000000..e37442f13 --- /dev/null +++ b/src/extensions/service-managers/per-campaign-messageservices/index.js @@ -0,0 +1,321 @@ +// legacy env vars: CONTACTS_PER_PHONE_NUMBER, EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE +// per-campaign-messageservice +// Implements: +// - onMessageSend: to update/change the message.messageservice_sid +// - onCampaignStart: to create the message service and allocate numbers to it +// - getCampaignData: provide the react component to allocate numbers +// - onCampaignUpdateSignal: to remove and deallocate the message service after archive +// - onOrganizationServiceVendorSetup: to disable requiring org-level messageservice setup +// - onVendorServiceFullyConfigured: to disable requiring org-level messageservice setup + +// FUTURE: ?org configure to enable it? +// TODO: how should AdminPhoneNumberBuying be affected -- can/should it 'steal' the message +// TODO: maybe it should remove/block the org-level messageservice_sid from being set? +// TODO: should it capture the response from twilio and then mark fully configured even if messageservice_sid isn't set? + +import { r, cacheableData } from "../../../server/models"; +import ownedPhoneNumber from "../../../server/api/lib/owned-phone-number"; +import { accessRequired } from "../../../server/api/errors"; +import { getConfig } from "../../../server/api/lib/config"; +import { getServiceNameFromOrganization } from "../../service-vendors"; +import * as twilio from "../../service-vendors/twilio"; +import { camelizeKeys } from "humps"; +import usAreaCodes from "us-area-codes"; + +export const name = "per-campaign-messageservices"; + +export const metadata = () => ({ + // set canSpendMoney=true, if this extension can lead to (additional) money being spent + // if it can, which operations below can trigger money being spent? + displayName: "Per-campaign Message Service", + description: + "Twilio has a cap of 250 numbers per-message service. This manages new message-services per-campaign so that the total numbers used (and thus contacts) can be multiplied by a number of simultaneous campaigns.", + canSpendMoney: true, + moneySpendingOperations: ["onCampaignStart"], + supportsOrgConfig: false, + supportsCampaignConfig: true +}); + +export async function onMessageSend({ + message, + contact, + organization, + campaign +}) { + if (campaign && campaign.messageservice_sid) { + return { + messageservice_sid: campaign.messageservice_sid + }; + } +} + +const _contactsPerPhoneNumber = organization => ({ + contactsPerPhoneNumber: Number( + getConfig("CONTACTS_PER_PHONE_NUMBER", organization) || 200 + ) +}); + +const _editCampaignData = async (organization, campaign) => { + // 1. inventoryPhoneNumberCounts (for a campaign) + const counts = await ownedPhoneNumber.listCampaignNumbers(campaign.id); + const inventoryPhoneNumberCounts = camelizeKeys(counts); + // 2. contactsAreaCodeCounts + const areaCodes = await r + .knex("campaign_contact") + .select( + r.knex.raw(` + substring(cell, 3, 3) AS area_code, + count(*) + `) + ) + .where({ campaign_id: campaign.id }) + .groupBy(1); + + const contactsAreaCodeCounts = areaCodes.map(data => ({ + areaCode: data.area_code, + state: usAreaCodes.get(Number(data.area_code)), + count: parseInt(data.count, 10) + })); + // 2. phoneNumberCounts (for organization) + const phoneNumberCounts = await ownedPhoneNumber.listOrganizationCounts( + organization + ); + // 3. fullyConfigured + const contactsPerNum = _contactsPerPhoneNumber(organization); + const numbersReserved = (inventoryPhoneNumberCounts || []).reduce( + (acc, entry) => acc + entry.count, + 0 + ); + const numbersNeeded = Math.ceil( + (campaign.contactsCount || 0) / contactsPerNum.contactsPerPhoneNumber + ); + return { + data: { + inventoryPhoneNumberCounts, + contactsAreaCodeCounts, + phoneNumberCounts, + messageserviceSid: campaign.messageservice_sid || null, + useOwnMessagingService: campaign.use_own_messaging_service, + ...contactsPerNum + }, + fullyConfigured: + Boolean(campaign.messageservice_sid) || + (numbersReserved >= numbersNeeded && false), // ?? TODO: only when campaignPhoneNumbersEnabled=EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS + // and NOT EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE + unArchiveable: + !campaign.use_own_messaging_service || campaign.messageservice_sid + }; +}; + +export async function getCampaignData({ + organization, + campaign, + user, + loaders, + fromCampaignStatsPage +}) { + // MUST NOT RETURN SECRETS! + // called both from edit and stats contexts: editMode==true for edit page + if (fromCampaignStatsPage) { + // STATS: campaign.messageservice_sid (enabled) + return { + data: { + useOwnMessagingService: campaign.use_own_messaging_service, + messageserviceSid: campaign.messageservice_sid || null, + ..._contactsPerPhoneNumber(organization) + }, + unArchiveable: + !campaign.use_own_messaging_service || campaign.messageservice_sid + }; + } else { + // EDIT + return await _editCampaignData(organization, campaign); + } +} + +export async function onCampaignUpdateSignal({ + organization, + campaign, + user, + updateData, + fromCampaignStatsPage +}) { + // TODO: + // 1. receive/process releaseCampaignNumbers button (also widget) -- from stats page + // 2. receive CampaignPhoneNumbers form (replace action on campaign save) + // inventoryPhoneNumberCounts in schema.js + // fullyConfigured ~= campaign.messageservice_sid && owned_phone_numbers + await accessRequired(user, campaign.organization_id, "ADMIN"); + const serviceName = getServiceNameFromOrganization(organization); + + if (updateData.releaseCampaignNumbers) { + if (!campaign.use_own_messaging_service) { + throw new Error( + "Campaign is not using its own messaging service, cannot release numbers" + ); + } + if (!campaign.messageservice_sid) { + throw new Error( + "Campaign no longer has a message service associated with it" + ); + } + + if (serviceName === "twilio") { + const shouldRetainServices = getConfig( + "CAMPAIGN_PHONES_RETAIN_MESSAGING_SERVICES", + organization, + { truthy: 1 } + ); + if (shouldRetainServices) { + // retain messaging services for analytics, just clear phones + await twilio.clearMessagingServicePhones( + organization, + campaign.messageservice_sid + ); + } else { + await twilio.deleteMessagingService( + organization, + campaign.messageservice_sid + ); + } + } + + await ownedPhoneNumber.releaseCampaignNumbers(campaign.id, r.knex); + await cacheableData.campaign.clear(campaign.id); + } else if (updateData.inventoryPhoneNumberCounts) { + if (campaign.is_started) { + throw new Error( + "Cannot update phone numbers once a campaign has started" + ); + } + const phoneCounts = updateData.inventoryPhoneNumberCounts; + await r.knex.transaction(async trx => { + await ownedPhoneNumber.releaseCampaignNumbers(campaign.id, trx); + for (const pc of phoneCounts) { + if (pc.count) { + await ownedPhoneNumber.allocateCampaignNumbers( + { + organizationId: organization.id, + campaignId: campaign.id, + areaCode: pc.areaCode, + amount: pc.count + }, + trx + ); + } + } + }); + } + + return await _editCampaignData(organization, campaign); +} + +// TODO: react-component.js +// components/CampaignPhoneNumbersForm.jsx +// move containers/AdminCampaignStats:: showReleaseNumbers +async function onVendorServiceFullyConfigured({ organization, serviceName }) { + return { + skipOrgMessageService: true + }; +} + +async function onOrganizationServiceVendorSetup({ + organization, + newConfig, + serviceName +}) { + return { + skipOrgMessageService: true + }; +} + +// Prepares a messaging service with owned number for the campaign +async function prepareTwilioCampaign(campaign, organization, trx) { + const ts = Math.floor(new Date() / 1000); + const baseUrl = getConfig("BASE_URL", organization); + const friendlyName = `Campaign ${campaign.id}: ${campaign.organization_id}-${ts} [${baseUrl}]`; + let msgSrvSid = campaign.messageservice_sid; + if (!campaign.messageservice_sid) { + const messagingService = await twilio.createMessagingService( + organization, + friendlyName + ); + msgSrvSid = messagingService.sid; + if (!msgSrvSid) { + throw new Error("Failed to create messaging service!"); + } + } + const phoneSids = ( + await trx("owned_phone_number") + .select("service_id") + .where({ + organization_id: campaign.organization_id, + service: "twilio", + allocated_to: "campaign", + allocated_to_id: campaign.id.toString() + }) + ).map(row => row.service_id); + console.log(`Transferring ${phoneSids.length} numbers to ${msgSrvSid}`); + try { + await twilio.addNumbersToMessagingService( + organization, + phoneSids, + msgSrvSid + ); + } catch (e) { + console.error("Failed to add numbers to messaging service", e); + await twilio.deleteMessagingService(organization, msgSrvSid); + throw new Error("Failed to add numbers to messaging service"); + } + return msgSrvSid; +} + +async function onCampaignStart({ organization, campaign, user }) { + try { + await r.knex.transaction(async trx => { + const campaignTrx = await trx("campaign") + .where("id", campaign.id) + // PG only: lock this campaign while starting, making this job idempotent + .forUpdate() + .first(); + if (campaignTrx.is_started) { + throw new Error("Campaign already started"); + } + + const serviceName = getServiceNameFromOrganization(organization); + + let messagingServiceSid; + if (serviceName === "twilio") { + messagingServiceSid = await prepareTwilioCampaign( + campaignTrx, + organization, + trx + ); + } else if (serviceName === "fakeservice") { + // simulate some latency + await new Promise(resolve => setTimeout(resolve, 1000)); + messagingServiceSid = "FAKEMESSAGINGSERVICE"; + } else { + throw new Error( + `Campaign phone numbers are not supported for service ${serviceName}` + ); + } + + await trx("campaign") + .where("id", campaign.id) + .update({ + is_started: true, + use_own_messaging_service: true, + messageservice_sid: messagingServiceSid + }); + }); + } catch (e) { + console.error( + `per-campaign-messageservices failed to start campaign: ${e.message}`, + e + ); + throw new Error( + `per-campaign-messageservices failed to start create messageservice: ${e.message}` + ); + } +} diff --git a/src/extensions/service-managers/per-campaign-messageservices/react-component.js b/src/extensions/service-managers/per-campaign-messageservices/react-component.js new file mode 100644 index 000000000..730529368 --- /dev/null +++ b/src/extensions/service-managers/per-campaign-messageservices/react-component.js @@ -0,0 +1,937 @@ +import React from "react"; +import type from "prop-types"; +import { StyleSheet, css } from "aphrodite"; +import _ from "lodash"; +import GSForm from "../../../components/forms/GSForm"; +import GSSubmitButton from "../../../components/forms/GSSubmitButton"; +import * as yup from "yup"; +import Form from "react-formal"; +import CampaignFormSectionHeading from "../../../components/CampaignFormSectionHeading"; +import { ListItem, List } from "material-ui/List"; +import AutoComplete from "material-ui/AutoComplete"; +import RaisedButton from "material-ui/RaisedButton"; +import FlatButton from "material-ui/FlatButton"; +import Checkbox from "material-ui/Checkbox"; +import IconButton from "material-ui/IconButton/IconButton"; +import AddIcon from "material-ui/svg-icons/content/add-circle"; +import RemoveIcon from "material-ui/svg-icons/content/remove-circle"; +import LoadingIndicator from "../../../components/LoadingIndicator"; +import theme from "../../../styles/theme"; + +// import { dataTest } from "../lib/attributes"; + +/* eslint-disable no-nested-ternary */ + +const maxNumbersPerCampaign = 400; + +const styles = StyleSheet.create({ + container: { + border: `1px solid ${theme.colors.lightGray}`, + borderRadius: 8 + }, + removeButton: { + width: 50 + }, + headerContainer: { + display: "flex", + alignItems: "center", + borderBottom: `1px solid ${theme.colors.lightGray}`, + marginBottom: 0, + padding: 10 + }, + input: { + width: 50, + paddingLeft: 0, + paddingRight: 0, + marginRight: 10, + marginTop: "auto", + marginBottom: "auto", + display: "inline-block" + }, + errorMessage: { + margin: "10px 0px", + color: theme.colors.red + } +}); + +const inlineStyles = { + autocomplete: { + marginBottom: 24, + width: "100%" + }, + header: { + ...theme.text.header + } +}; + +export class CampaignConfig extends React.Component { + static propTypes = { + user: type.object, + campaign: type.object, + serviceManagerInfo: type.object, + saveLabel: type.string, + onSubmit: type.func, + + formValues: type.object, + onChange: type.func, + phoneNumberCounts: type.array, + inventoryCounts: type.array, + isStarted: type.bool, + contactsAreaCodeCounts: type.array + }; + + constructor(props) { + super(props); + this.state = { + isRendering: true, + searchText: "", + showOnlySelected: false, + error: "", + suppressedAreaCodes: [], + inventoryPhoneNumberCounts: + props.serviceManagerInfo.data.inventoryPhoneNumberCounts + }; + } + + componentDidMount() { + this.setSuppressedAreaCodes(); + + setTimeout(() => { + this.setState({ + isRendering: false + }); + }, 1000); + } + + handleChange = changes => { + this.setState({ + ...changes, + hasChanged: true + }); + }; + + setSuppressedAreaCodes = () => { + const { isStarted } = this.props.campaign; + const { + phoneNumberCounts, + contactsAreaCodeCounts, + contactsPerPhoneNumber, + inventoryPhoneNumberCounts: inventoryCounts + } = this.props.serviceManagerInfo.data; + + /* okay this is wonky, but twilio confirmed that if you have + Area Code Geo-Match enabled, it will always choose the phone + with the matching area code. this means if you have a list + of 50k contacts all in a 917 area code but only one phone + in 917, they will all send from that phone. they will also be + throttled at 1message per second. not good. so we need to not + use matching area codes if there are too few to cover our list */ + const suppressedAreaCodes = contactsAreaCodeCounts.reduce( + (arr, contacts) => { + const needed = Math.ceil(contacts.count / contactsPerPhoneNumber); + let { availableCount } = + phoneNumberCounts.find( + phones => + phones.areaCode === contacts.areaCode && phones.availableCount + ) || {}; + + // if inventory is saved, add that to the available + const inventory = inventoryCounts.find( + reserved => reserved.areaCode === contacts.areaCode + ) || { count: 0 }; + + availableCount += inventory.count; + + if (availableCount < needed) arr.push(contacts.areaCode); + return arr; + }, + [] + ); + + this.setState({ + suppressedAreaCodes, + showOnlySelected: isStarted + }); + }; + + getTotalNumberCount = numbers => + numbers.reduce((total, entry) => total + entry.count, 0); + + getNumbersCount = count => (count === 1 ? "number" : "numbers"); + + formSchema = yup.object({ + areaCode: yup.string(), // TODO: validate + count: yup.number() + }); + + renderPhoneNumbers() { + const { + isRendering, + searchText, + showOnlySelected, + suppressedAreaCodes, + inventoryPhoneNumberCounts: reservedNumbers + } = this.state; + const { isStarted, contactsCount } = this.props.campaign; + const { + contactsPerPhoneNumber, + phoneNumberCounts + } = this.props.serviceManagerInfo.data; + const assignedNumberCount = this.getTotalNumberCount(reservedNumbers); + const numbersNeeded = Math.ceil(contactsCount / contactsPerPhoneNumber); + + /* need to add selected phone counts to available phones; + if navigated away after initial selection, the selected + area codes will be removed from the counts passed down (from org) */ + let areaCodes = _.orderBy( + phoneNumberCounts + .map(phoneNumber => { + const foundReserved = reservedNumbers.find( + reserved => reserved.areaCode === phoneNumber.areaCode + ) || { count: 0 }; + + return { + ...phoneNumber, + allocatedCount: isStarted + ? foundReserved.count + : phoneNumber.allocatedCount + foundReserved.count, + availableCount: phoneNumber.availableCount + foundReserved.count + }; + }) + .filter(phoneNumber => (isStarted ? phoneNumber.allocatedCount : true)), + ["state", "areaCode"] + ); + if (showOnlySelected) { + areaCodes = areaCodes.filter(item => + reservedNumbers.find(reserved => reserved.areaCode === item.areaCode) + ); + } + + if (searchText) { + if (!isNaN(searchText) && searchText.length <= 3) { + const foundAreaCode = areaCodes.find(({ areaCode }) => + areaCode.includes(searchText) + ); + areaCodes = foundAreaCode ? [foundAreaCode] : []; + } else if (isNaN(searchText)) { + areaCodes = areaCodes.filter(({ state }) => + state.toLowerCase().includes(searchText.toLowerCase()) + ); + } + } + + const states = Array.from(new Set(areaCodes.map(({ state }) => state))); + const getAssignedCount = areaCode => { + const inventory = this.state.inventoryPhoneNumberCounts; + return ( + (inventory.find(item => item.areaCode === areaCode) || {}).count || 0 + ); + }; + + const assignAreaCode = areaCode => { + const inventory = this.state.inventoryPhoneNumberCounts; + const inventoryPhoneNumberCounts = inventory.find( + item => item.areaCode === areaCode + ) + ? inventory.map(item => + item.areaCode === areaCode + ? { ...item, count: item.count + 1 } + : item + ) + : [...inventory, { areaCode, count: 1 }]; + + this.handleChange({ inventoryPhoneNumberCounts }); + if (_.sumBy(inventoryPhoneNumberCounts, "count") === numbersNeeded) { + this.setState({ showOnlySelected: true }); + } + }; + + const unassignAreaCode = areaCode => { + const inventory = this.state.inventoryPhoneNumberCounts; + const inventoryPhoneNumberCounts = inventory + .map(item => + item.areaCode === areaCode ? { ...item, count: item.count - 1 } : item + ) + .filter(item => item.count); + + this.handleChange({ inventoryPhoneNumberCounts }); + + if (!inventoryPhoneNumberCounts.length && showOnlySelected) { + this.setState({ showOnlySelected: false }); + } + }; + + return ( + + {isRendering ? ( + + ) : ( + states.map(state => ( + areaCode.state === state) + .map(({ areaCode, availableCount }) => { + const assignedCount = getAssignedCount(areaCode); + const isSuppressed = suppressedAreaCodes.includes(areaCode); + return ( + + + {areaCode} + + + {`${assignedCount}${ + !isStarted ? ` / ${availableCount}` : "" + }`} + + + } + rightIconButton={ + !isStarted && + (!isSuppressed ? ( +
+ unassignAreaCode(areaCode)} + > + + + assignAreaCode(areaCode)} + > + + +
+ ) : ( +
+ Not Enough to Reserve +
+ )) + } + /> + ); + })} + /> + )) + )} +
+ ); + } + + renderSubtitle = () => { + const { contactsPerPhoneNumber } = this.props.serviceManagerInfo.data; + return ( +
+ Select the area codes you would like to use for your campaign. +
    +
  • Contact an admin if you need more numbers.
  • +
  • + You can only assign one phone number for every{" "} + {contactsPerPhoneNumber} contacts. +
  • +
  • + Auto-Reserve first tries to find an exact match on area code, +
    + then tries to find other area codes in the same state, +
    + finally falling back to randomly assigning remaining area codes. +
  • +
  • + When done texting and replying, you will need to archive the + campaign +
    + and release the phone numbers so other campaigns can use them. +
  • +
+
+ ); + }; + + renderSearch() { + const { isStarted } = this.props.campaign; + const { phoneNumberCounts } = this.props.serviceManagerInfo.data; + + if (phoneNumberCounts.length === 0) { + return ( +
No phone numbers available
+ ); + } + + const filter = (searchText, key) => + key === "allphoneNumbers" + ? true + : AutoComplete.caseInsensitiveFilter(searchText, key); + + const autocomplete = ( + this.setState({ searchText })} + searchText={this.state.searchText} + filter={filter} + hintText="Find State or Area Code" + name="areaCode" + label="Find State or Area Code" + dataSource={[]} + /> + ); + const showAutocomplete = !isStarted && phoneNumberCounts.length > 0; + return
{showAutocomplete ? autocomplete : ""}
; + } + + renderErrorMessage() { + const { error } = this.state; + return
{error}
; + } + + renderAreaCodeTable() { + const { inventoryPhoneNumberCounts: reservedNumbers } = this.state; + const assignedNumberCount = this.getTotalNumberCount(reservedNumbers); + const { isStarted, contactsCount } = this.props.campaign; + const { + inventoryPhoneNumberCounts: inventoryCounts, + contactsAreaCodeCounts, + contactsPerPhoneNumber + } = this.props.serviceManagerInfo.data; + const { isRendering, hasReset } = this.state; + const numbersNeeded = Math.ceil(contactsCount / contactsPerPhoneNumber); + let remaining = numbersNeeded - assignedNumberCount; + + const headerColor = + assignedNumberCount === numbersNeeded + ? theme.colors.darkBlue + : theme.colors.red; + + const autoAssignRemaining = () => { + let inventory = this.state.inventoryPhoneNumberCounts; + const { suppressedAreaCodes } = this.state; + + const availableAreaCodes = _.flatten( + this.props.serviceManagerInfo.data.phoneNumberCounts + .filter( + phoneNumber => + // see NOTE in setSuppressedAreaCodes + !suppressedAreaCodes.includes(phoneNumber.areaCode) + ) + .map(phoneNumber => { + /* until we save and navigate back and props.inventoryCounts + has values, the phoneNumberCounts will need to have the + "form state inventory" subtracted from the available count + UNLESS we've used RESET after saving, then ADD props inventory + */ + + let foundAllocated = inventory.find( + ({ areaCode }) => areaCode === phoneNumber.areaCode + ) || { count: 0 }; + + if (hasReset) { + foundAllocated = inventoryCounts.find( + ({ areaCode }) => areaCode === phoneNumber.areaCode + ) || { count: 0 }; + } + + const availableCount = + !inventoryCounts.length && !hasReset + ? phoneNumber.availableCount - foundAllocated.count + : hasReset + ? phoneNumber.availableCount + foundAllocated.count + : phoneNumber.availableCount; + + return Array.from(Array(availableCount)).map(() => ({ + areaCode: phoneNumber.areaCode, + state: phoneNumber.state + })); + }) + ); + + /* eslint-disable no-param-reassign */ + + const matchedFromContacts = _.orderBy( + contactsAreaCodeCounts, + ["count"], + ["desc"] + // prioritze assigning the area codes with the most contacts + ).reduce((obj, contacts) => { + const needed = Math.ceil(contacts.count / contactsPerPhoneNumber); + // ignore outlier (less than .5% coverage) area codes + if ((contactsCount / needed) * 100 < 1) return obj; + + let foundAvailable = availableAreaCodes.filter( + avail => avail.areaCode === contacts.areaCode + ); + + if (remaining < needed) { + /* if we can only select less than are needed for full + coverage on this areacode then we should select none. + see NOTE in setSuppressedAreaCodes */ + foundAvailable = []; + } + + if (!foundAvailable.length) { + // if no exact match, try to fall back to state match + foundAvailable = _.shuffle(availableAreaCodes) + .filter(avail => avail.state === contacts.state) + .slice(0, remaining); + } + + // if nothing found, skip to be randomly assigned + if (!foundAvailable.length) return obj; + + if (foundAvailable.length > needed) { + // if we've got more than needed, randomly pick them + foundAvailable = _.shuffle(foundAvailable).slice(0, needed); + // otherwise use them all! + } + + foundAvailable.forEach(avail => { + obj[avail.areaCode] = (obj[avail.areaCode] || 0) + 1; + }); + + // now remove these from available + foundAvailable.forEach(found => { + availableAreaCodes.splice( + availableAreaCodes.findIndex( + avail => avail.areaCode === found.areaCode + ), + 1 + ); + }); + + remaining -= foundAvailable.length; + + return obj; + }, {}); + + let randomSample = {}; + + if (remaining) { + randomSample = _.sampleSize(availableAreaCodes, remaining).reduce( + (obj, sample) => { + if (matchedFromContacts[sample.areaCode]) { + matchedFromContacts[sample.areaCode] += 1; + } else { + obj[sample.areaCode] = (obj[sample.areaCode] || 0) + 1; + } + return obj; + }, + {} + ); + } + + // if these area codes are already selected, add the new counts + inventory = inventory.map(inventoryItem => { + let count = inventoryItem.count; + if (randomSample[inventoryItem.areaCode]) { + count += randomSample[inventoryItem.areaCode]; + delete randomSample[inventoryItem.areaCode]; + } + + if (matchedFromContacts[inventoryItem.areaCode]) { + count += matchedFromContacts[inventoryItem.areaCode]; + delete matchedFromContacts[inventoryItem.areaCode]; + } + + return { + ...inventoryItem, + count + }; + }); + + this.handleChange({ + inventoryPhoneNumberCounts: [ + ...inventory, + ...Object.entries({ + ...matchedFromContacts, + ...randomSample + }).map(([areaCode, count]) => ({ + areaCode, + count + })) + ] + }); + + this.setState({ showOnlySelected: true }); + }; + + const resetReserved = () => { + /* eslint-disable no-alert */ + const confirmed = confirm( + "Are you sure you want to clear your selected phones?" + ); + if (confirmed) { + this.handleChange({ inventoryPhoneNumberCounts: [] }); + this.setState({ hasReset: true, showOnlySelected: false }); + } + }; + + return ( +
+
+
+
+ + {"Reserved phone numbers: "} + {`${assignedNumberCount}/${numbersNeeded}`} + + + {!isStarted && ( + resetReserved()} + /> + )} +
+ {!isStarted && ( +
+ { + this.setState({ isRendering: true }); + setTimeout(() => autoAssignRemaining()); + setTimeout(() => { + this.setState({ isRendering: false }); + }, 500); + }} + /> + + { + this.setState(({ showOnlySelected }) => ({ + showOnlySelected: !showOnlySelected, + searchText: "" + })); + }} + /> +
+ )} +
+
+ {this.renderPhoneNumbers()} +
+ ); + } + + renderContactsAreaCodesTable() { + const { isRendering, searchText } = this.state; + const { contactsCount } = this.props.campaign; + const { + contactsAreaCodeCounts, + contactsPerPhoneNumber + } = this.props.serviceManagerInfo.data; + const areaCodes = _.orderBy( + contactsAreaCodeCounts, + ["count", "state", "areaCode"], + ["desc"] + ); + const states = Object.entries( + areaCodes.reduce((obj, item) => { + return { + ...obj, + [item.state]: (obj[item.state] || 0) + item.count + }; + }, {}) + ).reduce((arr, [state, count]) => { + // show all states with significant needs + return count / contactsPerPhoneNumber >= 0.4 + ? [ + ...arr, + { + state, + needed: Math.ceil(count / contactsPerPhoneNumber) + } + ] + : arr; + }, []); + + const getAssignedCount = ({ state, areaCode }) => { + const inventory = this.state.inventoryPhoneNumberCounts; + if (state) { + return _.sumBy( + inventory.filter(invItem => + areaCodes.find( + item => item.areaCode === invItem.areaCode && item.state === state + ) + ), + "count" + ); + } + return (inventory.find(item => item.areaCode === areaCode) || {}).count; + }; + + // only display area codes with more than 1% coverage + let filteredAreaCodes = areaCodes.filter( + item => (item.count / contactsCount) * 100 >= 1 + ); + + if (searchText) { + if (!isNaN(searchText) && searchText.length <= 3) { + const foundAreaCode = filteredAreaCodes.find(({ areaCode }) => + areaCode.includes(searchText) + ); + filteredAreaCodes = foundAreaCode ? [foundAreaCode] : []; + } else if (isNaN(searchText)) { + filteredAreaCodes = filteredAreaCodes.filter(({ state }) => + state.toLowerCase().includes(searchText.toLowerCase()) + ); + } + } + return ( +
+
+
+ Top Area Codes in Contacts List +
+
+ + {isRendering ? ( + + ) : ( + states.map(({ state, needed: stateNeeded }) => { + const stateAssigned = getAssignedCount({ state }); + return ( + + {state} + = stateNeeded + ? theme.colors.green + : theme.colors.black + }} + > + {stateAssigned || 0} + {" / "} + {stateNeeded} + +
+ } + primaryTogglesNestedList + initiallyOpen + nestedItems={filteredAreaCodes + .filter(areaCode => areaCode.state === state) + .map(({ areaCode, count }) => { + const needed = Math.ceil(count / contactsPerPhoneNumber); + const assignedCount = getAssignedCount({ areaCode }); + return ( + + + {areaCode} + + = needed + ? theme.colors.green + : assignedCount + ? theme.colors.red + : theme.colors.black + }} + > + {assignedCount || 0} + {" / "} + {needed} + + + + {((count / contactsCount) * 100).toFixed(1)} + + + % + +
+ } + /> + ); + })} + /> + ); + }) + )} + + + ); + } + + render() { + const { contactsPerPhoneNumber } = this.props.serviceManagerInfo.data; + const { inventoryPhoneNumberCounts: reservedNumbers } = this.state; + const assignedNumberCount = this.getTotalNumberCount(reservedNumbers); + const { contactsCount, isStarted } = this.props.campaign; + const { isRendering, hasReset } = this.state; + const numbersNeeded = Math.ceil(contactsCount / contactsPerPhoneNumber); + + return ( + { + if (assignedNumberCount === numbersNeeded || hasReset) { + this.props.onSubmit({ + inventoryPhoneNumberCounts: this.state.inventoryPhoneNumberCounts + }); + this.setState({ hasChanged: false }); + } + }} + > + + {numbersNeeded <= maxNumbersPerCampaign ? ( +
+ {this.renderSearch()} + {this.state.error && this.renderErrorMessage()} +
+ {this.renderAreaCodeTable()} + {this.renderContactsAreaCodesTable()} +
+ + +
+ ) : ( +
+ Sorry, you need to upload fewer contacts! +
+ )} +
+ ); + } +} diff --git a/src/extensions/service-managers/test-fake-example/index.js b/src/extensions/service-managers/test-fake-example/index.js index e839debbd..2db6b5e1c 100644 --- a/src/extensions/service-managers/test-fake-example/index.js +++ b/src/extensions/service-managers/test-fake-example/index.js @@ -7,6 +7,8 @@ export const metadata = () => ({ // set canSpendMoney=true, if this extension can lead to (additional) money being spent // if it can, which operations below can trigger money being spent? displayName: "Test Fake Service Manager Example", + description: + "Used for testing and demonstrating service-manager capabilities", canSpendMoney: false, moneySpendingOperations: ["onCampaignStart"], supportsOrgConfig: true, diff --git a/src/extensions/service-vendors/index.js b/src/extensions/service-vendors/index.js index 91236548a..f77f7a3bc 100644 --- a/src/extensions/service-vendors/index.js +++ b/src/extensions/service-vendors/index.js @@ -3,7 +3,7 @@ import serviceMap, { getService } from "./service_map"; import orgCache from "../../server/models/cacheable_queries/organization"; - +import { processServiceManagers } from "../service-managers"; export { getConfigKey, getService, @@ -32,12 +32,18 @@ export const getServiceFromOrganization = organization => export const fullyConfigured = async organization => { const serviceName = exports.getServiceNameFromOrganization(organization); + const serviceManagerData = await processServiceManagers( + "onVendorServiceFullyConfigured", + organization, + { + serviceName + } + ); const fn = tryGetFunctionFromService(serviceName, "fullyConfigured"); if (!fn) { return true; } - - return fn(); + return fn(organization, serviceManagerData); }; export const createMessagingService = (organization, friendlyName) => { diff --git a/src/extensions/service-vendors/twilio/index.js b/src/extensions/service-vendors/twilio/index.js index 8d42a35b9..09cee43dd 100644 --- a/src/extensions/service-vendors/twilio/index.js +++ b/src/extensions/service-vendors/twilio/index.js @@ -951,14 +951,22 @@ export const getMessageServiceSid = async ( return messageServiceSid; }; -export const updateConfig = async (oldConfig, config, organization) => { +export const updateConfig = async ( + oldConfig, + config, + organization, + serviceManagerData +) => { const { twilioAccountSid, twilioAuthToken, twilioMessageServiceSid } = config; - if (!twilioAccountSid || !twilioMessageServiceSid) { - throw new Error( - "twilioAccountSid and twilioMessageServiceSid are required" - ); + if (!twilioAccountSid) { + throw new Error("twilioAccountSid is required"); + } + if ( + !twilioMessageServiceSid && + (!serviceManagerData || !serviceManagerData.skipOrgMessageService) + ) { + throw new Error("twilioMessageServiceSid is required"); } - const newConfig = {}; newConfig.TWILIO_ACCOUNT_SID = twilioAccountSid.substr(0, 64); @@ -971,9 +979,10 @@ export const updateConfig = async (oldConfig, config, organization) => { twilioAuthToken ) : twilioAuthToken; - newConfig.TWILIO_MESSAGE_SERVICE_SID = - twilioMessageServiceSid && twilioMessageServiceSid.substr(0, 64); - + if (twilioMessageServiceSid) { + newConfig.TWILIO_MESSAGE_SERVICE_SID = + twilioMessageServiceSid && twilioMessageServiceSid.substr(0, 64); + } try { if (twilioAuthToken && global.TEST_ENVIRONMENT !== "1") { // Make sure Twilio credentials work. @@ -1013,7 +1022,7 @@ export const manualMessagingServicesEnabled = organization => { truthy: true } ); -export const fullyConfigured = async organization => { +export const fullyConfigured = async (organization, serviceManagerData) => { const { authToken, accountSid } = await getMessageServiceConfig( "twilio", organization @@ -1023,10 +1032,9 @@ export const fullyConfigured = async organization => { return false; } - if ( - exports.manualMessagingServicesEnabled(organization) || - exports.campaignNumbersEnabled(organization) - ) { + if (serviceManagerData && serviceManagerData.skipOrgMessageService) { + // exports.manualMessagingServicesEnabled(organization) || + // exports.campaignNumbersEnabled(organization) return true; } return !!(await exports.getMessageServiceSid(organization)); diff --git a/src/server/api/campaign.js b/src/server/api/campaign.js index 7329a4bc9..307fe96a6 100644 --- a/src/server/api/campaign.js +++ b/src/server/api/campaign.js @@ -743,6 +743,7 @@ export const resolvers = { creator: async (campaign, _, { loaders }) => campaign.creator_id ? loaders.user.load(campaign.creator_id) : null, isArchivedPermanently: campaign => { + // TODO: consider removal // started campaigns that have had their message service sid deleted can't be restarted // NOTE: this will need to change if campaign phone numbers are extended beyond twilio and fakeservice return ( diff --git a/src/server/api/lib/owned-phone-number.js b/src/server/api/lib/owned-phone-number.js index 6d6ea9fc6..41de93d62 100644 --- a/src/server/api/lib/owned-phone-number.js +++ b/src/server/api/lib/owned-phone-number.js @@ -1,4 +1,5 @@ import { r } from "../../models"; +import { getConfig } from "./config"; async function allocateCampaignNumbers( { organizationId, campaignId, areaCode, amount }, @@ -41,7 +42,7 @@ async function releaseCampaignNumbers(campaignId, transactionOrKnex) { } async function listCampaignNumbers(campaignId) { - return r + const nums = await r .knex("owned_phone_number") .select("area_code", r.knex.raw("count(*) as count")) .where({ @@ -49,10 +50,42 @@ async function listCampaignNumbers(campaignId) { allocated_to_id: campaignId.toString() }) .groupBy("area_code"); + return nums.map(n => ({ + area_code: n.area_code, + count: Number(n.count) + })); +} + +async function listOrganizationCounts(organization) { + const usAreaCodes = require("us-area-codes"); + const service = + getConfig("service", organization) || + getConfig("DEFAULT_SERVICE", organization); + const counts = await r + .knex("owned_phone_number") + .select( + "area_code", + r.knex.raw("COUNT(allocated_to) as allocated_count"), + r.knex.raw( + "SUM(CASE WHEN allocated_to IS NULL THEN 1 END) as available_count" + ) + ) + .where({ + service, + organization_id: organization.id + }) + .groupBy("area_code"); + return counts.map(row => ({ + areaCode: row.area_code, + state: usAreaCodes.get(Number(row.area_code)), + allocatedCount: Number(row.allocated_count), + availableCount: Number(row.available_count) + })); } export default { allocateCampaignNumbers, releaseCampaignNumbers, - listCampaignNumbers + listCampaignNumbers, + listOrganizationCounts }; diff --git a/src/server/api/mutations/startCampaign.js b/src/server/api/mutations/startCampaign.js index 32c06dbb3..ee43b348b 100644 --- a/src/server/api/mutations/startCampaign.js +++ b/src/server/api/mutations/startCampaign.js @@ -1,86 +1,26 @@ -import cacheableData from "../../models/cacheable_queries"; -import { r } from "../../models"; +import { cacheableData } from "../../models"; import { accessRequired } from "../errors"; -import { Notifications, sendUserNotification } from "../../notifications"; -import { serviceManagersHaveImplementation } from "../../../extensions/service-managers"; -import * as twilio from "../../../extensions/service-vendors/twilio"; import { getConfig } from "../lib/config"; import { jobRunner } from "../../../extensions/job-runners"; import { Tasks } from "../../../workers/tasks"; -import { Jobs } from "../../../workers/job-processes"; -export const startCampaign = async ( - _, - { id }, - { user, loaders, remainingMilliseconds } -) => { +export const startCampaign = async (_, { id }, { user }) => { const campaign = await cacheableData.campaign.load(id); await accessRequired(user, campaign.organization_id, "ADMIN"); - const organization = await loaders.organization.load( + const organization = await cacheableData.organization.load( campaign.organization_id ); - if ( - getConfig("EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS", organization, { - truthy: true - }) - ) { - await jobRunner.dispatchJob({ - queue_name: `${id}:start_campaign`, - job_type: Jobs.START_CAMPAIGN_WITH_PHONE_NUMBERS, - locks_queue: false, - campaign_id: id, - payload: JSON.stringify({}) - }); - - return await cacheableData.campaign.load(id, { - forceLoad: true - }); - } - - if (campaign.use_own_messaging_service) { - if (!campaign.messageservice_sid) { - const friendlyName = `Campaign: ${campaign.title} (${campaign.id}) [${process.env.BASE_URL}]`; - const messagingService = await twilio.createMessagingService( - organization, - friendlyName - ); - campaign.messageservice_sid = messagingService.sid; + // onCampaignStart service managers get to do stuff, + // before we update campaign.is_started (see workers/tasks.js::serviceManagerTrigger) + await jobRunner.dispatchTask(Tasks.SERVICE_MANAGER_TRIGGER, { + functionName: "onCampaignStart", + organizationId: organization.id, + data: { + campaign, + user } - } else { - campaign.messageservice_sid = await cacheableData.organization.getMessageServiceSid( - organization - ); - } - - campaign.is_started = true; - - await campaign.save(); - const campaignRefreshed = await cacheableData.campaign.load(id, { - forceLoad: true }); - await sendUserNotification({ - type: Notifications.CAMPAIGN_STARTED, - campaignId: id - }); - - if (r.redis && !getConfig("DISABLE_CONTACT_CACHELOAD")) { - // some asynchronous cache-priming: - await jobRunner.dispatchTask(Tasks.CAMPAIGN_START_CACHE, { - campaign: campaignRefreshed, - organization - }); - } - - if (serviceManagersHaveImplementation("onCampaignStart", organization)) { - await jobRunner.dispatchTask(Tasks.SERVICE_MANAGER_TRIGGER, { - functionName: "onCampaignStart", - organizationId: organization.id, - data: { - campaign: campaignRefreshed, - user - } - }); - } - return campaignRefreshed; + return campaign; + // TODO: maybe return a isStarting for component update }; diff --git a/src/server/api/mutations/updateServiceManager.js b/src/server/api/mutations/updateServiceManager.js index 241a43a1a..de3ebacfb 100644 --- a/src/server/api/mutations/updateServiceManager.js +++ b/src/server/api/mutations/updateServiceManager.js @@ -17,36 +17,28 @@ export const updateServiceManager = async ( ) => { const organization = await cacheableData.organization.load(organizationId); let campaign; - let result = {}; + let result; if (campaignId) { // FUTURE: maybe with specific metadata, this could be made lower // which could be useful in complement to texter-sideboxes await accessRequired(user, organizationId, "SUPERVOLUNTEER", true); campaign = await cacheableData.campaign.load(campaignId); - const response = await processServiceManagers( + result = await processServiceManagers( "onCampaignUpdateSignal", organization, { organization, campaign, user, updateData, fromCampaignStatsPage }, serviceManagerName ); - if (response && response.length && response[0]) { - result = response[0]; - } } else { // organization await accessRequired(user, organizationId, "OWNER", true); - const response = await processServiceManagers( + result = await processServiceManagers( "onOrganizationUpdateSignal", organization, { organization, user, updateData }, serviceManagerName ); - console.log("updateServiceManager organization", response); - if (response && response.length && response[0]) { - result = response[0]; - } } - console.log("updateServiceManager", result); return { id: `${serviceManagerName}-org${organizationId}-${campaignId || ""}${ fromCampaignStatsPage ? "stats" : "" diff --git a/src/server/api/mutations/updateServiceVendorConfig.js b/src/server/api/mutations/updateServiceVendorConfig.js index 619e60f36..940903cae 100644 --- a/src/server/api/mutations/updateServiceVendorConfig.js +++ b/src/server/api/mutations/updateServiceVendorConfig.js @@ -50,6 +50,17 @@ export const updateServiceVendorConfig = async ( }); let newConfig; + await processServiceManagers( + "onOrganizationServiceVendorSetup", + organization, + { + user, + serviceName, + oldConfig: existingConfig, + newConfig: configObject + } + ); + try { newConfig = await serviceConfigFunction( existingConfig, @@ -83,17 +94,6 @@ export const updateServiceVendorConfig = async ( await orgCache.clear(organization.id); const updatedOrganization = await orgCache.load(organization.id); - await processServiceManagers( - "onOrganizationServiceVendorSetup", - updatedOrganization, - { - user, - serviceName, - oldConfig: existingConfig, - newConfig - } - ); - return { id: `org${organization.id}-${serviceName}`, config: await orgCache.getMessageServiceConfig(updatedOrganization, { diff --git a/src/server/api/organization.js b/src/server/api/organization.js index ce7c14711..a514b699d 100644 --- a/src/server/api/organization.js +++ b/src/server/api/organization.js @@ -1,6 +1,7 @@ import { mapFieldsToModel } from "./lib/utils"; import { getConfig, getFeatures } from "./lib/config"; import { r, Organization, cacheableData } from "../models"; +import ownedPhoneNumber from "./lib/owned-phone-number"; import { getTags } from "./tag"; import { accessRequired } from "./errors"; import { getCampaigns } from "./campaign"; @@ -334,29 +335,7 @@ export const resolvers = { ) { return []; } - const usAreaCodes = require("us-area-codes"); - const service = - getConfig("service", organization) || getConfig("DEFAULT_SERVICE"); - const counts = await r - .knex("owned_phone_number") - .select( - "area_code", - r.knex.raw("COUNT(allocated_to) as allocated_count"), - r.knex.raw( - "SUM(CASE WHEN allocated_to IS NULL THEN 1 END) as available_count" - ) - ) - .where({ - service, - organization_id: organization.id - }) - .groupBy("area_code"); - return counts.map(row => ({ - areaCode: row.area_code, - state: usAreaCodes.get(Number(row.area_code)), - allocatedCount: Number(row.allocated_count), - availableCount: Number(row.available_count) - })); + return await ownedPhoneNumber.listOrganizationCounts(organization); } } }; diff --git a/src/server/api/schema.js b/src/server/api/schema.js index 6e1227895..c918793b2 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -381,31 +381,6 @@ async function editCampaign(id, campaign, loaders, user, origCampaignRecord) { }); } - if (campaign.hasOwnProperty("inventoryPhoneNumberCounts")) { - if (origCampaignRecord.isStarted) { - throw new Error( - "Cannot update phone numbers once a campaign has started" - ); - } - const phoneCounts = campaign.inventoryPhoneNumberCounts; - await r.knex.transaction(async trx => { - await ownedPhoneNumber.releaseCampaignNumbers(id, trx); - for (const pc of phoneCounts) { - if (pc.count) { - await ownedPhoneNumber.allocateCampaignNumbers( - { - organizationId, - campaignId: id, - areaCode: pc.areaCode, - amount: pc.count - }, - trx - ); - } - } - }); - } - const campaignRefreshed = await cacheableData.campaign.load(id, { forceLoad: changed }); @@ -931,14 +906,10 @@ const rootMutations = { const organization = await cacheableData.organization.load( campaign.organization_id ); - const serviceManagerResults = await processServiceManagers( - "onCampaignUnarchive", - organization, - { - campaign, - user - } - ); + await processServiceManagers("onCampaignUnarchive", organization, { + campaign, + user + }); await campaign.save(); await cacheableData.campaign.clear(id); return campaign; diff --git a/src/workers/jobs.js b/src/workers/jobs.js index 1d3c1d782..348343d62 100644 --- a/src/workers/jobs.js +++ b/src/workers/jobs.js @@ -1288,110 +1288,3 @@ export async function deletePhoneNumbers(job) { await defensivelyDeleteJob(job); } } - -// Prepares a messaging service with owned number for the campaign -async function prepareTwilioCampaign(campaign, organization, trx) { - const ts = Math.floor(new Date() / 1000); - const baseUrl = getConfig("BASE_URL", organization); - const friendlyName = `Campaign ${campaign.id}: ${campaign.organization_id}-${ts} [${baseUrl}]`; - const messagingService = await twilio.createMessagingService( - organization, - friendlyName - ); - const msgSrvSid = messagingService.sid; - if (!msgSrvSid) { - throw new Error("Failed to create messaging service!"); - } - const phoneSids = ( - await trx("owned_phone_number") - .select("service_id") - .where({ - organization_id: campaign.organization_id, - service: "twilio", - allocated_to: "campaign", - allocated_to_id: campaign.id.toString() - }) - ).map(row => row.service_id); - console.log(`Transferring ${phoneSids.length} numbers to ${msgSrvSid}`); - try { - await twilio.addNumbersToMessagingService( - organization, - phoneSids, - msgSrvSid - ); - } catch (e) { - console.error("Failed to add numbers to messaging service", e); - await twilio.deleteMessagingService(organization, msgSrvSid); - throw new Error("Failed to add numbers to messaging service"); - } - return msgSrvSid; -} - -// Start a campaign when EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS is enabled -// TODO: refactor this to share more code with the startCampaign mutation -export async function startCampaignWithPhoneNumbers(job) { - if (!job.campaign_id) { - throw new Error("Missing job.campaign_id"); - } - try { - let organization; - await r.knex.transaction(async trx => { - const campaign = await trx("campaign") - .where("id", job.campaign_id) - // PG only: lock this campaign while starting, making this job idempotent - .forUpdate() - .first(); - if (campaign.is_started) { - throw new Error("Campaign already started"); - } - organization = await trx("organization") - .where("id", campaign.organization_id) - .first(); - const service = getConfig("DEFAULT_SERVICE", organization); - - let messagingServiceSid; - if (service === "twilio") { - messagingServiceSid = await prepareTwilioCampaign( - campaign, - organization, - trx - ); - } else if (service === "fakeservice") { - // simulate some latency - await new Promise(resolve => setTimeout(resolve, 1000)); - messagingServiceSid = "FAKEMESSAGINGSERVICE"; - } else { - throw new Error( - `Campaign phone numbers are not supported for service ${service}` - ); - } - - await trx("campaign") - .where("id", campaign.id) - .update({ - is_started: true, - use_own_messaging_service: true, - messageservice_sid: messagingServiceSid - }); - }); - - await cacheableData.campaign.clear(job.campaign_id); - const reloadedCampaign = await cacheableData.campaign.load(job.campaign_id); - - await sendUserNotification({ - type: Notifications.CAMPAIGN_STARTED, - campaignId: job.campaign_id - }); - - // We are already in an background job process, so invoke the task directly rather than - // kicking it off through the dispatcher - await invokeTaskFunction(Tasks.CAMPAIGN_START_CACHE, { - organization, - campaign: reloadedCampaign - }); - } catch (e) { - console.error(`Job ${job.id} failed: ${e.message}`, e); - } finally { - await defensivelyDeleteJob(job); - } -} diff --git a/src/workers/tasks.js b/src/workers/tasks.js index c63629506..5ebb32506 100644 --- a/src/workers/tasks.js +++ b/src/workers/tasks.js @@ -3,7 +3,8 @@ // See src/extensions/job-runners/README.md for more details import serviceMap from "../extensions/service-vendors"; import * as ActionHandlers from "../extensions/action-handlers"; -import { cacheableData } from "../server/models"; +import { r, cacheableData } from "../server/models"; +import { Notifications, sendUserNotification } from "../server/notifications"; import { processServiceManagers } from "../extensions/service-managers"; export const Tasks = Object.freeze({ @@ -23,7 +24,36 @@ const serviceManagerTrigger = async ({ if (organizationId) { organization = await cacheableData.organization.load(organizationId); } - await processServiceManagers(functionName, organization, data); + const serviceManagerData = await processServiceManagers( + functionName, + organization, + data + ); + + // This is a little hacky rather than making another task, but while it's a single + // exception, it feels fine -- if this becomes a bunch of if...else ifs, then reconsider + if ( + functionName === "onCampaignStart" && + data.campaign && + !(serviceManagerData && serviceManagerData.blockCampaignStart) + ) { + await r + .knex("campaign") + .where("id", data.campaign.id) + .update({ is_started: true }); + await cacheableData.campaign.load(data.campaign.id, { forceLoad: true }); + await sendUserNotification({ + type: Notifications.CAMPAIGN_STARTED, + campaignId: data.campaign.id + }); + // TODO: Decide if we want/need this anymore, relying on FUTURE campaign-contact cache load changes + // We are already in an background job process, so invoke the task directly rather than + // kicking it off through the dispatcher + // await invokeTaskFunction(Tasks.CAMPAIGN_START_CACHE, { + // organization, + // campaign: reloadedCampaign + // }); + } }; const sendMessage = async ({ @@ -37,8 +67,20 @@ const sendMessage = async ({ if (!service) { throw new Error(`Failed to find service for message ${message}`); } + const serviceManagerData = await processServiceManagers( + "onMessageSend", + organization, + { message, contact, campaign } + ); - await service.sendMessage({ message, contact, trx, organization, campaign }); + await service.sendMessage({ + message, + contact, + trx, + organization, + campaign, + serviceManagerData + }); }; const questionResponseActionHandler = async ({ From 7ab36109fad22dde236303c5f116ec684fc5b26c Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Thu, 10 Jun 2021 14:55:05 -0400 Subject: [PATCH 086/191] service-vendor-refactor: fix twilio tests/adapt new components to material-ui update --- __test__/extensions/service-vendors/twilio.test.js | 6 ++---- src/components/CampaignServiceManagers.jsx | 12 +++++------- src/extensions/service-vendors/twilio/index.js | 9 ++++++--- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/__test__/extensions/service-vendors/twilio.test.js b/__test__/extensions/service-vendors/twilio.test.js index 69b053d54..68c8e51be 100644 --- a/__test__/extensions/service-vendors/twilio.test.js +++ b/__test__/extensions/service-vendors/twilio.test.js @@ -1002,9 +1002,7 @@ describe("config functions", () => { } catch (caught) { error = caught; } - expect(error.message).toEqual( - "twilioAccountSid and twilioMessageServiceSid are required" - ); + expect(error.message).toEqual("twilioAccountSid is required"); expect(crypto.symmetricEncrypt).not.toHaveBeenCalled(); expect(twilioLibrary.Twilio).not.toHaveBeenCalled(); expect(twilioApiAccountsListMock).not.toHaveBeenCalled(); @@ -1120,7 +1118,7 @@ describe("config functions", () => { .spyOn(twilio, "getMessageServiceSid") .mockResolvedValue("fake_message_service_sid"); }); - it("returns true", async () => { + it("fullyConfigured returns true", async () => { expect(await twilio.fullyConfigured("everything_is_mocked")).toEqual( true ); diff --git a/src/components/CampaignServiceManagers.jsx b/src/components/CampaignServiceManagers.jsx index 725aac553..279704c48 100644 --- a/src/components/CampaignServiceManagers.jsx +++ b/src/components/CampaignServiceManagers.jsx @@ -4,15 +4,13 @@ import React from "react"; import gql from "graphql-tag"; import GSForm from "../components/forms/GSForm"; import Form from "react-formal"; -import Dialog from "material-ui/Dialog"; import GSSubmitButton from "../components/forms/GSSubmitButton"; -import FlatButton from "material-ui/FlatButton"; -import RaisedButton from "material-ui/RaisedButton"; import * as yup from "yup"; -import { Card, CardText, CardActions, CardHeader } from "material-ui/Card"; +import Card from "@material-ui/core/Card"; +import CardHeader from "@material-ui/core/CardHeader"; +import CardContent from "@material-ui/core/CardContent"; import { StyleSheet, css } from "aphrodite"; import theme from "../styles/theme"; -import Toggle from "material-ui/Toggle"; import moment from "moment"; import CampaignTexterUIForm from "../components/CampaignTexterUIForm"; import OrganizationFeatureSettings from "../components/OrganizationFeatureSettings"; @@ -68,7 +66,7 @@ export class CampaignServiceVendors extends React.Component { }} /> ) : null} - + - +
); })} diff --git a/src/extensions/service-vendors/twilio/index.js b/src/extensions/service-vendors/twilio/index.js index 09cee43dd..94e1eb4f9 100644 --- a/src/extensions/service-vendors/twilio/index.js +++ b/src/extensions/service-vendors/twilio/index.js @@ -1032,9 +1032,12 @@ export const fullyConfigured = async (organization, serviceManagerData) => { return false; } - if (serviceManagerData && serviceManagerData.skipOrgMessageService) { - // exports.manualMessagingServicesEnabled(organization) || - // exports.campaignNumbersEnabled(organization) + if ( + (serviceManagerData && serviceManagerData.skipOrgMessageService) || + // legacy options + exports.manualMessagingServicesEnabled(organization) || + exports.campaignNumbersEnabled(organization) + ) { return true; } return !!(await exports.getMessageServiceSid(organization)); From c73b0f93ba0cb2543ec512e2fea1baaa5abfdefd Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Fri, 11 Jun 2021 12:08:46 -0400 Subject: [PATCH 087/191] update service-managers/per-campaign-messageservice for material-ui upgrade --- src/components/CampaignServiceManagers.jsx | 8 +- src/containers/AdminCampaignEdit.jsx | 10 +- src/containers/AdminPhoneNumberInventory.js | 6 +- .../per-campaign-messageservices/index.js | 9 +- .../react-component.js | 415 ++++++++++-------- .../test-fake-example/react-component.js | 11 +- src/server/middleware/render-index.js | 7 + 7 files changed, 249 insertions(+), 217 deletions(-) diff --git a/src/components/CampaignServiceManagers.jsx b/src/components/CampaignServiceManagers.jsx index 279704c48..e00deeb99 100644 --- a/src/components/CampaignServiceManagers.jsx +++ b/src/components/CampaignServiceManagers.jsx @@ -4,7 +4,6 @@ import React from "react"; import gql from "graphql-tag"; import GSForm from "../components/forms/GSForm"; import Form from "react-formal"; -import GSSubmitButton from "../components/forms/GSSubmitButton"; import * as yup from "yup"; import Card from "@material-ui/core/Card"; import CardHeader from "@material-ui/core/CardHeader"; @@ -16,9 +15,8 @@ import CampaignTexterUIForm from "../components/CampaignTexterUIForm"; import OrganizationFeatureSettings from "../components/OrganizationFeatureSettings"; import { getServiceVendorComponent } from "../extensions/service-vendors/components"; import { getServiceManagerComponent } from "../extensions/service-managers/components"; -import GSTextField from "../components/forms/GSTextField"; -export class CampaignServiceVendors extends React.Component { +export class CampaignServiceManagers extends React.Component { static propTypes = { formValues: type.object, onChange: type.func, @@ -52,7 +50,7 @@ export class CampaignServiceVendors extends React.Component { return null; } return ( - + {serviceManagerComponentName === "CampaignConfig" ? ( - {messageServiceName === "twilio" && - messageServiceConfig.TWILIO_MESSAGE_SERVICE_SID && - messageServiceConfig.TWILIO_MESSAGE_SERVICE_SID.length > 0 && + {serviceName === "twilio" && + serviceConfig.TWILIO_MESSAGE_SERVICE_SID && + serviceConfig.TWILIO_MESSAGE_SERVICE_SID.length > 0 && !this.props.data.organization.campaignPhoneNumbersEnabled && ( { ...contactsPerNum }, fullyConfigured: + // Two mutually exclusive modes: EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS vs. EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE Boolean(campaign.messageservice_sid) || - (numbersReserved >= numbersNeeded && false), // ?? TODO: only when campaignPhoneNumbersEnabled=EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS - // and NOT EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE + (numbersReserved >= numbersNeeded && + !getConfig( + "EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE", + organization, + { truthy: 1 } + )), unArchiveable: !campaign.use_own_messaging_service || campaign.messageservice_sid }; diff --git a/src/extensions/service-managers/per-campaign-messageservices/react-component.js b/src/extensions/service-managers/per-campaign-messageservices/react-component.js index 730529368..bca57296a 100644 --- a/src/extensions/service-managers/per-campaign-messageservices/react-component.js +++ b/src/extensions/service-managers/per-campaign-messageservices/react-component.js @@ -2,19 +2,30 @@ import React from "react"; import type from "prop-types"; import { StyleSheet, css } from "aphrodite"; import _ from "lodash"; -import GSForm from "../../../components/forms/GSForm"; -import GSSubmitButton from "../../../components/forms/GSSubmitButton"; import * as yup from "yup"; + import Form from "react-formal"; + +import Button from "@material-ui/core/Button"; +import IconButton from "@material-ui/core/IconButton"; +import AddIcon from "@material-ui/icons/Add"; +import RemoveIcon from "@material-ui/icons/Remove"; +import Checkbox from "@material-ui/core/Checkbox"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; + +import List from "@material-ui/core/List"; +import ListItem from "@material-ui/core/ListItem"; +import ListItemText from "@material-ui/core/ListItemText"; +import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction"; +import Collapse from "@material-ui/core/Collapse"; +import ExpandLess from "@material-ui/icons/ExpandLess"; +import ExpandMore from "@material-ui/icons/ExpandMore"; +import Autocomplete from "@material-ui/lab/Autocomplete"; +import TextField from "@material-ui/core/TextField"; + +import GSForm from "../../../components/forms/GSForm"; +import GSSubmitButton from "../../../components/forms/GSSubmitButton"; import CampaignFormSectionHeading from "../../../components/CampaignFormSectionHeading"; -import { ListItem, List } from "material-ui/List"; -import AutoComplete from "material-ui/AutoComplete"; -import RaisedButton from "material-ui/RaisedButton"; -import FlatButton from "material-ui/FlatButton"; -import Checkbox from "material-ui/Checkbox"; -import IconButton from "material-ui/IconButton/IconButton"; -import AddIcon from "material-ui/svg-icons/content/add-circle"; -import RemoveIcon from "material-ui/svg-icons/content/remove-circle"; import LoadingIndicator from "../../../components/LoadingIndicator"; import theme from "../../../styles/theme"; @@ -266,44 +277,49 @@ export class CampaignConfig extends React.Component { style={{ maxHeight: 360, minHeight: 360, - overflowY: "auto", - padding: "0 15px 0 0" + overflowY: "auto" }} > {isRendering ? ( ) : ( - states.map(state => ( + states.map(state => [ areaCode.state === state) - .map(({ areaCode, availableCount }) => { - const assignedCount = getAssignedCount(areaCode); - const isSuppressed = suppressedAreaCodes.includes(areaCode); - return ( - - - {areaCode} - + button + onClick={() => + this.setState({ + [`${state}-open`]: !this.state[`${state}-open`] + }) + } + > + + {this.state[`${state}-open`] ? : } + , + + + {areaCodes + .filter(areaCode => areaCode.state === state) + .map(({ areaCode, availableCount }) => { + const assignedCount = getAssignedCount(areaCode); + const isSuppressed = suppressedAreaCodes.includes(areaCode); + return ( + + + - {`${assignedCount}${ - !isStarted ? ` / ${availableCount}` : "" - }`} + {!isStarted && isSuppressed && ( + + Not Enough to Reserve + + )} + {!isStarted && !isSuppressed && ( + unassignAreaCode(areaCode)} + > + + + )} + {assignedCount}{" "} + {!isStarted && ` / ${availableCount}`} + {!isStarted && !isSuppressed && ( + assignAreaCode(areaCode)} + > + + + )} - - } - rightIconButton={ - !isStarted && - (!isSuppressed ? ( -
- unassignAreaCode(areaCode)} - > - - - assignAreaCode(areaCode)} - > - - -
- ) : ( -
- Not Enough to Reserve -
- )) - } - /> - ); - })} - /> - )) +
+
+ ); + })} +
+
+ ]) )} ); @@ -399,26 +412,28 @@ export class CampaignConfig extends React.Component { ); } - const filter = (searchText, key) => - key === "allphoneNumbers" - ? true - : AutoComplete.caseInsensitiveFilter(searchText, key); - const autocomplete = ( - this.setState({ searchText })} - searchText={this.state.searchText} - filter={filter} - hintText="Find State or Area Code" + onInputChange={(event, searchText) => this.setState({ searchText })} + inputValue={this.state.searchText} name="areaCode" - label="Find State or Area Code" - dataSource={[]} + getOptionLabel={option => + `${option.state}: ${option.areaCode} / available: ${option.availableCount}` + } + options={phoneNumberCounts} + renderInput={params => ( + + )} /> ); const showAutocomplete = !isStarted && phoneNumberCounts.length > 0; - return
{showAutocomplete ? autocomplete : ""}
; + return
{showAutocomplete && autocomplete}
; } renderErrorMessage() { @@ -611,44 +626,23 @@ export class CampaignConfig extends React.Component { className={css(styles.container)} style={{ flex: 1, marginRight: 50, maxWidth: 500 }} > -
+
-
- - {"Reserved phone numbers: "} - {`${assignedNumberCount}/${numbersNeeded}`} - - - {!isStarted && ( - resetReserved()} - /> - )} +
+ Reserved phone numbers: + {` ${assignedNumberCount}/${numbersNeeded}`}
{!isStarted && ( -
- + + + {!isStarted && ( + + )} + + { - this.setState(({ showOnlySelected }) => ({ - showOnlySelected: !showOnlySelected, - searchText: "" - })); - }} + labelPlacement="start" + control={ + { + this.setState(({ showOnlySelected }) => ({ + showOnlySelected: !showOnlySelected, + searchText: "" + })); + }} + /> + } />
)} @@ -766,8 +782,7 @@ export class CampaignConfig extends React.Component { style={{ maxHeight: 420, minHeight: 420, - overflowY: "auto", - padding: "0 15px 0 0" + overflowY: "auto" }} > {isRendering ? ( @@ -775,59 +790,69 @@ export class CampaignConfig extends React.Component { ) : ( states.map(({ state, needed: stateNeeded }) => { const stateAssigned = getAssignedCount({ state }); - return ( + return [ + this.setState({ + [`${state}-open`]: !this.state[`${state}-open`] + }) + } + > + + + = stateNeeded + ? theme.colors.green + : theme.colors.black }} > - {state} - = stateNeeded - ? theme.colors.green - : theme.colors.black - }} - > - {stateAssigned || 0} - {" / "} - {stateNeeded} - -
- } - primaryTogglesNestedList - initiallyOpen - nestedItems={filteredAreaCodes - .filter(areaCode => areaCode.state === state) - .map(({ areaCode, count }) => { - const needed = Math.ceil(count / contactsPerPhoneNumber); - const assignedCount = getAssignedCount({ areaCode }); - return ( - + {this.state[`${state}-open`] ? ( + + ) : ( + + )} + + , + + + {filteredAreaCodes + .filter(areaCode => areaCode.state === state) + .map(({ areaCode, count }) => { + const needed = Math.ceil( + count / contactsPerPhoneNumber + ); + const assignedCount = getAssignedCount({ areaCode }); + return ( +
- - {areaCode} - + {areaCode}
- } - /> - ); - })} - /> - ); +
+ ); + })} +
+
+ ]; }) )} diff --git a/src/extensions/service-managers/test-fake-example/react-component.js b/src/extensions/service-managers/test-fake-example/react-component.js index 061bc0432..1d9503c58 100644 --- a/src/extensions/service-managers/test-fake-example/react-component.js +++ b/src/extensions/service-managers/test-fake-example/react-component.js @@ -1,9 +1,5 @@ /* eslint no-console: 0 */ import { css } from "aphrodite"; -import { CardText } from "material-ui/Card"; -import Dialog from "material-ui/Dialog"; -import FlatButton from "material-ui/FlatButton"; -import { Table, TableBody, TableRow, TableRowColumn } from "material-ui/Table"; import PropTypes from "prop-types"; import React from "react"; import Form from "react-formal"; @@ -48,7 +44,6 @@ export class OrgConfig extends React.Component { as={GSSubmitButton} label="Save" style={this.props.inlineStyles.dialogButton} - component={GSSubmitButton} />
@@ -97,11 +92,7 @@ export class CampaignConfig extends React.Component { name="savedText" fullWidth /> - + ) : (
diff --git a/src/server/middleware/render-index.js b/src/server/middleware/render-index.js index 890efb0fa..091df2d4d 100644 --- a/src/server/middleware/render-index.js +++ b/src/server/middleware/render-index.js @@ -99,6 +99,13 @@ export default function renderIndex(html, css, assetMap) { window.CAN_GOOGLE_IMPORT=${canGoogleImport} window.DOWNTIME="${process.env.DOWNTIME || ""}" window.DOWNTIME_TEXTER="${process.env.DOWNTIME_TEXTER || ""}" + window.EXPERIMENTAL_PER_CAMPAIGN_MESSAGING_LEGACY=${getConfig( + "EXPERIMENTAL_PER_CAMPAIGN_MESSAGING_LEGACY", + null, + { + truthy: 1 + } + ) || false} window.EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE=${process.env .EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE || false} window.TWILIO_MULTI_ORG=${process.env.TWILIO_MULTI_ORG || false} From 4218329bb8df2725b6af7eee2408d212dd24cc28 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Fri, 11 Jun 2021 12:10:01 -0400 Subject: [PATCH 088/191] move components/CampaignMessageServiceForm.jsx => extensions/per-campaign-messageservices/react-component-campaignmessageservice.js for conditional import/changes --- .../react-component-campaignmessageservice.js | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/extensions/service-managers/per-campaign-messageservices/react-component-campaignmessageservice.js diff --git a/src/extensions/service-managers/per-campaign-messageservices/react-component-campaignmessageservice.js b/src/extensions/service-managers/per-campaign-messageservices/react-component-campaignmessageservice.js new file mode 100644 index 000000000..09c585622 --- /dev/null +++ b/src/extensions/service-managers/per-campaign-messageservices/react-component-campaignmessageservice.js @@ -0,0 +1,102 @@ +import type from "prop-types"; +import Switch from "@material-ui/core/Switch"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import React from "react"; +import Form from "react-formal"; +import GSForm from "./forms/GSForm"; +import GSTextField from "./forms/GSTextField"; +import GSSubmitButton from "./forms/GSSubmitButton"; +import CampaignFormSectionHeading from "./CampaignFormSectionHeading"; +import * as yup from "yup"; +import cloneDeep from "lodash/cloneDeep"; + +export default class CampaignMessagingServiceForm extends React.Component { + formSchema = yup.object({ + useOwnMessagingService: yup.boolean(), + messageserviceSid: yup + .string() + .transform(value => (!value ? null : value)) + .nullable() + }); + + toggled(name, isToggled) { + const formValues = cloneDeep(this.props.formValues); + formValues[name] = isToggled; + this.props.onChange(formValues); + } + + addToggleFormField(name, label) { + return ( +
+ ( + { + this.toggled(name, isToggled); + }} + /> + } + labelPlacement="start" + label={label} + /> + )} + name={name} + /> +
+ ); + } + + render() { + return ( + + + + {this.addToggleFormField( + "useOwnMessagingService", + "Create a new Messaging Service for this Campaign?" + )} + + {this.props.formValues.useOwnMessagingService && ( +
+ + +
+ )} + + +
+ ); + } +} + +CampaignMessagingServiceForm.propTypes = { + saveLabel: type.string, + saveDisabled: type.bool, + onSubmit: type.func, + onChange: type.func, + formValues: type.object, + ensureComplete: type.bool +}; From 819747381343b741658f8eade095b09d5984e95e Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Fri, 11 Jun 2021 22:58:33 -0400 Subject: [PATCH 089/191] bugfixes for service-vendor-refactor AND material-ui upgrade. Also some bandwidth fixes --- src/containers/Settings.jsx | 10 +++---- .../per-campaign-messageservices/index.js | 23 +++++++-------- .../react-component-campaignmessageservice.js | 8 ++--- .../react-component.js | 2 ++ .../bandwidth/react-component.js | 29 ++++++++++++------- .../bandwidth/setup-and-numbers.js | 11 +++++-- 6 files changed, 49 insertions(+), 34 deletions(-) diff --git a/src/containers/Settings.jsx b/src/containers/Settings.jsx index de6c28ae1..57e242d4b 100644 --- a/src/containers/Settings.jsx +++ b/src/containers/Settings.jsx @@ -175,7 +175,7 @@ class Settings extends React.Component { : theme.colors.yellow }} /> - + {serviceManagers.map(sm => { const ServiceManagerComp = getServiceManagerComponent( sm.name, @@ -183,7 +183,7 @@ class Settings extends React.Component { ); const serviceManagerName = sm.name; return ( - + - + - + ); })} - + ); } diff --git a/src/extensions/service-managers/per-campaign-messageservices/index.js b/src/extensions/service-managers/per-campaign-messageservices/index.js index 4f68c4360..ccc9b04b7 100644 --- a/src/extensions/service-managers/per-campaign-messageservices/index.js +++ b/src/extensions/service-managers/per-campaign-messageservices/index.js @@ -8,10 +8,9 @@ // - onOrganizationServiceVendorSetup: to disable requiring org-level messageservice setup // - onVendorServiceFullyConfigured: to disable requiring org-level messageservice setup -// FUTURE: ?org configure to enable it? // TODO: how should AdminPhoneNumberBuying be affected -- can/should it 'steal' the message -// TODO: maybe it should remove/block the org-level messageservice_sid from being set? -// TODO: should it capture the response from twilio and then mark fully configured even if messageservice_sid isn't set? +// TODO: maybe it should remove/block the org-level messageservice_sid from being set? (or warn in org config) +// TODO/FUTURE: org config for: manualMessageServiceMode and CAMPAIGN_PHONES_RETAIN_MESSAGING_SERVICES import { r, cacheableData } from "../../../server/models"; import ownedPhoneNumber from "../../../server/api/lib/owned-phone-number"; @@ -89,8 +88,16 @@ const _editCampaignData = async (organization, campaign) => { const numbersNeeded = Math.ceil( (campaign.contactsCount || 0) / contactsPerNum.contactsPerPhoneNumber ); + // 4. which mode: + // previously: EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS vs. EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE + const manualMessageServiceMode = getConfig( + "EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE", + organization, + { truthy: 1 } + ); return { data: { + manualMessageServiceMode, inventoryPhoneNumberCounts, contactsAreaCodeCounts, phoneNumberCounts, @@ -101,12 +108,7 @@ const _editCampaignData = async (organization, campaign) => { fullyConfigured: // Two mutually exclusive modes: EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS vs. EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE Boolean(campaign.messageservice_sid) || - (numbersReserved >= numbersNeeded && - !getConfig( - "EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE", - organization, - { truthy: 1 } - )), + (numbersReserved >= numbersNeeded && !manualMessageServiceMode), unArchiveable: !campaign.use_own_messaging_service || campaign.messageservice_sid }; @@ -215,9 +217,6 @@ export async function onCampaignUpdateSignal({ return await _editCampaignData(organization, campaign); } -// TODO: react-component.js -// components/CampaignPhoneNumbersForm.jsx -// move containers/AdminCampaignStats:: showReleaseNumbers async function onVendorServiceFullyConfigured({ organization, serviceName }) { return { skipOrgMessageService: true diff --git a/src/extensions/service-managers/per-campaign-messageservices/react-component-campaignmessageservice.js b/src/extensions/service-managers/per-campaign-messageservices/react-component-campaignmessageservice.js index 09c585622..22ee3a714 100644 --- a/src/extensions/service-managers/per-campaign-messageservices/react-component-campaignmessageservice.js +++ b/src/extensions/service-managers/per-campaign-messageservices/react-component-campaignmessageservice.js @@ -3,10 +3,10 @@ import Switch from "@material-ui/core/Switch"; import FormControlLabel from "@material-ui/core/FormControlLabel"; import React from "react"; import Form from "react-formal"; -import GSForm from "./forms/GSForm"; -import GSTextField from "./forms/GSTextField"; -import GSSubmitButton from "./forms/GSSubmitButton"; -import CampaignFormSectionHeading from "./CampaignFormSectionHeading"; +import GSForm from "../../../components/forms/GSForm"; +import GSTextField from "../../../components/forms/GSTextField"; +import GSSubmitButton from "../../../components/forms/GSSubmitButton"; +import CampaignFormSectionHeading from "../../../components/CampaignFormSectionHeading"; import * as yup from "yup"; import cloneDeep from "lodash/cloneDeep"; diff --git a/src/extensions/service-managers/per-campaign-messageservices/react-component.js b/src/extensions/service-managers/per-campaign-messageservices/react-component.js index bca57296a..d735256b6 100644 --- a/src/extensions/service-managers/per-campaign-messageservices/react-component.js +++ b/src/extensions/service-managers/per-campaign-messageservices/react-component.js @@ -29,6 +29,8 @@ import CampaignFormSectionHeading from "../../../components/CampaignFormSectionH import LoadingIndicator from "../../../components/LoadingIndicator"; import theme from "../../../styles/theme"; +import CampaignMessagingServiceForm from "./react-component-campaignmessageservice"; + // import { dataTest } from "../lib/attributes"; /* eslint-disable no-nested-ternary */ diff --git a/src/extensions/service-vendors/bandwidth/react-component.js b/src/extensions/service-vendors/bandwidth/react-component.js index 7befc62a3..a37b5c938 100644 --- a/src/extensions/service-vendors/bandwidth/react-component.js +++ b/src/extensions/service-vendors/bandwidth/react-component.js @@ -64,7 +64,10 @@ export class OrgConfig extends React.Component { this.setState(value); }; - handleOpenDialog = () => this.setState({ dialogOpen: true }); + handleOpenDialog = () => { + console.log("bandwidth.handleopendialog", this.state); + this.setState({ dialogOpen: true }); + }; handleCloseDialog = () => this.setState({ dialogOpen: false }); @@ -202,13 +205,14 @@ export class OrgConfig extends React.Component {
- You can set Twilio API credentials specifically for this + You can set Bandwidth API credentials specifically for this Organization by entering them here. - + {config.autoConfigError && ( + + {config.autoConfigError} + + )} + Changing information here will break any campaigns that are - currently running. Do you want to contunue? + currently running. Do you want to continue? @@ -359,13 +367,12 @@ export class OrgConfig extends React.Component { > Cancel - + > + Save + diff --git a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js index 538dac345..b9379dcff 100644 --- a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js +++ b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js @@ -119,8 +119,15 @@ export async function updateConfig(oldConfig, config, organization) { } delete finalConfig.autoConfigError; } catch (err) { - console.log("bandwidth.updateConfig autoconfigure Error", err); - finalConfig.autoConfigError = "Auto-configuration failed"; + console.log( + "bandwidth.updateConfig autoconfigure Error", + err.message, + err.text, + "xxxx", + err + ); + finalConfig.autoConfigError = `Auto-configuration failed. ${err.message || + ""}`; } return finalConfig; From 11f95ea68b159c9ec89319781167737874ad4e63 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Mon, 14 Jun 2021 13:35:11 -0400 Subject: [PATCH 090/191] service-vendor/bandwidth: fix bandwidth setup bugs --- .../bandwidth/react-component.js | 23 ++++++++- .../bandwidth/setup-and-numbers.js | 48 ++++++++++++------- 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/extensions/service-vendors/bandwidth/react-component.js b/src/extensions/service-vendors/bandwidth/react-component.js index a37b5c938..381514593 100644 --- a/src/extensions/service-vendors/bandwidth/react-component.js +++ b/src/extensions/service-vendors/bandwidth/react-component.js @@ -222,7 +222,7 @@ export class OrgConfig extends React.Component { /> @@ -338,6 +338,27 @@ export class OrgConfig extends React.Component { label="Application Id" name="applicationId" /> +
+ {document.location.hostname === "localhost" ? ( + + You will need to deploy somewhere with a publicly + accessible url + + ) : ( + +
+ If you create the application manually, set the + Callback URL to:{" "} +
+ + {document.location.protocol} + {"//"} + {document.location.hostname}/bandwidth/ + {organizationId} + +
+ )} +
diff --git a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js index b9379dcff..085b38934 100644 --- a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js +++ b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js @@ -15,10 +15,10 @@ export async function getNumbersClient(organization, options) { (await getMessageServiceConfig("bandwidth", organization, { obscureSensitiveInformation: false })); - const password = await convertSecret( + const password = await getSecret( "bandwidthPassword", - organization, - config.password + config.password, + organization ); const client = new BandwidthNumbers.Client({ userName: config.userName, @@ -93,8 +93,6 @@ export async function updateConfig(oldConfig, config, organization) { }; // console.log('bandwdith finalConfig', finalConfig); - // TODO: test credentials with login - try { if ( !config.siteId || @@ -122,12 +120,12 @@ export async function updateConfig(oldConfig, config, organization) { console.log( "bandwidth.updateConfig autoconfigure Error", err.message, - err.text, + err.response && err.response.text, "xxxx", err ); finalConfig.autoConfigError = `Auto-configuration failed. ${err.message || - ""}`; + ""} ${(err.response && err.response.text) || ""}`; } return finalConfig; @@ -199,7 +197,7 @@ export async function createAccountBaseline(organization, options) { // 1. create sub-account/Site if (!config.siteId || (options && options.newEverything)) { const site = await BandwidthNumbers.Site.createAsync(client, { - name: `Spoke ${organization.name} (Subaccount)`, + name: `Spoke org${organization.id} ${organization.name} (Subaccount)`, address: { houseNumber: config.houseNumber, streetName: config.streetName, @@ -218,7 +216,11 @@ export async function createAccountBaseline(organization, options) { if (!config.sipPeerId || (options && options.newEverything)) { location = await BandwidthNumbers.SipPeer.createAsync(client, { siteId, - peerName: `Spoke ${organization.name} (Location)`, + // Peer Names can only contain alphanumerics + peerName: `Spoke org${organization.id} ${organization.name.replace( + /\W/, + "" + )}`, isDefaultPeer: true }); configChanges.sipPeerId = location.id; @@ -232,16 +234,26 @@ export async function createAccountBaseline(organization, options) { // 3. Enable SMS and MMS on location // Note: create your own if you want different parameters (enforced) await location.createSmsSettingsAsync({ - tollFree: true, - protocol: "http", - zone1: true, - zone2: false, - zone3: false, - zone4: false, - zone5: false + sipPeerSmsFeatureSettings: { + tollFree: true, + protocol: "http", + zone1: true, + zone2: false, + zone3: false, + zone4: false, + zone5: false + }, + httpSettings: {} }); await location.createMmsSettingsAsync({ - protocol: "http" + mmsSettings: { + protocol: "HTTP" + }, + protocols: { + HTTP: { + httpSettings: {} + } + } }); return configChanges; } @@ -252,7 +264,7 @@ export async function createMessagingService( serviceConfig ) { const baseUrl = getConfig("BASE_URL", organization); - if (!baseUrl) { + if (!baseUrl || /\/\/localhost(:|\/)/.test(baseUrl)) { return; } const { client, config } = await getNumbersClient(organization, { From f19855143d08aab2a200fc85223f6c33086f19d9 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Mon, 14 Jun 2021 14:30:25 -0400 Subject: [PATCH 091/191] bandwidth: restrict account name to 50char max --- src/extensions/service-vendors/bandwidth/setup-and-numbers.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js index 085b38934..5ea83cce0 100644 --- a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js +++ b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js @@ -197,7 +197,8 @@ export async function createAccountBaseline(organization, options) { // 1. create sub-account/Site if (!config.siteId || (options && options.newEverything)) { const site = await BandwidthNumbers.Site.createAsync(client, { - name: `Spoke org${organization.id} ${organization.name} (Subaccount)`, + // subaccount names max=50 characters + name: `Spoke org${organization.id} ${organization.name.substr(0, 38)}`, address: { houseNumber: config.houseNumber, streetName: config.streetName, From 42b52e3020402e57f22f9c3dce75921d199bff2d Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Mon, 14 Jun 2021 19:32:48 -0400 Subject: [PATCH 092/191] service-vendor/bandwidth: fix more setup bugs and use org hmac for bandwidth pw --- .../service-vendors/bandwidth/index.js | 9 +++-- .../bandwidth/setup-and-numbers.js | 37 ++++++++++++------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/extensions/service-vendors/bandwidth/index.js b/src/extensions/service-vendors/bandwidth/index.js index 916577b18..6d8ac7e21 100644 --- a/src/extensions/service-vendors/bandwidth/index.js +++ b/src/extensions/service-vendors/bandwidth/index.js @@ -13,6 +13,8 @@ export { fullyConfigured } from "./setup-and-numbers"; +import { webhookBasicAuthPw } from "./setup-and-numbers"; + import { sendMessage, handleIncomingMessage, @@ -66,9 +68,10 @@ export function addServerEndpoints(addPostRoute) { .toString() .split(":"); - // TODO: better login/password auto-creation/context - // await verifyBandwidthServer(password) - if (login !== "bandwidth.com" || password !== "testtest") { + if ( + login !== "bandwidth.com" || + password !== webhookBasicAuthPw(req.params.orgId || "") + ) { res.set("WWW-Authenticate", 'Basic realm="401"'); res.status(401).send("Authentication required."); return; diff --git a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js index 5ea83cce0..0b9f03277 100644 --- a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js +++ b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js @@ -1,3 +1,4 @@ +import { createHmac } from "crypto"; import BandwidthNumbers from "@bandwidth/numbers"; import BandwidthMessaging from "@bandwidth/messaging"; @@ -28,6 +29,14 @@ export async function getNumbersClient(organization, options) { return { client, config }; } +export const webhookBasicAuthPw = organizationId => { + // password has a max of 63 chars + const hmac = createHmac("sha256", getConfig("SESSION_SECRET") || ""); + hmac.update(getConfig("BASE_URL") || ""); + hmac.update(String(organizationId)); + return hmac.digest("base64"); +}; + export const getServiceConfig = async ( serviceConfig, organization, @@ -40,8 +49,6 @@ export const getServiceConfig = async ( } = options; let password; if (serviceConfig) { - // Note, allows unencrypted auth tokens to be (manually) stored in the db - // @todo: decide if this is necessary, or if UI/envars is sufficient. if (serviceConfig.password) { password = obscureSensitiveInformation ? "" @@ -111,7 +118,7 @@ export async function updateConfig(oldConfig, config, organization) { if (!config.applicationId) { finalConfig.applicationId = await createMessagingService( organization, - `Spoke app, org Id=${organization.id}`, + `Spoke app, org${organization.id}`, finalConfig ); } @@ -219,7 +226,7 @@ export async function createAccountBaseline(organization, options) { siteId, // Peer Names can only contain alphanumerics peerName: `Spoke org${organization.id} ${organization.name.replace( - /\W/, + /\W/g, "" )}`, isDefaultPeer: true @@ -272,25 +279,27 @@ export async function createMessagingService( serviceConfig }); // 1. create application + const callbackUrl = `${baseUrl}/bandwidth/${(organization && + organization.id) || + ""}`; const application = await BandwidthNumbers.Application.createMessagingApplicationAsync( client, { appName: friendlyName || "Spoke app", - msgCallbackUrl: `${baseUrl}/bandwidth/${(organization && - organization.id) || - ""}`, + callbackUrl, + msgCallbackUrl: callbackUrl, + inboundCallbackUrl: callbackUrl, + outboundCallbackUrl: callbackUrl, callbackCreds: { userId: "bandwidth.com", - password: "testtest" // TODO: see index.js + password: webhookBasicAuthPw(organization.id) }, - requestedCallbackTypes: [ - "message-delivered", - "message-failed", - "message-sending" - ].map(c => ({ callbackType: c })) + requestedCallbackTypes: { + callbackType: ["message-sending", "message-delivered", "message-failed"] + } } ); - console.log("bandwidth createMessagingService", JSON.stringify(result)); + console.log("bandwidth createMessagingService", JSON.stringify(application)); // 2. assign application to subaccount|site and location|sippeer const location = await BandwidthNumbers.SipPeer.getAsync( client, From 4bd83a4eb374c9ac146163c957478af3fe3fa9ca Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Tue, 15 Jun 2021 10:56:00 -0400 Subject: [PATCH 093/191] service-vendors/bandwidth: syncAccountNumbers on first load --- .../bandwidth/react-component.js | 61 +++++++++---- .../bandwidth/setup-and-numbers.js | 91 ++++++++++++++++--- src/workers/jobs.js | 1 + 3 files changed, 118 insertions(+), 35 deletions(-) diff --git a/src/extensions/service-vendors/bandwidth/react-component.js b/src/extensions/service-vendors/bandwidth/react-component.js index 381514593..82da81206 100644 --- a/src/extensions/service-vendors/bandwidth/react-component.js +++ b/src/extensions/service-vendors/bandwidth/react-component.js @@ -38,28 +38,40 @@ export class OrgConfig extends React.Component { sipPeerId, applicationId } = this.props.config; - const allSet = - userName && password && accountId && siteId && sipPeerId && applicationId; - this.state = { allSet, ...this.props.config, country: "United States" }; + const allSet = Boolean( + userName && password && accountId && siteId && sipPeerId && applicationId + ); + this.state = { ...this.props.config, country: "United States" }; this.props.onAllSetChanged(allSet); + console.log("constructor"); } - /* - const { - accountSid: prevAccountSid, - authToken: prevAuthToken, - messageServiceSid: prevMessageServiceSid - } = prevProps.config; - const prevAllSet = prevAccountSid && prevAuthToken && prevMessageServiceSid; - - const { accountSid, authToken, messageServiceSid } = this.props.config; - const allSet = accountSid && authToken && messageServiceSid; - - if (!!prevAllSet !== !!allSet) { - this.props.onAllSetChanged(allSet); + UNSAFE_componentWillReceiveProps(nextProps) { + // new results after saving first setup + const newData = {}; + if ( + nextProps.config.siteId && + this.props.config.siteId !== this.state.siteId + ) { + newData.siteId = nextProps.config.sipPeerId; + } + if ( + nextProps.config.sipPeerId && + this.props.config.sipPeerId !== this.state.sipPeerId + ) { + newData.sipPeerId = nextProps.config.sipPeerId; + } + if ( + nextProps.config.applicationId && + this.props.config.applicationId !== this.state.applicationId + ) { + newData.applicationId = nextProps.config.applicationId; + } + if (Object.values(newData).length) { + this.setState(newData); } } - */ + onFormChange = value => { this.setState(value); }; @@ -77,6 +89,7 @@ export class OrgConfig extends React.Component { config.password = password; } let newError; + this.handleCloseDialog(); try { await this.props.onSubmit(config); this.setState({ @@ -92,13 +105,21 @@ export class OrgConfig extends React.Component { } this.setState({ error: newError }); } - this.handleCloseDialog(); }; render() { const { organizationId, inlineStyles, styles, config } = this.props; - const { accountSid, authToken, messageServiceSid } = config; - const allSet = accountSid && authToken && messageServiceSid; + const { + userName, + password, + accountId, + siteId, + sipPeerId, + applicationId + } = config; + const allSet = Boolean( + userName && password && accountId && siteId && sipPeerId && applicationId + ); let baseUrl = "http://base"; if (typeof window !== "undefined") { baseUrl = window.location.origin; diff --git a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js index 0b9f03277..41ecce507 100644 --- a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js +++ b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js @@ -1,26 +1,41 @@ import { createHmac } from "crypto"; +import remove from "lodash/remove"; + import BandwidthNumbers from "@bandwidth/numbers"; import BandwidthMessaging from "@bandwidth/messaging"; import { log } from "../../../lib"; import { getFormattedPhoneNumber } from "../../../lib/phone-format"; import { sleep } from "../../../workers/lib"; +import { r } from "../../../server/models"; import { getConfig } from "../../../server/api/lib/config"; import { getSecret, convertSecret } from "../../secret-manager"; import { getMessageServiceConfig, getConfigKey } from "../service_map"; export async function getNumbersClient(organization, options) { - const config = - (options && options.serviceConfig) || - (await getMessageServiceConfig("bandwidth", organization, { + let config; + let password; + if (options && options.serviceConfig) { + config = options.serviceConfig; + console.log("bandwidth.getNumbersClient.serviceConfig", config); + password = await getSecret( + "bandwidthPassword", + config.password, + organization + ); + } else { + config = await getMessageServiceConfig("bandwidth", organization, { obscureSensitiveInformation: false - })); - const password = await getSecret( - "bandwidthPassword", - config.password, - organization - ); + }); + console.log( + "bandwidth.getNumbersClient.getMessageServiceConfig", + config.userName, + config.accountId + ); + password = config.password; + } + const client = new BandwidthNumbers.Client({ userName: config.userName, password: password, @@ -52,10 +67,12 @@ export const getServiceConfig = async ( if (serviceConfig.password) { password = obscureSensitiveInformation ? "" - : await getSecret( + : /// TODO: checkout args order here for getSecret? also will this be redundant to call in getNumbersClient? + /// then conflicts with saving-initial vs. load + await getSecret( "bandwidthPassword", - organization, - serviceConfig.password + serviceConfig.password, + organization ); } else { password = obscureSensitiveInformation @@ -123,6 +140,11 @@ export async function updateConfig(oldConfig, config, organization) { ); } delete finalConfig.autoConfigError; + if (true || config.sipPeerId !== finalConfig.sipPeerId) { + await syncAccountNumbers(organization, { + serviceConfig: finalConfig + }); + } } catch (err) { console.log( "bandwidth.updateConfig autoconfigure Error", @@ -171,8 +193,8 @@ export async function buyNumbersInAreaCode( area_code: cn.telephoneNumber.fullNumber.slice(0, 3), phone_number: getFormattedPhoneNumber(cn.telephoneNumber.fullNumber), service: "bandwidth", - allocated_to_id: config.sipPeerId, - service_id: cn.telephoneNumber.fullNumber, + allocated_to_id: null, + service_id: `${config.sipPeerId}.${cn.telephoneNumber.fullNumber}`, allocated_at: new Date() })); await r.knex("owned_phone_number").insert(newNumbers); @@ -187,6 +209,45 @@ export async function buyNumbersInAreaCode( return totalPurchased; } +export async function syncAccountNumbers(organization, options) { + const { client, config } = await getNumbersClient(organization, options); + if (!config.siteId || !config.sipPeerId) { + return; + } + const sipPeer = await BandwidthNumbers.SipPeer.getAsync( + client, + config.siteId, + config.sipPeerId + ); + const telephoneNumbers = await sipPeer.getTnsAsync(); + // [ { fullNumber: '2135551234' }, .... ] + console.log("syncAccountNumbers", telephoneNumbers.length); + if (telephoneNumbers.length) { + const nums = telephoneNumbers.map(tn => `+1${tn.fullNumber}`); + const existingNums = await r + .knex("owned_phone_number") + .where("service", "bandwidth") + .whereIn("phone_number", nums) + .pluck("phone_number"); + const newNums = existingNums.length + ? remove(nums, e => existingNums.indexOf(e) + 1) + : nums; + if (newNums.length) { + console.log("Bandwidth, new numbers to load", newNums.length, newNums[0]); + const newNumbers = newNums.map(tn => ({ + organization_id: organization.id, + area_code: tn.slice(2, 5), + phone_number: tn, + service: "bandwidth", + allocated_to_id: null, + service_id: `${config.sipPeerId}.${tn.slice(2)}`, + allocated_at: new Date() + })); + await r.knex("owned_phone_number").insert(newNumbers); + } + } +} + export async function deleteNumbersInAreaCode(organization, areaCode) { // TODO // disconnect (i.e. delete/drop) numbers @@ -244,7 +305,7 @@ export async function createAccountBaseline(organization, options) { await location.createSmsSettingsAsync({ sipPeerSmsFeatureSettings: { tollFree: true, - protocol: "http", + protocol: "HTTP", zone1: true, zone2: false, zone3: false, diff --git a/src/workers/jobs.js b/src/workers/jobs.js index 348343d62..81a5cc532 100644 --- a/src/workers/jobs.js +++ b/src/workers/jobs.js @@ -1254,6 +1254,7 @@ export async function buyPhoneNumbers(job) { }); } catch (err) { log.error(`JOB ${job.id} FAILED: ${err.message}`, err); + console.log("full job error", err); } finally { await defensivelyDeleteJob(job); } From e2c079a88ea3cb8d6d546a8dadfae4f85277d01e Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Tue, 15 Jun 2021 16:41:22 -0400 Subject: [PATCH 094/191] service-managers/numpicker-basic: initial stub --- src/extensions/service-managers/index.js | 19 ++++--- .../service-managers/numpicker-basic/index.js | 53 +++++++++++++++++++ .../bandwidth/setup-and-numbers.js | 2 +- 3 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 src/extensions/service-managers/numpicker-basic/index.js diff --git a/src/extensions/service-managers/index.js b/src/extensions/service-managers/index.js index dbcf0f460..f6027a785 100644 --- a/src/extensions/service-managers/index.js +++ b/src/extensions/service-managers/index.js @@ -40,17 +40,24 @@ export async function processServiceManagers( typeof m[funcName] === "function" && (!specificServiceManagerName || m.name === specificServiceManagerName) ); - const resultArray = []; - // explicitly process these in order in case the order matters + const serviceManagerData = {}; + // Explicitly process these in order in case the order matters + // Current serviceManagerData state is passed along, so a later serviceManager + // can decide to do something if a previous one hasn't yet. for (let i = 0, l = funkyManagers.length; i < l; i++) { - resultArray.push( - await funkyManagers[i][funcName]({ organization, ...funcArgs }) - ); + const result = await funkyManagers[i][funcName]({ + organization, + serviceManagerData, + ...funcArgs + }); + if (result) { + Object.assign(serviceManagerData, result); + } } // NOTE: some methods pass a shared modifiable object, e.g. 'saveData' // that might be modified in-place, rather than the resultArray // being important. - return resultArray.reduce((a, b) => Object.assign(a, b), {}); + return serviceManagerData; } export async function getServiceManagerData( diff --git a/src/extensions/service-managers/numpicker-basic/index.js b/src/extensions/service-managers/numpicker-basic/index.js new file mode 100644 index 000000000..55636fa84 --- /dev/null +++ b/src/extensions/service-managers/numpicker-basic/index.js @@ -0,0 +1,53 @@ +/// All functions are OPTIONAL EXCEPT metadata() and const name=. +/// DO NOT IMPLEMENT ANYTHING YOU WILL NOT USE -- the existence of a function adds behavior/UI (sometimes costly) + +import { r, cacheableData } from "../../../server/models"; + +export const name = "numpicker-basic"; + +export const metadata = () => ({ + // set canSpendMoney=true, if this extension can lead to (additional) money being spent + // if it can, which operations below can trigger money being spent? + displayName: "Basic Number Picker", + description: + "Picks a number available in owned_phone_number table for the service to send messages with. Defaults to basic rotation.", + canSpendMoney: false, + moneySpendingOperations: [], + supportsOrgConfig: true, + supportsCampaignConfig: false +}); + +export async function onMessageSend({ + message, + contact, + organization, + campaign, + serviceManagerData +}) { + if ( + message.user_number || + (serviceManagerData && serviceManagerData.user_number) + ) { + // This is meant as a fallback -- if another serviceManager already + // chose a phone number then don't change anything + return; + } + const serviceName = cacheableData.organization.getMessageService( + organization + ); + const selectedPhone = await r + .knex("owned_phone_number") + .where({ service: serviceName, organization_id: organization.id }) + .whereNull("allocated_to_id") + .orderByRaw("random()") + .select("phone_number") + .first(); + // TODO: caching + // TODO: something better than pure rotation -- maybe with caching we use metrics + // based on sad deliveryreports + if (selectedPhone && selectedPhone.phone_number) { + return { user_number: selectedPhone.phone_number }; + } else { + // TODO: what should we do if there's no result? + } +} diff --git a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js index 41ecce507..85b8b1cf3 100644 --- a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js +++ b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js @@ -97,7 +97,7 @@ export async function fullyConfigured(organization) { ) { return false; } - // TODO: also needs some number to send with + // TODO: also needs some number to send with numpicker in servicemanagers return true; } From 746fda0083b076f6c3a396112467f835e8e99a65 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Tue, 15 Jun 2021 18:39:46 -0400 Subject: [PATCH 095/191] update yarn.lock --- yarn.lock | 3221 +++++++++++++++++++++++++++-------------------------- 1 file changed, 1657 insertions(+), 1564 deletions(-) diff --git a/yarn.lock b/yarn.lock index f326f63d0..2d554b08c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14,6 +14,13 @@ needle "^1.4.2" passport-oauth2 "^1.3.0" +"@apimatic/schema@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@apimatic/schema/-/schema-0.4.1.tgz#cb7a122895846638b01f402cc4ca1a32f656aa11" + integrity sha512-KdGp4GaC0sTlcwshahvqZ8OrB/QEM99lxm3sEAo5JgVQP3XH0y/+zeguV8OZhiXRsHERoVZvcI4rKBSHcL84gQ== + dependencies: + lodash.flatten "^4.4.0" + "@babel/code-frame@7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" @@ -21,17 +28,17 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" - integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35", "@babel/code-frame@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb" + integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw== dependencies: - "@babel/highlight" "^7.10.4" + "@babel/highlight" "^7.14.5" -"@babel/compat-data@^7.12.5", "@babel/compat-data@^7.12.7": - version "7.12.7" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.12.7.tgz#9329b4782a7d6bbd7eef57e11addf91ee3ef1e41" - integrity sha512-YaxPMGs/XIWtYqrdEOZOCPsVWfEoriXopnsz3/i7apYPXQ3698UFhS6dVT1KN5qOsWmVgw/FOrmQgpRaZayGsw== +"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.14.5.tgz#8ef4c18e58e801c5c95d3c1c0f2874a2680fadea" + integrity sha512-kixrYn4JwfAVPa0f2yfzc2AWti6WRRyO3XjWW5PJAvtE11qhSayrrcrEnee05KAtNaPC+EwehE8Qt1UedEVB8w== "@babel/core@7.2.2": version "7.2.2" @@ -54,250 +61,272 @@ source-map "^0.5.0" "@babel/core@^7.1.6", "@babel/core@^7.4.5": - version "7.12.10" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.10.tgz#b79a2e1b9f70ed3d84bbfb6d8c4ef825f606bccd" - integrity sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.12.10" - "@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" + version "7.14.6" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.14.6.tgz#e0814ec1a950032ff16c13a2721de39a8416fcab" + integrity sha512-gJnOEWSqTk96qG5BoIrl5bVtc23DCycmIePPYnamY9RboYdI4nFy5vAQMSl81O5K/W0sLDWfGysnOECC+KUUCA== + dependencies: + "@babel/code-frame" "^7.14.5" + "@babel/generator" "^7.14.5" + "@babel/helper-compilation-targets" "^7.14.5" + "@babel/helper-module-transforms" "^7.14.5" + "@babel/helpers" "^7.14.6" + "@babel/parser" "^7.14.6" + "@babel/template" "^7.14.5" + "@babel/traverse" "^7.14.5" + "@babel/types" "^7.14.5" convert-source-map "^1.7.0" debug "^4.1.0" - gensync "^1.0.0-beta.1" + gensync "^1.0.0-beta.2" json5 "^2.1.2" - lodash "^4.17.19" - semver "^5.4.1" + semver "^6.3.0" source-map "^0.5.0" -"@babel/generator@^7.12.10", "@babel/generator@^7.12.11", "@babel/generator@^7.2.2": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.11.tgz#98a7df7b8c358c9a37ab07a24056853016aba3af" - integrity sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA== +"@babel/generator@^7.14.5", "@babel/generator@^7.2.2": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.5.tgz#848d7b9f031caca9d0cd0af01b063f226f52d785" + integrity sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA== dependencies: - "@babel/types" "^7.12.11" + "@babel/types" "^7.14.5" jsesc "^2.5.1" source-map "^0.5.0" -"@babel/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.10.4", "@babel/helper-annotate-as-pure@^7.12.10": - version "7.12.10" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz#54ab9b000e60a93644ce17b3f37d313aaf1d115d" - integrity sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ== +"@babel/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz#7bf478ec3b71726d56a8ca5775b046fc29879e61" + integrity sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA== dependencies: - "@babel/types" "^7.12.10" + "@babel/types" "^7.14.5" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz#bb0b75f31bf98cbf9ff143c1ae578b87274ae1a3" - integrity sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg== +"@babel/helper-builder-binary-assignment-operator-visitor@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.14.5.tgz#b939b43f8c37765443a19ae74ad8b15978e0a191" + integrity sha512-YTA/Twn0vBXDVGJuAX6PwW7x5zQei1luDDo2Pl6q1qZ7hVNl0RZrhHCQG/ArGpR29Vl7ETiB8eJyrvpuRp300w== dependencies: - "@babel/helper-explode-assignable-expression" "^7.10.4" - "@babel/types" "^7.10.4" + "@babel/helper-explode-assignable-expression" "^7.14.5" + "@babel/types" "^7.14.5" -"@babel/helper-compilation-targets@^7.12.5": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz#cb470c76198db6a24e9dbc8987275631e5d29831" - integrity sha512-+qH6NrscMolUlzOYngSBMIOQpKUGPPsc61Bu5W10mg84LxZ7cmvnBHzARKbDoFxVvqqAbj6Tg6N7bSrWSPXMyw== +"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.5.tgz#7a99c5d0967911e972fe2c3411f7d5b498498ecf" + integrity sha512-v+QtZqXEiOnpO6EYvlImB6zCD2Lel06RzOPzmkz/D/XgQiUu3C/Jb1LOqSt/AIA34TYi/Q+KlT8vTQrgdxkbLw== dependencies: - "@babel/compat-data" "^7.12.5" - "@babel/helper-validator-option" "^7.12.1" - browserslist "^4.14.5" - semver "^5.5.0" + "@babel/compat-data" "^7.14.5" + "@babel/helper-validator-option" "^7.14.5" + browserslist "^4.16.6" + semver "^6.3.0" -"@babel/helper-create-class-features-plugin@^7.12.1", "@babel/helper-create-class-features-plugin@^7.3.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz#3c45998f431edd4a9214c5f1d3ad1448a6137f6e" - integrity sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w== +"@babel/helper-create-class-features-plugin@^7.14.5", "@babel/helper-create-class-features-plugin@^7.14.6", "@babel/helper-create-class-features-plugin@^7.3.0": + version "7.14.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.6.tgz#f114469b6c06f8b5c59c6c4e74621f5085362542" + integrity sha512-Z6gsfGofTxH/+LQXqYEK45kxmcensbzmk/oi8DmaQytlQCgqNZt9XQF8iqlI/SeXWVjaMNxvYvzaYw+kh42mDg== dependencies: - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-member-expression-to-functions" "^7.12.1" - "@babel/helper-optimise-call-expression" "^7.10.4" - "@babel/helper-replace-supers" "^7.12.1" - "@babel/helper-split-export-declaration" "^7.10.4" + "@babel/helper-annotate-as-pure" "^7.14.5" + "@babel/helper-function-name" "^7.14.5" + "@babel/helper-member-expression-to-functions" "^7.14.5" + "@babel/helper-optimise-call-expression" "^7.14.5" + "@babel/helper-replace-supers" "^7.14.5" + "@babel/helper-split-export-declaration" "^7.14.5" -"@babel/helper-create-regexp-features-plugin@^7.12.1": - version "7.12.7" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.7.tgz#2084172e95443fa0a09214ba1bb328f9aea1278f" - integrity sha512-idnutvQPdpbduutvi3JVfEgcVIHooQnhvhx0Nk9isOINOIGYkZea1Pk2JlJRiUnMefrlvr0vkByATBY/mB4vjQ== +"@babel/helper-create-regexp-features-plugin@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.5.tgz#c7d5ac5e9cf621c26057722fb7a8a4c5889358c4" + integrity sha512-TLawwqpOErY2HhWbGJ2nZT5wSkR192QpN+nBg1THfBfftrlvOh+WbhrxXCH4q4xJ9Gl16BGPR/48JA+Ryiho/A== dependencies: - "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-annotate-as-pure" "^7.14.5" regexpu-core "^4.7.1" -"@babel/helper-define-map@^7.1.0", "@babel/helper-define-map@^7.10.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz#b53c10db78a640800152692b13393147acb9bb30" - integrity sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ== +"@babel/helper-define-map@^7.1.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.14.5.tgz#c1579b056a70a7da2f4a27ee492ec58f5ba3c439" + integrity sha512-spfQRnoChdYWwyFetQDBSDBgH42VskaquRI52kbLei5MjV7s3NPq30/sh2S3YdT20Ku/ZpaNnTVgmDo20NWylg== dependencies: - "@babel/helper-function-name" "^7.10.4" - "@babel/types" "^7.10.5" - lodash "^4.17.19" + "@babel/helper-function-name" "^7.14.5" + "@babel/types" "^7.14.5" -"@babel/helper-explode-assignable-expression@^7.10.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.12.1.tgz#8006a466695c4ad86a2a5f2fb15b5f2c31ad5633" - integrity sha512-dmUwH8XmlrUpVqgtZ737tK88v07l840z9j3OEhCLwKTkjlvKpfqXVIZ0wpK3aeOxspwGrf/5AP5qLx4rO3w5rA== - dependencies: - "@babel/types" "^7.12.1" - -"@babel/helper-function-name@^7.1.0", "@babel/helper-function-name@^7.10.4", "@babel/helper-function-name@^7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz#1fd7738aee5dcf53c3ecff24f1da9c511ec47b42" - integrity sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA== - dependencies: - "@babel/helper-get-function-arity" "^7.12.10" - "@babel/template" "^7.12.7" - "@babel/types" "^7.12.11" - -"@babel/helper-get-function-arity@^7.12.10": - version "7.12.10" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz#b158817a3165b5faa2047825dfa61970ddcc16cf" - integrity sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag== - dependencies: - "@babel/types" "^7.12.10" - -"@babel/helper-hoist-variables@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz#d49b001d1d5a68ca5e6604dda01a6297f7c9381e" - integrity sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-member-expression-to-functions@^7.12.1", "@babel/helper-member-expression-to-functions@^7.12.7": - version "7.12.7" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz#aa77bd0396ec8114e5e30787efa78599d874a855" - integrity sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw== - dependencies: - "@babel/types" "^7.12.7" - -"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.12.5": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz#1bfc0229f794988f76ed0a4d4e90860850b54dfb" - integrity sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA== - dependencies: - "@babel/types" "^7.12.5" - -"@babel/helper-module-transforms@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz#7954fec71f5b32c48e4b303b437c34453fd7247c" - integrity sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w== - dependencies: - "@babel/helper-module-imports" "^7.12.1" - "@babel/helper-replace-supers" "^7.12.1" - "@babel/helper-simple-access" "^7.12.1" - "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/helper-validator-identifier" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.12.1" - "@babel/types" "^7.12.1" - lodash "^4.17.19" - -"@babel/helper-optimise-call-expression@^7.0.0", "@babel/helper-optimise-call-expression@^7.10.4", "@babel/helper-optimise-call-expression@^7.12.10": - version "7.12.10" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz#94ca4e306ee11a7dd6e9f42823e2ac6b49881e2d" - integrity sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ== - dependencies: - "@babel/types" "^7.12.10" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" - integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== - -"@babel/helper-remap-async-to-generator@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz#8c4dbbf916314f6047dc05e6a2217074238347fd" - integrity sha512-9d0KQCRM8clMPcDwo8SevNs+/9a8yWVVmaE80FGJcEP8N1qToREmWEGnBn8BUlJhYRFz6fqxeRL1sl5Ogsed7A== - dependencies: - "@babel/helper-annotate-as-pure" "^7.10.4" - "@babel/helper-wrap-function" "^7.10.4" - "@babel/types" "^7.12.1" - -"@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.12.1": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz#ea511658fc66c7908f923106dd88e08d1997d60d" - integrity sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.12.7" - "@babel/helper-optimise-call-expression" "^7.12.10" - "@babel/traverse" "^7.12.10" - "@babel/types" "^7.12.11" - -"@babel/helper-simple-access@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz#32427e5aa61547d38eb1e6eaf5fd1426fdad9136" - integrity sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA== - dependencies: - "@babel/types" "^7.12.1" - -"@babel/helper-skip-transparent-expression-wrappers@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz#462dc63a7e435ade8468385c63d2b84cce4b3cbf" - integrity sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA== - dependencies: - "@babel/types" "^7.12.1" - -"@babel/helper-split-export-declaration@^7.0.0", "@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0", "@babel/helper-split-export-declaration@^7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz#1b4cc424458643c47d37022223da33d76ea4603a" - integrity sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g== - dependencies: - "@babel/types" "^7.12.11" - -"@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" - integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== - -"@babel/helper-validator-option@^7.12.1", "@babel/helper-validator-option@^7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz#d66cb8b7a3e7fe4c6962b32020a131ecf0847f4f" - integrity sha512-TBFCyj939mFSdeX7U7DDj32WtzYY7fDcalgq8v3fBZMNOJQNn7nOYzMaUCiPxPYfCup69mtIpqlKgMZLvQ8Xhw== - -"@babel/helper-wrap-function@^7.10.4": - version "7.12.3" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.12.3.tgz#3332339fc4d1fbbf1c27d7958c27d34708e990d9" - integrity sha512-Cvb8IuJDln3rs6tzjW3Y8UeelAOdnpB8xtQ4sme2MSZ9wOxrbThporC0y/EtE16VAtoyEfLM404Xr1e0OOp+ow== +"@babel/helper-define-polyfill-provider@^0.2.2": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.3.tgz#0525edec5094653a282688d34d846e4c75e9c0b6" + integrity sha512-RH3QDAfRMzj7+0Nqu5oqgO5q9mFtQEVvCRsi8qCEfzLR9p2BHfn5FzhSB2oj1fF7I2+DcTORkYaQ6aTR9Cofew== dependencies: - "@babel/helper-function-name" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helpers@^7.12.5", "@babel/helpers@^7.2.0": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.5.tgz#1a1ba4a768d9b58310eda516c449913fe647116e" - integrity sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA== + "@babel/helper-compilation-targets" "^7.13.0" + "@babel/helper-module-imports" "^7.12.13" + "@babel/helper-plugin-utils" "^7.13.0" + "@babel/traverse" "^7.13.0" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + semver "^6.1.2" + +"@babel/helper-explode-assignable-expression@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.14.5.tgz#8aa72e708205c7bb643e45c73b4386cdf2a1f645" + integrity sha512-Htb24gnGJdIGT4vnRKMdoXiOIlqOLmdiUYpAQ0mYfgVT/GDm8GOYhgi4GL+hMKrkiPRohO4ts34ELFsGAPQLDQ== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-function-name@^7.1.0", "@babel/helper-function-name@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz#89e2c474972f15d8e233b52ee8c480e2cfcd50c4" + integrity sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ== dependencies: - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.12.5" - "@babel/types" "^7.12.5" + "@babel/helper-get-function-arity" "^7.14.5" + "@babel/template" "^7.14.5" + "@babel/types" "^7.14.5" -"@babel/highlight@^7.0.0", "@babel/highlight@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" - integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" +"@babel/helper-get-function-arity@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz#25fbfa579b0937eee1f3b805ece4ce398c431815" + integrity sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-hoist-variables@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz#e0dd27c33a78e577d7c8884916a3e7ef1f7c7f8d" + integrity sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-member-expression-to-functions@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.5.tgz#d5c70e4ad13b402c95156c7a53568f504e2fb7b8" + integrity sha512-UxUeEYPrqH1Q/k0yRku1JE7dyfyehNwT6SVkMHvYvPDv4+uu627VXBckVj891BO8ruKBkiDoGnZf4qPDD8abDQ== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz#6d1a44df6a38c957aa7c312da076429f11b422f3" + integrity sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-module-transforms@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.14.5.tgz#7de42f10d789b423eb902ebd24031ca77cb1e10e" + integrity sha512-iXpX4KW8LVODuAieD7MzhNjmM6dzYY5tfRqT+R9HDXWl0jPn/djKmA+G9s/2C2T9zggw5tK1QNqZ70USfedOwA== + dependencies: + "@babel/helper-module-imports" "^7.14.5" + "@babel/helper-replace-supers" "^7.14.5" + "@babel/helper-simple-access" "^7.14.5" + "@babel/helper-split-export-declaration" "^7.14.5" + "@babel/helper-validator-identifier" "^7.14.5" + "@babel/template" "^7.14.5" + "@babel/traverse" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/helper-optimise-call-expression@^7.0.0", "@babel/helper-optimise-call-expression@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz#f27395a8619e0665b3f0364cddb41c25d71b499c" + integrity sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9" + integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ== + +"@babel/helper-remap-async-to-generator@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.14.5.tgz#51439c913612958f54a987a4ffc9ee587a2045d6" + integrity sha512-rLQKdQU+HYlxBwQIj8dk4/0ENOUEhA/Z0l4hN8BexpvmSMN9oA9EagjnhnDpNsRdWCfjwa4mn/HyBXO9yhQP6A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.14.5" + "@babel/helper-wrap-function" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.14.5.tgz#0ecc0b03c41cd567b4024ea016134c28414abb94" + integrity sha512-3i1Qe9/8x/hCHINujn+iuHy+mMRLoc77b2nI9TB0zjH1hvn9qGlXjWlggdwUcju36PkPCy/lpM7LLUdcTyH4Ow== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.14.5" + "@babel/helper-optimise-call-expression" "^7.14.5" + "@babel/traverse" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/helper-simple-access@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.14.5.tgz#66ea85cf53ba0b4e588ba77fc813f53abcaa41c4" + integrity sha512-nfBN9xvmCt6nrMZjfhkl7i0oTV3yxR4/FztsbOASyTvVcoYd0TRHh7eMLdlEcCqobydC0LAF3LtC92Iwxo0wyw== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-skip-transparent-expression-wrappers@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.14.5.tgz#96f486ac050ca9f44b009fbe5b7d394cab3a0ee4" + integrity sha512-dmqZB7mrb94PZSAOYtr+ZN5qt5owZIAgqtoTuqiFbHFtxgEcmQlRJVI+bO++fciBunXtB6MK7HrzrfcAzIz2NQ== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-split-export-declaration@^7.0.0", "@babel/helper-split-export-declaration@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz#22b23a54ef51c2b7605d851930c1976dd0bc693a" + integrity sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-validator-identifier@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz#d0f0e277c512e0c938277faa85a3968c9a44c0e8" + integrity sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg== + +"@babel/helper-validator-option@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3" + integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow== + +"@babel/helper-wrap-function@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.14.5.tgz#5919d115bf0fe328b8a5d63bcb610f51601f2bff" + integrity sha512-YEdjTCq+LNuNS1WfxsDCNpgXkJaIyqco6DAelTUjT4f2KIWC1nBcaCaSdHTBqQVLnTBexBcVcFhLSU1KnYuePQ== + dependencies: + "@babel/helper-function-name" "^7.14.5" + "@babel/template" "^7.14.5" + "@babel/traverse" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/helpers@^7.14.6", "@babel/helpers@^7.2.0": + version "7.14.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.14.6.tgz#5b58306b95f1b47e2a0199434fa8658fa6c21635" + integrity sha512-yesp1ENQBiLI+iYHSJdoZKUtRpfTlL1grDIX9NRlAVppljLw/4tTyYupIB7uIYmC3stW/imAv8EqaKaS/ibmeA== + dependencies: + "@babel/template" "^7.14.5" + "@babel/traverse" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/highlight@^7.0.0", "@babel/highlight@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" + integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg== + dependencies: + "@babel/helper-validator-identifier" "^7.14.5" chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.12.10", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.2.2": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79" - integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg== +"@babel/parser@^7.0.0", "@babel/parser@^7.14.5", "@babel/parser@^7.14.6", "@babel/parser@^7.2.2": + version "7.14.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.6.tgz#d85cc68ca3cac84eae384c06f032921f5227f4b2" + integrity sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ== -"@babel/plugin-proposal-async-generator-functions@^7.12.1", "@babel/plugin-proposal-async-generator-functions@^7.2.0": - version "7.12.12" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.12.tgz#04b8f24fd4532008ab4e79f788468fd5a8476566" - integrity sha512-nrz9y0a4xmUrRq51bYkWJIO5SBZyG2ys2qinHsN0zHDHVsUaModrkpyWWWXfGqYQmOL3x9sQIcTNN/pBGpo09A== +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.14.5.tgz#4b467302e1548ed3b1be43beae2cc9cf45e0bb7e" + integrity sha512-ZoJS2XCKPBfTmL122iP6NM9dOg+d4lc9fFk3zxc8iDjvt8Pk4+TlsHSKhIPf6X+L5ORCdBzqMZDjL/WHj7WknQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-remap-async-to-generator" "^7.12.1" - "@babel/plugin-syntax-async-generators" "^7.8.0" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5" + "@babel/plugin-proposal-optional-chaining" "^7.14.5" + +"@babel/plugin-proposal-async-generator-functions@^7.14.5", "@babel/plugin-proposal-async-generator-functions@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.5.tgz#4024990e3dd74181f4f426ea657769ff49a2df39" + integrity sha512-tbD/CG3l43FIXxmu4a7RBe4zH7MLJ+S/lFowPFO7HetS2hyOZ/0nnnznegDuzFzfkyQYTxqdTH/hKmuBngaDAA== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-remap-async-to-generator" "^7.14.5" + "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-proposal-class-properties@7.3.0": version "7.3.0" @@ -307,13 +336,22 @@ "@babel/helper-create-class-features-plugin" "^7.3.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-proposal-class-properties@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz#a082ff541f2a29a4821065b8add9346c0c16e5de" - integrity sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w== +"@babel/plugin-proposal-class-properties@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.14.5.tgz#40d1ee140c5b1e31a350f4f5eed945096559b42e" + integrity sha512-q/PLpv5Ko4dVc1LYMpCY7RVAAO4uk55qPwrIuJ5QJ8c6cVuAmhu7I/49JOppXL6gXf7ZHzpRVEUZdYoPLM04Gg== dependencies: - "@babel/helper-create-class-features-plugin" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-create-class-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-proposal-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.14.5.tgz#158e9e10d449c3849ef3ecde94a03d9f1841b681" + integrity sha512-KBAH5ksEnYHCegqseI5N9skTdxgJdmDoAOc0uXa+4QMYKeZD0w5IARh4FMlTNtaHhbB8v+KzMdTgxMMzsIy6Yg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-class-static-block" "^7.14.5" "@babel/plugin-proposal-decorators@7.3.0": version "7.3.0" @@ -324,52 +362,52 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-decorators" "^7.2.0" -"@babel/plugin-proposal-dynamic-import@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz#43eb5c2a3487ecd98c5c8ea8b5fdb69a2749b2dc" - integrity sha512-a4rhUSZFuq5W8/OO8H7BL5zspjnc1FLd9hlOxIK/f7qG4a0qsqk8uvF/ywgBA8/OmjsapjpvaEOYItfGG1qIvQ== +"@babel/plugin-proposal-dynamic-import@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.14.5.tgz#0c6617df461c0c1f8fff3b47cd59772360101d2c" + integrity sha512-ExjiNYc3HDN5PXJx+bwC50GIx/KKanX2HiggnIUAYedbARdImiCU4RhhHfdf0Kd7JNXGpsBBBCOm+bBVy3Gb0g== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-dynamic-import" "^7.8.0" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" -"@babel/plugin-proposal-export-namespace-from@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.1.tgz#8b9b8f376b2d88f5dd774e4d24a5cc2e3679b6d4" - integrity sha512-6CThGf0irEkzujYS5LQcjBx8j/4aQGiVv7J9+2f7pGfxqyKh3WnmVJYW3hdrQjyksErMGBPQrCnHfOtna+WLbw== +"@babel/plugin-proposal-export-namespace-from@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.14.5.tgz#dbad244310ce6ccd083072167d8cea83a52faf76" + integrity sha512-g5POA32bXPMmSBu5Dx/iZGLGnKmKPc5AiY7qfZgurzrCYgIztDlHFbznSNCoQuv57YQLnQfaDi7dxCtLDIdXdA== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" -"@babel/plugin-proposal-json-strings@^7.12.1", "@babel/plugin-proposal-json-strings@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.1.tgz#d45423b517714eedd5621a9dfdc03fa9f4eb241c" - integrity sha512-GoLDUi6U9ZLzlSda2Df++VSqDJg3CG+dR0+iWsv6XRw1rEq+zwt4DirM9yrxW6XWaTpmai1cWJLMfM8qQJf+yw== +"@babel/plugin-proposal-json-strings@^7.14.5", "@babel/plugin-proposal-json-strings@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.14.5.tgz#38de60db362e83a3d8c944ac858ddf9f0c2239eb" + integrity sha512-NSq2fczJYKVRIsUJyNxrVUMhB27zb7N7pOFGQOhBKJrChbGcgEAqyZrmZswkPk18VMurEeJAaICbfm57vUeTbQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-json-strings" "^7.8.0" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-json-strings" "^7.8.3" -"@babel/plugin-proposal-logical-assignment-operators@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.1.tgz#f2c490d36e1b3c9659241034a5d2cd50263a2751" - integrity sha512-k8ZmVv0JU+4gcUGeCDZOGd0lCIamU/sMtIiX3UWnUc5yzgq6YUGyEolNYD+MLYKfSzgECPcqetVcJP9Afe/aCA== +"@babel/plugin-proposal-logical-assignment-operators@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.14.5.tgz#6e6229c2a99b02ab2915f82571e0cc646a40c738" + integrity sha512-YGn2AvZAo9TwyhlLvCCWxD90Xq8xJ4aSgaX3G5D/8DW94L8aaT+dS5cSP+Z06+rCJERGSr9GxMBZ601xoc2taw== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.1.tgz#3ed4fff31c015e7f3f1467f190dbe545cd7b046c" - integrity sha512-nZY0ESiaQDI1y96+jk6VxMOaL4LPo/QDHBqL+SF3/vl6dHkTwHlOI8L4ZwuRBHgakRBw5zsVylel7QPbbGuYgg== +"@babel/plugin-proposal-nullish-coalescing-operator@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.14.5.tgz#ee38589ce00e2cc59b299ec3ea406fcd3a0fdaf6" + integrity sha512-gun/SOnMqjSb98Nkaq2rTKMwervfdAoz6NphdY0vTfuzMfryj+tDGb2n6UkDKwez+Y8PZDhE3D143v6Gepp4Hg== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" -"@babel/plugin-proposal-numeric-separator@^7.12.7": - version "7.12.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.7.tgz#8bf253de8139099fea193b297d23a9d406ef056b" - integrity sha512-8c+uy0qmnRTeukiGsjLGy6uVs/TFjJchGXUeBqlG4VWYOdJWkhhVPdQ3uHwbmalfJwv2JsV0qffXP4asRfL2SQ== +"@babel/plugin-proposal-numeric-separator@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.14.5.tgz#83631bf33d9a51df184c2102a069ac0c58c05f18" + integrity sha512-yiclALKe0vyZRZE0pS6RXgjUOt87GWv6FYa5zqj15PvhOGFO69R5DusPlgK/1K5dVnCtegTiWu9UaBSrLLJJBg== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-numeric-separator" "^7.10.4" "@babel/plugin-proposal-object-rest-spread@7.3.2": @@ -380,68 +418,87 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" -"@babel/plugin-proposal-object-rest-spread@^7.12.1", "@babel/plugin-proposal-object-rest-spread@^7.3.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz#def9bd03cea0f9b72283dac0ec22d289c7691069" - integrity sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-transform-parameters" "^7.12.1" - -"@babel/plugin-proposal-optional-catch-binding@^7.12.1", "@babel/plugin-proposal-optional-catch-binding@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.1.tgz#ccc2421af64d3aae50b558a71cede929a5ab2942" - integrity sha512-hFvIjgprh9mMw5v42sJWLI1lzU5L2sznP805zeT6rySVRA0Y18StRhDqhSxlap0oVgItRsB6WSROp4YnJTJz0g== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" - -"@babel/plugin-proposal-optional-chaining@^7.12.7": - version "7.12.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.7.tgz#e02f0ea1b5dc59d401ec16fb824679f683d3303c" - integrity sha512-4ovylXZ0PWmwoOvhU2vhnzVNnm88/Sm9nx7V8BPgMvAzn5zDou3/Awy0EjglyubVHasJj+XCEkr/r1X3P5elCA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" - "@babel/plugin-syntax-optional-chaining" "^7.8.0" - -"@babel/plugin-proposal-private-methods@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz#86814f6e7a21374c980c10d38b4493e703f4a389" - integrity sha512-mwZ1phvH7/NHK6Kf8LP7MYDogGV+DKB1mryFOEwx5EBNQrosvIczzZFTUmWaeujd5xT6G1ELYWUz3CutMhjE1w== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-proposal-unicode-property-regex@^7.12.1", "@babel/plugin-proposal-unicode-property-regex@^7.2.0", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.1.tgz#2a183958d417765b9eae334f47758e5d6a82e072" - integrity sha512-MYq+l+PvHuw/rKUz1at/vb6nCnQ2gmJBNaM62z0OgH7B2W1D9pvkpYtlti9bGtizNIU1K3zm4bZF9F91efVY0w== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-async-generators@^7.2.0", "@babel/plugin-syntax-async-generators@^7.8.0": +"@babel/plugin-proposal-object-rest-spread@^7.14.5", "@babel/plugin-proposal-object-rest-spread@^7.3.1": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.14.5.tgz#e581d5ccdfa187ea6ed73f56c6a21c1580b90fbf" + integrity sha512-VzMyY6PWNPPT3pxc5hi9LloKNr4SSrVCg7Yr6aZpW4Ym07r7KqSU/QXYwjXLVxqwSv0t/XSXkFoKBPUkZ8vb2A== + dependencies: + "@babel/compat-data" "^7.14.5" + "@babel/helper-compilation-targets" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.14.5" + +"@babel/plugin-proposal-optional-catch-binding@^7.14.5", "@babel/plugin-proposal-optional-catch-binding@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.14.5.tgz#939dd6eddeff3a67fdf7b3f044b5347262598c3c" + integrity sha512-3Oyiixm0ur7bzO5ybNcZFlmVsygSIQgdOa7cTfOYCMY+wEPAYhZAJxi3mixKFCTCKUhQXuCTtQ1MzrpL3WT8ZQ== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-proposal-optional-chaining@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.14.5.tgz#fa83651e60a360e3f13797eef00b8d519695b603" + integrity sha512-ycz+VOzo2UbWNI1rQXxIuMOzrDdHGrI23fRiz/Si2R4kv2XZQ1BK8ccdHwehMKBlcH/joGW/tzrUmo67gbJHlQ== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-proposal-private-methods@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.14.5.tgz#37446495996b2945f30f5be5b60d5e2aa4f5792d" + integrity sha512-838DkdUA1u+QTCplatfq4B7+1lnDa/+QMI89x5WZHBcnNv+47N8QEj2k9I2MUU9xIv8XJ4XvPCviM/Dj7Uwt9g== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-proposal-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.14.5.tgz#9f65a4d0493a940b4c01f8aa9d3f1894a587f636" + integrity sha512-62EyfyA3WA0mZiF2e2IV9mc9Ghwxcg8YTu8BS4Wss4Y3PY725OmS9M0qLORbJwLqFtGh+jiE4wAmocK2CTUK2Q== + dependencies: + "@babel/helper-annotate-as-pure" "^7.14.5" + "@babel/helper-create-class-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-proposal-unicode-property-regex@^7.14.5", "@babel/plugin-proposal-unicode-property-regex@^7.2.0", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.14.5.tgz#0f95ee0e757a5d647f378daa0eca7e93faa8bbe8" + integrity sha512-6axIeOU5LnY471KenAB9vI8I5j7NQ2d652hIYwVyRfgaZT5UpiqFKCuVXCDMSrU+3VFafnu2c5m3lrWIlr6A5Q== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-async-generators@^7.2.0", "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-class-properties@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz#bcb297c5366e79bebadef509549cd93b04f19978" - integrity sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA== +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-decorators@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.12.1.tgz#81a8b535b284476c41be6de06853a8802b98c5dd" - integrity sha512-ir9YW5daRrTYiy9UJ2TzdNIJEZu8KclVzDcfSt4iEmOtwQ4llPtWInNKJyKnVXp1vE4bbVd5S31M/im3mYMO1w== + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.14.5.tgz#eafb9c0cbe09c8afeb964ba3a7bbd63945a72f20" + integrity sha512-c4sZMRWL4GSvP1EXy0woIP7m4jkVcEuG8R1TOZxPBPtp4FSM/kiPZub9UIs/Jrb5ZAOzvTUSGYrWsrSu1JvoPw== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-dynamic-import@7.2.0": version "7.2.0" @@ -450,7 +507,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-syntax-dynamic-import@^7.8.0": +"@babel/plugin-syntax-dynamic-import@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== @@ -465,25 +522,25 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-flow@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.12.1.tgz#a77670d9abe6d63e8acadf4c31bb1eb5a506bbdd" - integrity sha512-1lBLLmtxrwpm4VKmtVFselI/P3pX+G63fAtUUt6b2Nzgao77KNDwyuRt90Mj2/9pKobtt68FdvjfqohZjg/FCA== + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.14.5.tgz#2ff654999497d7d7d142493260005263731da180" + integrity sha512-9WK5ZwKCdWHxVuU13XNT6X73FGmutAXeor5lGFq6qhOFtMFUF4jkbijuyUdZZlpYq6E2hZeZf/u3959X9wsv0Q== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-json-strings@^7.2.0", "@babel/plugin-syntax-json-strings@^7.8.0": +"@babel/plugin-syntax-json-strings@^7.2.0", "@babel/plugin-syntax-json-strings@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz#9d9d357cc818aa7ae7935917c1257f67677a0926" - integrity sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg== +"@babel/plugin-syntax-jsx@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.14.5.tgz#000e2e25d8673cce49300517a3eda44c263e4201" + integrity sha512-ohuFIsOMXJnbOMRfX7/w7LocdR6R7whhuRD4ax8IipLcLPlZGJKkBxgHp++U4N/vKyU16/YDQr2f5seajD3jIw== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" @@ -492,7 +549,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0": +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== @@ -506,70 +563,77 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-object-rest-spread@^7.2.0", "@babel/plugin-syntax-object-rest-spread@^7.8.0": +"@babel/plugin-syntax-object-rest-spread@^7.2.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-optional-catch-binding@^7.2.0", "@babel/plugin-syntax-optional-catch-binding@^7.8.0": +"@babel/plugin-syntax-optional-catch-binding@^7.2.0", "@babel/plugin-syntax-optional-catch-binding@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-optional-chaining@^7.8.0": +"@babel/plugin-syntax-optional-chaining@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-top-level-await@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz#dd6c0b357ac1bb142d98537450a319625d13d2a0" - integrity sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A== +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-typescript@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.1.tgz#460ba9d77077653803c3dd2e673f76d66b4029e5" - integrity sha512-UZNEcCY+4Dp9yYRCAHrHDU+9ZXLYaY9MgBXSRLkB9WjYFRR6quJBumfVrEkUxrePPBwFcpWfNKXqVRQQtm7mMA== +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-arrow-functions@^7.12.1", "@babel/plugin-transform-arrow-functions@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz#8083ffc86ac8e777fbe24b5967c4b2521f3cb2b3" - integrity sha512-5QB50qyN44fzzz4/qxDPQMBCTHgxg3n0xRBLJUmBlLoU/sFvxVWGZF/ZUfMVDQuJUKXaBhbupxIzIfZ6Fwk/0A== +"@babel/plugin-syntax-typescript@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.14.5.tgz#b82c6ce471b165b5ce420cf92914d6fb46225716" + integrity sha512-u6OXzDaIXjEstBRRoBCQ/uKQKlbuaeE5in0RvWdA4pN6AhqxTIwUsnHPU1CFZA/amYObMsuWhYfRl3Ch90HD0Q== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-async-to-generator@^7.12.1", "@babel/plugin-transform-async-to-generator@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz#3849a49cc2a22e9743cbd6b52926d30337229af1" - integrity sha512-SDtqoEcarK1DFlRJ1hHRY5HvJUj5kX4qmtpMAm2QnhOlyuMC4TMdCRgW6WXpv93rZeYNeLP22y8Aq2dbcDRM1A== +"@babel/plugin-transform-arrow-functions@^7.14.5", "@babel/plugin-transform-arrow-functions@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.14.5.tgz#f7187d9588a768dd080bf4c9ffe117ea62f7862a" + integrity sha512-KOnO0l4+tD5IfOdi4x8C1XmEIRWUjNRV8wc6K2vz/3e8yAOoZZvsRXRRIF/yo/MAOFb4QjtAw9xSxMXbSMRy8A== dependencies: - "@babel/helper-module-imports" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-remap-async-to-generator" "^7.12.1" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-block-scoped-functions@^7.12.1", "@babel/plugin-transform-block-scoped-functions@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.1.tgz#f2a1a365bde2b7112e0a6ded9067fdd7c07905d9" - integrity sha512-5OpxfuYnSgPalRpo8EWGPzIYf0lHBWORCkj5M0oLBwHdlux9Ri36QqGW3/LR13RSVOAoUUMzoPI/jpE4ABcHoA== +"@babel/plugin-transform-async-to-generator@^7.14.5", "@babel/plugin-transform-async-to-generator@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.14.5.tgz#72c789084d8f2094acb945633943ef8443d39e67" + integrity sha512-szkbzQ0mNk0rpu76fzDdqSyPu0MuvpXgC+6rz5rpMb5OIRxdmHfQxrktL8CYolL2d8luMCZTR0DpIMIdL27IjA== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-module-imports" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-remap-async-to-generator" "^7.14.5" -"@babel/plugin-transform-block-scoping@^7.12.11", "@babel/plugin-transform-block-scoping@^7.2.0": - version "7.12.12" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.12.tgz#d93a567a152c22aea3b1929bb118d1d0a175cdca" - integrity sha512-VOEPQ/ExOVqbukuP7BYJtI5ZxxsmegTwzZ04j1aF0dkSypGo9XpDHuOrABsJu+ie+penpSJheDJ11x1BEZNiyQ== +"@babel/plugin-transform-block-scoped-functions@^7.14.5", "@babel/plugin-transform-block-scoped-functions@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.14.5.tgz#e48641d999d4bc157a67ef336aeb54bc44fd3ad4" + integrity sha512-dtqWqdWZ5NqBX3KzsVCWfQI3A53Ft5pWFCT2eCVUftWZgjc5DpDponbIF1+c+7cSGk2wN0YK7HGL/ezfRbpKBQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-block-scoping@^7.14.5", "@babel/plugin-transform-block-scoping@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.14.5.tgz#8cc63e61e50f42e078e6f09be775a75f23ef9939" + integrity sha512-LBYm4ZocNgoCqyxMLoOnwpsmQ18HWTQvql64t3GvMUzLQrNoV1BDG0lNftC8QKYERkZgCCT/7J5xWGObGAyHDw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-transform-classes@7.2.2": version "7.2.2" @@ -585,26 +649,25 @@ "@babel/helper-split-export-declaration" "^7.0.0" globals "^11.1.0" -"@babel/plugin-transform-classes@^7.12.1", "@babel/plugin-transform-classes@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz#65e650fcaddd3d88ddce67c0f834a3d436a32db6" - integrity sha512-/74xkA7bVdzQTBeSUhLLJgYIcxw/dpEpCdRDiHgPJ3Mv6uC11UhjpOhl72CgqbBCmt1qtssCyB2xnJm1+PFjog== - dependencies: - "@babel/helper-annotate-as-pure" "^7.10.4" - "@babel/helper-define-map" "^7.10.4" - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-optimise-call-expression" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-replace-supers" "^7.12.1" - "@babel/helper-split-export-declaration" "^7.10.4" +"@babel/plugin-transform-classes@^7.14.5", "@babel/plugin-transform-classes@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.14.5.tgz#0e98e82097b38550b03b483f9b51a78de0acb2cf" + integrity sha512-J4VxKAMykM06K/64z9rwiL6xnBHgB1+FVspqvlgCdwD1KUbQNfszeKVVOMh59w3sztHYIZDgnhOC4WbdEfHFDA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.14.5" + "@babel/helper-function-name" "^7.14.5" + "@babel/helper-optimise-call-expression" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-replace-supers" "^7.14.5" + "@babel/helper-split-export-declaration" "^7.14.5" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.12.1", "@babel/plugin-transform-computed-properties@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.12.1.tgz#d68cf6c9b7f838a8a4144badbe97541ea0904852" - integrity sha512-vVUOYpPWB7BkgUWPo4C44mUQHpTZXakEqFjbv8rQMg7TC6S6ZhGZ3otQcRH6u7+adSlE5i0sp63eMC/XGffrzg== +"@babel/plugin-transform-computed-properties@^7.14.5", "@babel/plugin-transform-computed-properties@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.14.5.tgz#1b9d78987420d11223d41195461cc43b974b204f" + integrity sha512-pWM+E4283UxaVzLb8UBXv4EIxMovU4zxT1OPnpHJcmnvyY9QbPPTKZfEj31EUvG3/EQRbYAGaYEUZ4yWOBC2xg== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-transform-destructuring@7.3.2": version "7.3.2" @@ -613,35 +676,35 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-destructuring@^7.12.1", "@babel/plugin-transform-destructuring@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.1.tgz#b9a570fe0d0a8d460116413cb4f97e8e08b2f847" - integrity sha512-fRMYFKuzi/rSiYb2uRLiUENJOKq4Gnl+6qOv5f8z0TZXg3llUwUhsNNwrwaT/6dUhJTzNpBr+CUvEWBtfNY1cw== +"@babel/plugin-transform-destructuring@^7.14.5", "@babel/plugin-transform-destructuring@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.14.5.tgz#d32ad19ff1a6da1e861dc62720d80d9776e3bf35" + integrity sha512-wU9tYisEbRMxqDezKUqC9GleLycCRoUsai9ddlsq54r8QRLaeEhc+d+9DqCG+kV9W2GgQjTZESPTpn5bAFMDww== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-dotall-regex@^7.12.1", "@babel/plugin-transform-dotall-regex@^7.2.0", "@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.1.tgz#a1d16c14862817b6409c0a678d6f9373ca9cd975" - integrity sha512-B2pXeRKoLszfEW7J4Hg9LoFaWEbr/kzo3teWHmtFCszjRNa/b40f9mfeqZsIDLLt/FjwQ6pz/Gdlwy85xNckBA== +"@babel/plugin-transform-dotall-regex@^7.14.5", "@babel/plugin-transform-dotall-regex@^7.2.0", "@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.14.5.tgz#2f6bf76e46bdf8043b4e7e16cf24532629ba0c7a" + integrity sha512-loGlnBdj02MDsFaHhAIJzh7euK89lBrGIdM9EAtHFo6xKygCUGuuWe07o1oZVk287amtW1n0808sQM99aZt3gw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-create-regexp-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-duplicate-keys@^7.12.1", "@babel/plugin-transform-duplicate-keys@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.1.tgz#745661baba295ac06e686822797a69fbaa2ca228" - integrity sha512-iRght0T0HztAb/CazveUpUQrZY+aGKKaWXMJ4uf9YJtqxSUe09j3wteztCUDRHs+SRAL7yMuFqUsLoAKKzgXjw== +"@babel/plugin-transform-duplicate-keys@^7.14.5", "@babel/plugin-transform-duplicate-keys@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.14.5.tgz#365a4844881bdf1501e3a9f0270e7f0f91177954" + integrity sha512-iJjbI53huKbPDAsJ8EmVmvCKeeq21bAze4fu9GBQtSLqfvzj2oRuHVx4ZkDwEhg1htQ+5OBZh/Ab0XDf5iBZ7A== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-exponentiation-operator@^7.12.1", "@babel/plugin-transform-exponentiation-operator@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.1.tgz#b0f2ed356ba1be1428ecaf128ff8a24f02830ae0" - integrity sha512-7tqwy2bv48q+c1EHbXK0Zx3KXd2RVQp6OC7PbwFNt/dPTAV3Lu5sWtWuAj8owr5wqtWnqHfl2/mJlUmqkChKug== +"@babel/plugin-transform-exponentiation-operator@^7.14.5", "@babel/plugin-transform-exponentiation-operator@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.14.5.tgz#5154b8dd6a3dfe6d90923d61724bd3deeb90b493" + integrity sha512-jFazJhMBc9D27o9jDnIE5ZErI0R0m7PbKXVq77FFvqFbzvTMuv8jaAwLZ5PviOLSFttqKIW0/wxNSDbjLk0tYA== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-transform-flow-strip-types@7.2.3": version "7.2.3" @@ -651,108 +714,108 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-flow" "^7.2.0" -"@babel/plugin-transform-for-of@^7.12.1", "@babel/plugin-transform-for-of@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.12.1.tgz#07640f28867ed16f9511c99c888291f560921cfa" - integrity sha512-Zaeq10naAsuHo7heQvyV0ptj4dlZJwZgNAtBYBnu5nNKJoW62m0zKcIEyVECrUKErkUkg6ajMy4ZfnVZciSBhg== +"@babel/plugin-transform-for-of@^7.14.5", "@babel/plugin-transform-for-of@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.14.5.tgz#dae384613de8f77c196a8869cbf602a44f7fc0eb" + integrity sha512-CfmqxSUZzBl0rSjpoQSFoR9UEj3HzbGuGNL21/iFTmjb5gFggJp3ph0xR1YBhexmLoKRHzgxuFvty2xdSt6gTA== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-function-name@^7.12.1", "@babel/plugin-transform-function-name@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.1.tgz#2ec76258c70fe08c6d7da154003a480620eba667" - integrity sha512-JF3UgJUILoFrFMEnOJLJkRHSk6LUSXLmEFsA23aR2O5CSLUxbeUX1IZ1YQ7Sn0aXb601Ncwjx73a+FVqgcljVw== +"@babel/plugin-transform-function-name@^7.14.5", "@babel/plugin-transform-function-name@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.14.5.tgz#e81c65ecb900746d7f31802f6bed1f52d915d6f2" + integrity sha512-vbO6kv0fIzZ1GpmGQuvbwwm+O4Cbm2NrPzwlup9+/3fdkuzo1YqOZcXw26+YUJB84Ja7j9yURWposEHLYwxUfQ== dependencies: - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-function-name" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-literals@^7.12.1", "@babel/plugin-transform-literals@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.1.tgz#d73b803a26b37017ddf9d3bb8f4dc58bfb806f57" - integrity sha512-+PxVGA+2Ag6uGgL0A5f+9rklOnnMccwEBzwYFL3EUaKuiyVnUipyXncFcfjSkbimLrODoqki1U9XxZzTvfN7IQ== +"@babel/plugin-transform-literals@^7.14.5", "@babel/plugin-transform-literals@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.14.5.tgz#41d06c7ff5d4d09e3cf4587bd3ecf3930c730f78" + integrity sha512-ql33+epql2F49bi8aHXxvLURHkxJbSmMKl9J5yHqg4PLtdE6Uc48CH1GS6TQvZ86eoB/ApZXwm7jlA+B3kra7A== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-member-expression-literals@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.1.tgz#496038602daf1514a64d43d8e17cbb2755e0c3ad" - integrity sha512-1sxePl6z9ad0gFMB9KqmYofk34flq62aqMt9NqliS/7hPEpURUCMbyHXrMPlo282iY7nAvUB1aQd5mg79UD9Jg== +"@babel/plugin-transform-member-expression-literals@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.14.5.tgz#b39cd5212a2bf235a617d320ec2b48bcc091b8a7" + integrity sha512-WkNXxH1VXVTKarWFqmso83xl+2V3Eo28YY5utIkbsmXoItO8Q3aZxN4BTS2k0hz9dGUloHK26mJMyQEYfkn/+Q== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-modules-amd@^7.12.1", "@babel/plugin-transform-modules-amd@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.12.1.tgz#3154300b026185666eebb0c0ed7f8415fefcf6f9" - integrity sha512-tDW8hMkzad5oDtzsB70HIQQRBiTKrhfgwC/KkJeGsaNFTdWhKNt/BiE8c5yj19XiGyrxpbkOfH87qkNg1YGlOQ== +"@babel/plugin-transform-modules-amd@^7.14.5", "@babel/plugin-transform-modules-amd@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.14.5.tgz#4fd9ce7e3411cb8b83848480b7041d83004858f7" + integrity sha512-3lpOU8Vxmp3roC4vzFpSdEpGUWSMsHFreTWOMMLzel2gNGfHE5UWIh/LN6ghHs2xurUp4jRFYMUIZhuFbody1g== dependencies: - "@babel/helper-module-transforms" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-module-transforms" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.12.1", "@babel/plugin-transform-modules-commonjs@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.1.tgz#fa403124542636c786cf9b460a0ffbb48a86e648" - integrity sha512-dY789wq6l0uLY8py9c1B48V8mVL5gZh/+PQ5ZPrylPYsnAvnEMjqsUXkuoDVPeVK+0VyGar+D08107LzDQ6pag== +"@babel/plugin-transform-modules-commonjs@^7.14.5", "@babel/plugin-transform-modules-commonjs@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.14.5.tgz#7aaee0ea98283de94da98b28f8c35701429dad97" + integrity sha512-en8GfBtgnydoao2PS+87mKyw62k02k7kJ9ltbKe0fXTHrQmG6QZZflYuGI1VVG7sVpx4E1n7KBpNlPb8m78J+A== dependencies: - "@babel/helper-module-transforms" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-simple-access" "^7.12.1" + "@babel/helper-module-transforms" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-simple-access" "^7.14.5" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-systemjs@^7.12.1", "@babel/plugin-transform-modules-systemjs@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.1.tgz#663fea620d593c93f214a464cd399bf6dc683086" - integrity sha512-Hn7cVvOavVh8yvW6fLwveFqSnd7rbQN3zJvoPNyNaQSvgfKmDBO9U1YL9+PCXGRlZD9tNdWTy5ACKqMuzyn32Q== +"@babel/plugin-transform-modules-systemjs@^7.14.5", "@babel/plugin-transform-modules-systemjs@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.14.5.tgz#c75342ef8b30dcde4295d3401aae24e65638ed29" + integrity sha512-mNMQdvBEE5DcMQaL5LbzXFMANrQjd2W7FPzg34Y4yEz7dBgdaC+9B84dSO+/1Wba98zoDbInctCDo4JGxz1VYA== dependencies: - "@babel/helper-hoist-variables" "^7.10.4" - "@babel/helper-module-transforms" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-validator-identifier" "^7.10.4" + "@babel/helper-hoist-variables" "^7.14.5" + "@babel/helper-module-transforms" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-validator-identifier" "^7.14.5" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-umd@^7.12.1", "@babel/plugin-transform-modules-umd@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.12.1.tgz#eb5a218d6b1c68f3d6217b8fa2cc82fec6547902" - integrity sha512-aEIubCS0KHKM0zUos5fIoQm+AZUMt1ZvMpqz0/H5qAQ7vWylr9+PLYurT+Ic7ID/bKLd4q8hDovaG3Zch2uz5Q== +"@babel/plugin-transform-modules-umd@^7.14.5", "@babel/plugin-transform-modules-umd@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.14.5.tgz#fb662dfee697cce274a7cda525190a79096aa6e0" + integrity sha512-RfPGoagSngC06LsGUYyM9QWSXZ8MysEjDJTAea1lqRjNECE3y0qIJF/qbvJxc4oA4s99HumIMdXOrd+TdKaAAA== dependencies: - "@babel/helper-module-transforms" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-module-transforms" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-named-capturing-groups-regex@^7.12.1", "@babel/plugin-transform-named-capturing-groups-regex@^7.3.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.1.tgz#b407f5c96be0d9f5f88467497fa82b30ac3e8753" - integrity sha512-tB43uQ62RHcoDp9v2Nsf+dSM8sbNodbEicbQNA53zHz8pWUhsgHSJCGpt7daXxRydjb0KnfmB+ChXOv3oADp1Q== +"@babel/plugin-transform-named-capturing-groups-regex@^7.14.5", "@babel/plugin-transform-named-capturing-groups-regex@^7.3.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.14.5.tgz#d537e8ee083ee6f6aa4f4eef9d2081d555746e4c" + integrity sha512-+Xe5+6MWFo311U8SchgeX5c1+lJM+eZDBZgD+tvXu9VVQPXwwVzeManMMjYX6xw2HczngfOSZjoFYKwdeB/Jvw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.12.1" + "@babel/helper-create-regexp-features-plugin" "^7.14.5" -"@babel/plugin-transform-new-target@^7.0.0", "@babel/plugin-transform-new-target@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.1.tgz#80073f02ee1bb2d365c3416490e085c95759dec0" - integrity sha512-+eW/VLcUL5L9IvJH7rT1sT0CzkdUTvPrXC2PXTn/7z7tXLBuKvezYbGdxD5WMRoyvyaujOq2fWoKl869heKjhw== +"@babel/plugin-transform-new-target@^7.0.0", "@babel/plugin-transform-new-target@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.14.5.tgz#31bdae8b925dc84076ebfcd2a9940143aed7dbf8" + integrity sha512-Nx054zovz6IIRWEB49RDRuXGI4Gy0GMgqG0cII9L3MxqgXz/+rgII+RU58qpo4g7tNEx1jG7rRVH4ihZoP4esQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-object-super@^7.12.1", "@babel/plugin-transform-object-super@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.1.tgz#4ea08696b8d2e65841d0c7706482b048bed1066e" - integrity sha512-AvypiGJH9hsquNUn+RXVcBdeE3KHPZexWRdimhuV59cSoOt5kFBmqlByorAeUlGG2CJWd0U+4ZtNKga/TB0cAw== +"@babel/plugin-transform-object-super@^7.14.5", "@babel/plugin-transform-object-super@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.14.5.tgz#d0b5faeac9e98597a161a9cf78c527ed934cdc45" + integrity sha512-MKfOBWzK0pZIrav9z/hkRqIk/2bTv9qvxHzPQc12RcVkMOzpIKnFCNYJip00ssKWYkd8Sf5g0Wr7pqJ+cmtuFg== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-replace-supers" "^7.12.1" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-replace-supers" "^7.14.5" -"@babel/plugin-transform-parameters@^7.12.1", "@babel/plugin-transform-parameters@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.1.tgz#d2e963b038771650c922eff593799c96d853255d" - integrity sha512-xq9C5EQhdPK23ZeCdMxl8bbRnAgHFrw5EOC3KJUsSylZqdkCaFEXxGSBuTSObOpiiHHNyb82es8M1QYgfQGfNg== +"@babel/plugin-transform-parameters@^7.14.5", "@babel/plugin-transform-parameters@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.14.5.tgz#49662e86a1f3ddccac6363a7dfb1ff0a158afeb3" + integrity sha512-Tl7LWdr6HUxTmzQtzuU14SqbgrSKmaR77M0OKyq4njZLQTPfOvzblNKyNkGwOfEFCEx7KeYHQHDI0P3F02IVkA== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-property-literals@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.1.tgz#41bc81200d730abb4456ab8b3fbd5537b59adecd" - integrity sha512-6MTCR/mZ1MQS+AwZLplX4cEySjCpnIF26ToWo942nqn8hXSm7McaHQNeGx/pt7suI1TWOWMfa/NgBhiqSnX0cQ== +"@babel/plugin-transform-property-literals@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.14.5.tgz#0ddbaa1f83db3606f1cdf4846fa1dfb473458b34" + integrity sha512-r1uilDthkgXW8Z1vJz2dKYLV1tuw2xsbrp3MrZmD99Wh9vsfKoob+JTgri5VUb/JqyKRXotlOtwgu4stIYCmnw== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-transform-react-constant-elements@7.2.0": version "7.2.0" @@ -763,11 +826,11 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-react-constant-elements@^7.0.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.12.1.tgz#4471f0851feec3231cc9aaa0dccde39947c1ac1e" - integrity sha512-KOHd0tIRLoER+J+8f9DblZDa1fLGPwaaN1DI1TVHuQFOpjHV22C3CUB3obeC4fexHY9nx+fH0hQNvLFFfA1mxA== + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.14.5.tgz#41790d856f7c5cec82d2bcf5d0e5064d682522ed" + integrity sha512-NBqLEx1GxllIOXJInJAQbrnwwYJsV3WaMHIcOwD8rhYS0AabTWn7kHdHgPgu5RmHLU0q4DMxhAMu8ue/KampgQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-transform-react-display-name@7.2.0": version "7.2.0" @@ -776,66 +839,66 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-react-display-name@^7.0.0", "@babel/plugin-transform-react-display-name@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.12.1.tgz#1cbcd0c3b1d6648c55374a22fc9b6b7e5341c00d" - integrity sha512-cAzB+UzBIrekfYxyLlFqf/OagTvHLcVBb5vpouzkYkBclRPraiygVnafvAoipErZLI8ANv8Ecn6E/m5qPXD26w== +"@babel/plugin-transform-react-display-name@^7.0.0", "@babel/plugin-transform-react-display-name@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.14.5.tgz#baa92d15c4570411301a85a74c13534873885b65" + integrity sha512-07aqY1ChoPgIxsuDviptRpVkWCSbXWmzQqcgy65C6YSFOfPFvb/DX3bBRHh7pCd/PMEEYHYWUTSVkCbkVainYQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-react-jsx-development@^7.12.7": - version "7.12.12" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.12.tgz#bccca33108fe99d95d7f9e82046bfe762e71f4e7" - integrity sha512-i1AxnKxHeMxUaWVXQOSIco4tvVvvCxMSfeBMnMM06mpaJt3g+MpxYQQrDfojUQldP1xxraPSJYSMEljoWM/dCg== +"@babel/plugin-transform-react-jsx-development@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.14.5.tgz#1a6c73e2f7ed2c42eebc3d2ad60b0c7494fcb9af" + integrity sha512-rdwG/9jC6QybWxVe2UVOa7q6cnTpw8JRRHOxntG/h6g/guAOe6AhtQHJuJh5FwmnXIT1bdm5vC2/5huV8ZOorQ== dependencies: - "@babel/plugin-transform-react-jsx" "^7.12.12" + "@babel/plugin-transform-react-jsx" "^7.14.5" "@babel/plugin-transform-react-jsx-self@^7.0.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.12.1.tgz#ef43cbca2a14f1bd17807dbe4376ff89d714cf28" - integrity sha512-FbpL0ieNWiiBB5tCldX17EtXgmzeEZjFrix72rQYeq9X6nUK38HCaxexzVQrZWXanxKJPKVVIU37gFjEQYkPkA== + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.14.5.tgz#703b5d1edccd342179c2a99ee8c7065c2b4403cc" + integrity sha512-M/fmDX6n0cfHK/NLTcPmrfVAORKDhK8tyjDhyxlUjYyPYYO8FRWwuxBA3WBx8kWN/uBUuwGa3s/0+hQ9JIN3Tg== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-transform-react-jsx-source@^7.0.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.12.1.tgz#d07de6863f468da0809edcf79a1aa8ce2a82a26b" - integrity sha512-keQ5kBfjJNRc6zZN1/nVHCd6LLIHq4aUKcVnvE/2l+ZZROSbqoiGFRtT5t3Is89XJxBQaP7NLZX2jgGHdZvvFQ== + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.14.5.tgz#79f728e60e6dbd31a2b860b0bf6c9765918acf1d" + integrity sha512-1TpSDnD9XR/rQ2tzunBVPThF5poaYT9GqP+of8fAtguYuI/dm2RkrMBDemsxtY0XBzvW7nXjYM0hRyKX9QYj7Q== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-react-jsx@^7.0.0", "@babel/plugin-transform-react-jsx@^7.12.10", "@babel/plugin-transform-react-jsx@^7.12.12": - version "7.12.12" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.12.tgz#b0da51ffe5f34b9a900e9f1f5fb814f9e512d25e" - integrity sha512-JDWGuzGNWscYcq8oJVCtSE61a5+XAOos+V0HrxnDieUus4UMnBEosDnY1VJqU5iZ4pA04QY7l0+JvHL1hZEfsw== +"@babel/plugin-transform-react-jsx@^7.0.0", "@babel/plugin-transform-react-jsx@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.14.5.tgz#39749f0ee1efd8a1bd729152cf5f78f1d247a44a" + integrity sha512-7RylxNeDnxc1OleDm0F5Q/BSL+whYRbOAR+bwgCxIr0L32v7UFh/pz1DLMZideAUxKT6eMoS2zQH6fyODLEi8Q== dependencies: - "@babel/helper-annotate-as-pure" "^7.12.10" - "@babel/helper-module-imports" "^7.12.5" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-jsx" "^7.12.1" - "@babel/types" "^7.12.12" + "@babel/helper-annotate-as-pure" "^7.14.5" + "@babel/helper-module-imports" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-jsx" "^7.14.5" + "@babel/types" "^7.14.5" -"@babel/plugin-transform-react-pure-annotations@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.12.1.tgz#05d46f0ab4d1339ac59adf20a1462c91b37a1a42" - integrity sha512-RqeaHiwZtphSIUZ5I85PEH19LOSzxfuEazoY7/pWASCAIBuATQzpSVD+eT6MebeeZT2F4eSL0u4vw6n4Nm0Mjg== +"@babel/plugin-transform-react-pure-annotations@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.14.5.tgz#18de612b84021e3a9802cbc212c9d9f46d0d11fc" + integrity sha512-3X4HpBJimNxW4rhUy/SONPyNQHp5YRr0HhJdT2OH1BRp0of7u3Dkirc7x9FRJMKMqTBI079VZ1hzv7Ouuz///g== dependencies: - "@babel/helper-annotate-as-pure" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-annotate-as-pure" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-regenerator@^7.0.0", "@babel/plugin-transform-regenerator@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz#5f0a28d842f6462281f06a964e88ba8d7ab49753" - integrity sha512-gYrHqs5itw6i4PflFX3OdBPMQdPbF4bj2REIUxlMRUFk0/ZOAIpDFuViuxPjUL7YC8UPnf+XG7/utJvqXdPKng== +"@babel/plugin-transform-regenerator@^7.0.0", "@babel/plugin-transform-regenerator@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.14.5.tgz#9676fd5707ed28f522727c5b3c0aa8544440b04f" + integrity sha512-NVIY1W3ITDP5xQl50NgTKlZ0GrotKtLna08/uGY6ErQt6VEQZXla86x/CTddm5gZdcr+5GSsvMeTmWA5Ii6pkg== dependencies: regenerator-transform "^0.14.2" -"@babel/plugin-transform-reserved-words@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.1.tgz#6fdfc8cc7edcc42b36a7c12188c6787c873adcd8" - integrity sha512-pOnUfhyPKvZpVyBHhSBoX8vfA09b7r00Pmm1sH+29ae2hMTKVmSp4Ztsr8KBKjLjx17H0eJqaRC3bR2iThM54A== +"@babel/plugin-transform-reserved-words@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.14.5.tgz#c44589b661cfdbef8d4300dcc7469dffa92f8304" + integrity sha512-cv4F2rv1nD4qdexOGsRQXJrOcyb5CrgjUH9PKrrtyhSDBNWGxd0UIitjyJiWagS+EbUGjG++22mGH1Pub8D6Vg== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-transform-runtime@7.2.0": version "7.2.0" @@ -847,65 +910,65 @@ resolve "^1.8.1" semver "^5.5.1" -"@babel/plugin-transform-shorthand-properties@^7.12.1", "@babel/plugin-transform-shorthand-properties@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz#0bf9cac5550fce0cfdf043420f661d645fdc75e3" - integrity sha512-GFZS3c/MhX1OusqB1MZ1ct2xRzX5ppQh2JU1h2Pnfk88HtFTM+TWQqJNfwkmxtPQtb/s1tk87oENfXJlx7rSDw== +"@babel/plugin-transform-shorthand-properties@^7.14.5", "@babel/plugin-transform-shorthand-properties@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.14.5.tgz#97f13855f1409338d8cadcbaca670ad79e091a58" + integrity sha512-xLucks6T1VmGsTB+GWK5Pl9Jl5+nRXD1uoFdA5TSO6xtiNjtXTjKkmPdFXVLGlK5A2/or/wQMKfmQ2Y0XJfn5g== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-spread@^7.12.1", "@babel/plugin-transform-spread@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.12.1.tgz#527f9f311be4ec7fdc2b79bb89f7bf884b3e1e1e" - integrity sha512-vuLp8CP0BE18zVYjsEBZ5xoCecMK6LBMMxYzJnh01rxQRvhNhH1csMMmBfNo5tGpGO+NhdSNW2mzIvBu3K1fng== +"@babel/plugin-transform-spread@^7.14.5", "@babel/plugin-transform-spread@^7.2.0": + version "7.14.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.14.6.tgz#6bd40e57fe7de94aa904851963b5616652f73144" + integrity sha512-Zr0x0YroFJku7n7+/HH3A2eIrGMjbmAIbJSVv0IZ+t3U2WUQUA64S/oeied2e+MaGSjmt4alzBCsK9E8gh+fag== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5" -"@babel/plugin-transform-sticky-regex@^7.12.7", "@babel/plugin-transform-sticky-regex@^7.2.0": - version "7.12.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.7.tgz#560224613ab23987453948ed21d0b0b193fa7fad" - integrity sha512-VEiqZL5N/QvDbdjfYQBhruN0HYjSPjC4XkeqW4ny/jNtH9gcbgaqBIXYEZCNnESMAGs0/K/R7oFGMhOyu/eIxg== +"@babel/plugin-transform-sticky-regex@^7.14.5", "@babel/plugin-transform-sticky-regex@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.14.5.tgz#5b617542675e8b7761294381f3c28c633f40aeb9" + integrity sha512-Z7F7GyvEMzIIbwnziAZmnSNpdijdr4dWt+FJNBnBLz5mwDFkqIXU9wmBcWWad3QeJF5hMTkRe4dAq2sUZiG+8A== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-template-literals@^7.12.1", "@babel/plugin-transform-template-literals@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.12.1.tgz#b43ece6ed9a79c0c71119f576d299ef09d942843" - integrity sha512-b4Zx3KHi+taXB1dVRBhVJtEPi9h1THCeKmae2qP0YdUHIFhVjtpqqNfxeVAa1xeHVhAy4SbHxEwx5cltAu5apw== +"@babel/plugin-transform-template-literals@^7.14.5", "@babel/plugin-transform-template-literals@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.14.5.tgz#a5f2bc233937d8453885dc736bdd8d9ffabf3d93" + integrity sha512-22btZeURqiepOfuy/VkFr+zStqlujWaarpMErvay7goJS6BWwdd6BY9zQyDLDa4x2S3VugxFb162IZ4m/S/+Gg== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-typeof-symbol@^7.12.10", "@babel/plugin-transform-typeof-symbol@^7.2.0": - version "7.12.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.10.tgz#de01c4c8f96580bd00f183072b0d0ecdcf0dec4b" - integrity sha512-JQ6H8Rnsogh//ijxspCjc21YPd3VLVoYtAwv3zQmqAt8YGYUtdo5usNhdl4b9/Vir2kPFZl6n1h0PfUz4hJhaA== +"@babel/plugin-transform-typeof-symbol@^7.14.5", "@babel/plugin-transform-typeof-symbol@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.14.5.tgz#39af2739e989a2bd291bf6b53f16981423d457d4" + integrity sha512-lXzLD30ffCWseTbMQzrvDWqljvZlHkXU+CnseMhkMNqU1sASnCsz3tSzAaH3vCUXb9PHeUb90ZT1BdFTm1xxJw== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-transform-typescript@^7.1.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.12.1.tgz#d92cc0af504d510e26a754a7dbc2e5c8cd9c7ab4" - integrity sha512-VrsBByqAIntM+EYMqSm59SiMEf7qkmI9dqMt6RbD/wlwueWmYcI0FFK5Fj47pP6DRZm+3teXjosKlwcZJ5lIMw== + version "7.14.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.14.6.tgz#6e9c2d98da2507ebe0a883b100cde3c7279df36c" + integrity sha512-XlTdBq7Awr4FYIzqhmYY80WN0V0azF74DMPyFqVHBvf81ZUgc4X7ZOpx6O8eLDK6iM5cCQzeyJw0ynTaefixRA== dependencies: - "@babel/helper-create-class-features-plugin" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-typescript" "^7.12.1" + "@babel/helper-create-class-features-plugin" "^7.14.6" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-typescript" "^7.14.5" -"@babel/plugin-transform-unicode-escapes@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz#5232b9f81ccb07070b7c3c36c67a1b78f1845709" - integrity sha512-I8gNHJLIc7GdApm7wkVnStWssPNbSRMPtgHdmH3sRM1zopz09UWPS4x5V4n1yz/MIWTVnJ9sp6IkuXdWM4w+2Q== +"@babel/plugin-transform-unicode-escapes@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.14.5.tgz#9d4bd2a681e3c5d7acf4f57fa9e51175d91d0c6b" + integrity sha512-crTo4jATEOjxj7bt9lbYXcBAM3LZaUrbP2uUdxb6WIorLmjNKSpHfIybgY4B8SRpbf8tEVIWH3Vtm7ayCrKocA== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-unicode-regex@^7.12.1", "@babel/plugin-transform-unicode-regex@^7.2.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.1.tgz#cc9661f61390db5c65e3febaccefd5c6ac3faecb" - integrity sha512-SqH4ClNngh/zGwHZOOQMTD+e8FGWexILV+ePMyiDJttAWRh5dhDL8rcl5lSgU3Huiq6Zn6pWTMvdPAb21Dwdyg== +"@babel/plugin-transform-unicode-regex@^7.14.5", "@babel/plugin-transform-unicode-regex@^7.2.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.14.5.tgz#4cd09b6c8425dd81255c7ceb3fb1836e7414382e" + integrity sha512-UygduJpC5kHeCiRw/xDVzC+wj8VaYSoKl5JNVmbP7MadpNinAm3SvZCxZ42H37KZBKztz46YC73i9yV34d0Tzw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-create-regexp-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/polyfill@^7.4.4": version "7.12.1" @@ -965,78 +1028,85 @@ semver "^5.3.0" "@babel/preset-env@^7.1.6": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.12.11.tgz#55d5f7981487365c93dbbc84507b1c7215e857f9" - integrity sha512-j8Tb+KKIXKYlDBQyIOy4BLxzv1NUOwlHfZ74rvW+Z0Gp4/cI2IMDPBWAgWceGcE7aep9oL/0K9mlzlMGxA8yNw== - dependencies: - "@babel/compat-data" "^7.12.7" - "@babel/helper-compilation-targets" "^7.12.5" - "@babel/helper-module-imports" "^7.12.5" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-validator-option" "^7.12.11" - "@babel/plugin-proposal-async-generator-functions" "^7.12.1" - "@babel/plugin-proposal-class-properties" "^7.12.1" - "@babel/plugin-proposal-dynamic-import" "^7.12.1" - "@babel/plugin-proposal-export-namespace-from" "^7.12.1" - "@babel/plugin-proposal-json-strings" "^7.12.1" - "@babel/plugin-proposal-logical-assignment-operators" "^7.12.1" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.12.1" - "@babel/plugin-proposal-numeric-separator" "^7.12.7" - "@babel/plugin-proposal-object-rest-spread" "^7.12.1" - "@babel/plugin-proposal-optional-catch-binding" "^7.12.1" - "@babel/plugin-proposal-optional-chaining" "^7.12.7" - "@babel/plugin-proposal-private-methods" "^7.12.1" - "@babel/plugin-proposal-unicode-property-regex" "^7.12.1" - "@babel/plugin-syntax-async-generators" "^7.8.0" - "@babel/plugin-syntax-class-properties" "^7.12.1" - "@babel/plugin-syntax-dynamic-import" "^7.8.0" + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.14.5.tgz#c0c84e763661fd0e74292c3d511cb33b0c668997" + integrity sha512-ci6TsS0bjrdPpWGnQ+m4f+JSSzDKlckqKIJJt9UZ/+g7Zz9k0N8lYU8IeLg/01o2h8LyNZDMLGgRLDTxpudLsA== + dependencies: + "@babel/compat-data" "^7.14.5" + "@babel/helper-compilation-targets" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-validator-option" "^7.14.5" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.14.5" + "@babel/plugin-proposal-async-generator-functions" "^7.14.5" + "@babel/plugin-proposal-class-properties" "^7.14.5" + "@babel/plugin-proposal-class-static-block" "^7.14.5" + "@babel/plugin-proposal-dynamic-import" "^7.14.5" + "@babel/plugin-proposal-export-namespace-from" "^7.14.5" + "@babel/plugin-proposal-json-strings" "^7.14.5" + "@babel/plugin-proposal-logical-assignment-operators" "^7.14.5" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.14.5" + "@babel/plugin-proposal-numeric-separator" "^7.14.5" + "@babel/plugin-proposal-object-rest-spread" "^7.14.5" + "@babel/plugin-proposal-optional-catch-binding" "^7.14.5" + "@babel/plugin-proposal-optional-chaining" "^7.14.5" + "@babel/plugin-proposal-private-methods" "^7.14.5" + "@babel/plugin-proposal-private-property-in-object" "^7.14.5" + "@babel/plugin-proposal-unicode-property-regex" "^7.14.5" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-json-strings" "^7.8.0" + "@babel/plugin-syntax-json-strings" "^7.8.3" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" - "@babel/plugin-syntax-optional-chaining" "^7.8.0" - "@babel/plugin-syntax-top-level-await" "^7.12.1" - "@babel/plugin-transform-arrow-functions" "^7.12.1" - "@babel/plugin-transform-async-to-generator" "^7.12.1" - "@babel/plugin-transform-block-scoped-functions" "^7.12.1" - "@babel/plugin-transform-block-scoping" "^7.12.11" - "@babel/plugin-transform-classes" "^7.12.1" - "@babel/plugin-transform-computed-properties" "^7.12.1" - "@babel/plugin-transform-destructuring" "^7.12.1" - "@babel/plugin-transform-dotall-regex" "^7.12.1" - "@babel/plugin-transform-duplicate-keys" "^7.12.1" - "@babel/plugin-transform-exponentiation-operator" "^7.12.1" - "@babel/plugin-transform-for-of" "^7.12.1" - "@babel/plugin-transform-function-name" "^7.12.1" - "@babel/plugin-transform-literals" "^7.12.1" - "@babel/plugin-transform-member-expression-literals" "^7.12.1" - "@babel/plugin-transform-modules-amd" "^7.12.1" - "@babel/plugin-transform-modules-commonjs" "^7.12.1" - "@babel/plugin-transform-modules-systemjs" "^7.12.1" - "@babel/plugin-transform-modules-umd" "^7.12.1" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.12.1" - "@babel/plugin-transform-new-target" "^7.12.1" - "@babel/plugin-transform-object-super" "^7.12.1" - "@babel/plugin-transform-parameters" "^7.12.1" - "@babel/plugin-transform-property-literals" "^7.12.1" - "@babel/plugin-transform-regenerator" "^7.12.1" - "@babel/plugin-transform-reserved-words" "^7.12.1" - "@babel/plugin-transform-shorthand-properties" "^7.12.1" - "@babel/plugin-transform-spread" "^7.12.1" - "@babel/plugin-transform-sticky-regex" "^7.12.7" - "@babel/plugin-transform-template-literals" "^7.12.1" - "@babel/plugin-transform-typeof-symbol" "^7.12.10" - "@babel/plugin-transform-unicode-escapes" "^7.12.1" - "@babel/plugin-transform-unicode-regex" "^7.12.1" - "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.12.11" - core-js-compat "^3.8.0" - semver "^5.5.0" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-transform-arrow-functions" "^7.14.5" + "@babel/plugin-transform-async-to-generator" "^7.14.5" + "@babel/plugin-transform-block-scoped-functions" "^7.14.5" + "@babel/plugin-transform-block-scoping" "^7.14.5" + "@babel/plugin-transform-classes" "^7.14.5" + "@babel/plugin-transform-computed-properties" "^7.14.5" + "@babel/plugin-transform-destructuring" "^7.14.5" + "@babel/plugin-transform-dotall-regex" "^7.14.5" + "@babel/plugin-transform-duplicate-keys" "^7.14.5" + "@babel/plugin-transform-exponentiation-operator" "^7.14.5" + "@babel/plugin-transform-for-of" "^7.14.5" + "@babel/plugin-transform-function-name" "^7.14.5" + "@babel/plugin-transform-literals" "^7.14.5" + "@babel/plugin-transform-member-expression-literals" "^7.14.5" + "@babel/plugin-transform-modules-amd" "^7.14.5" + "@babel/plugin-transform-modules-commonjs" "^7.14.5" + "@babel/plugin-transform-modules-systemjs" "^7.14.5" + "@babel/plugin-transform-modules-umd" "^7.14.5" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.14.5" + "@babel/plugin-transform-new-target" "^7.14.5" + "@babel/plugin-transform-object-super" "^7.14.5" + "@babel/plugin-transform-parameters" "^7.14.5" + "@babel/plugin-transform-property-literals" "^7.14.5" + "@babel/plugin-transform-regenerator" "^7.14.5" + "@babel/plugin-transform-reserved-words" "^7.14.5" + "@babel/plugin-transform-shorthand-properties" "^7.14.5" + "@babel/plugin-transform-spread" "^7.14.5" + "@babel/plugin-transform-sticky-regex" "^7.14.5" + "@babel/plugin-transform-template-literals" "^7.14.5" + "@babel/plugin-transform-typeof-symbol" "^7.14.5" + "@babel/plugin-transform-unicode-escapes" "^7.14.5" + "@babel/plugin-transform-unicode-regex" "^7.14.5" + "@babel/preset-modules" "^0.1.4" + "@babel/types" "^7.14.5" + babel-plugin-polyfill-corejs2 "^0.2.2" + babel-plugin-polyfill-corejs3 "^0.2.2" + babel-plugin-polyfill-regenerator "^0.2.2" + core-js-compat "^3.14.0" + semver "^6.3.0" -"@babel/preset-modules@^0.1.3": +"@babel/preset-modules@^0.1.4": version "0.1.4" resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.4.tgz#362f2b68c662842970fdb5e254ffc8fc1c2e415e" integrity sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg== @@ -1059,15 +1129,16 @@ "@babel/plugin-transform-react-jsx-source" "^7.0.0" "@babel/preset-react@^7.0.0": - version "7.12.10" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.12.10.tgz#4fed65f296cbb0f5fb09de6be8cddc85cc909be9" - integrity sha512-vtQNjaHRl4DUpp+t+g4wvTHsLQuye+n0H/wsXIZRn69oz/fvNC7gQ4IK73zGJBaxvHoxElDvnYCthMcT7uzFoQ== + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.14.5.tgz#0fbb769513f899c2c56f3a882fa79673c2d4ab3c" + integrity sha512-XFxBkjyObLvBaAvkx1Ie95Iaq4S/GUEIrejyrntQ/VCMKUYvKLoyKxOBzJ2kjA3b6rC9/KL6KXfDC2GqvLiNqQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-transform-react-display-name" "^7.12.1" - "@babel/plugin-transform-react-jsx" "^7.12.10" - "@babel/plugin-transform-react-jsx-development" "^7.12.7" - "@babel/plugin-transform-react-pure-annotations" "^7.12.1" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-validator-option" "^7.14.5" + "@babel/plugin-transform-react-display-name" "^7.14.5" + "@babel/plugin-transform-react-jsx" "^7.14.5" + "@babel/plugin-transform-react-jsx-development" "^7.14.5" + "@babel/plugin-transform-react-pure-annotations" "^7.14.5" "@babel/preset-typescript@7.1.0": version "7.1.0" @@ -1078,11 +1149,11 @@ "@babel/plugin-transform-typescript" "^7.1.0" "@babel/runtime-corejs3@^7.12.1": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.14.0.tgz#6bf5fbc0b961f8e3202888cb2cd0fb7a0a9a3f66" - integrity sha512-0R0HTZWHLk6G8jIk0FtoX+AatCtKnswS98VhXwGImFc759PJRp4Tru0PQYZofyijTFUr+gT8Mu7sgXVJLQ0ceg== + version "7.14.6" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.14.6.tgz#066b966eda40481740180cb3caab861a3f208cd3" + integrity sha512-Xl8SPYtdjcMoCsIM4teyVRg7jIcgl8F2kRtoCcXuHzXswt9UxZCS6BzRo8fcnCuP6u2XtPgvyonmEPF57Kxo9Q== dependencies: - core-js-pure "^3.0.0" + core-js-pure "^3.14.0" regenerator-runtime "^0.13.4" "@babel/runtime@7.3.1": @@ -1092,53 +1163,68 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.13.tgz#0a21452352b02542db0ffb928ac2d3ca7cb6d66d" - integrity sha512-8+3UMPBrjFa/6TtKi/7sehPKqfAm4g6K+YQjyyFOLUTxzOngcRZTlAVY8sc2CORJYqdHQY8gRPHmn+qo15rCBw== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3": - version "7.13.10" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d" - integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.14.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d" + integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg== dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.10.4", "@babel/template@^7.12.7", "@babel/template@^7.2.2": - version "7.12.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc" - integrity sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/parser" "^7.12.7" - "@babel/types" "^7.12.7" - -"@babel/traverse@^7.0.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.10", "@babel/traverse@^7.12.5", "@babel/traverse@^7.2.2": - version "7.12.12" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.12.tgz#d0cd87892704edd8da002d674bc811ce64743376" - integrity sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w== - dependencies: - "@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/template@^7.14.5", "@babel/template@^7.2.2": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4" + integrity sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g== + dependencies: + "@babel/code-frame" "^7.14.5" + "@babel/parser" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/traverse@^7.0.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.14.5", "@babel/traverse@^7.2.2": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.5.tgz#c111b0f58afab4fea3d3385a406f692748c59870" + integrity sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg== + dependencies: + "@babel/code-frame" "^7.14.5" + "@babel/generator" "^7.14.5" + "@babel/helper-function-name" "^7.14.5" + "@babel/helper-hoist-variables" "^7.14.5" + "@babel/helper-split-export-declaration" "^7.14.5" + "@babel/parser" "^7.14.5" + "@babel/types" "^7.14.5" debug "^4.1.0" globals "^11.1.0" - lodash "^4.17.19" -"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.12.5", "@babel/types@^7.12.7", "@babel/types@^7.2.2", "@babel/types@^7.4.4": - version "7.12.12" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299" - integrity sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ== +"@babel/types@^7.0.0", "@babel/types@^7.14.5", "@babel/types@^7.2.2", "@babel/types@^7.4.4": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.5.tgz#3bb997ba829a2104cedb20689c4a5b8121d383ff" + integrity sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg== dependencies: - "@babel/helper-validator-identifier" "^7.12.11" - lodash "^4.17.19" + "@babel/helper-validator-identifier" "^7.14.5" to-fast-properties "^2.0.0" +"@bandwidth/messaging@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@bandwidth/messaging/-/messaging-3.0.0.tgz#7ff19238e2d0d95122ccb29e5adeb375e97c859f" + integrity sha512-DeFOML9leEZOGggb4hSOdJ4mrHmktk+JVYkGCKjNkJPOn0wQsDmse0O4EJLudd71WgmHPWKWXEA4tMFWLPaIjg== + dependencies: + "@apimatic/schema" "^0.4.1" + "@types/node" "^14.14.27" + axios "^0.21.1" + detect-node "^2.0.4" + form-data "^3.0.0" + lodash.flatmap "^4.5.0" + tiny-warning "^1.0.3" + +"@bandwidth/numbers@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@bandwidth/numbers/-/numbers-1.7.0.tgz#0205d4835af11b129ccd615ef57f249b0066f54e" + integrity sha512-Puy2SOlAAyGFnlC4J4Zs/gcBergOqGSOGh4n0q5CvfactEBlCFP999r/rURji9XlPtSOTlZBLLg7lJJJ3Pa7vw== + dependencies: + bluebird "^3.7.2" + streamifier "^0.1.1" + superagent "^3.7.0" + xml2js "^0.4.4" + "@csstools/convert-colors@^1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" @@ -1211,14 +1297,14 @@ integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw== "@material-ui/core@^4.11.3": - version "4.11.3" - resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.11.3.tgz#f22e41775b0bd075e36a7a093d43951bf7f63850" - integrity sha512-Adt40rGW6Uds+cAyk3pVgcErpzU/qxc7KBR94jFHBYretU4AtWZltYcNsbeMn9tXL86jjVL1kuGcIHsgLgFGRw== + version "4.11.4" + resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.11.4.tgz#4fb9fe5dec5dcf780b687e3a40cff78b2b9640a4" + integrity sha512-oqb+lJ2Dl9HXI9orc6/aN8ZIAMkeThufA5iZELf2LQeBn2NtjVilF5D2w7e9RpntAzDb4jK5DsVhkfOvFY/8fg== dependencies: "@babel/runtime" "^7.4.4" - "@material-ui/styles" "^4.11.3" + "@material-ui/styles" "^4.11.4" "@material-ui/system" "^4.11.3" - "@material-ui/types" "^5.1.0" + "@material-ui/types" "5.1.0" "@material-ui/utils" "^4.11.2" "@types/react-transition-group" "^4.2.0" clsx "^1.0.4" @@ -1236,9 +1322,9 @@ "@babel/runtime" "^7.4.4" "@material-ui/lab@^4.0.0-alpha.57": - version "4.0.0-alpha.57" - resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.57.tgz#e8961bcf6449e8a8dabe84f2700daacfcafbf83a" - integrity sha512-qo/IuIQOmEKtzmRD2E4Aa6DB4A87kmY6h0uYhjUmrrgmEAgbbw9etXpWPVXuRK6AGIQCjFzV6WO2i21m1R4FCw== + version "4.0.0-alpha.58" + resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.58.tgz#c7ebb66f49863c5acbb20817163737caa299fafc" + integrity sha512-GKHlJqLxUeHH3L3dGQ48ZavYrqGOTXkFkiEiuYMAnAvXAZP4rhMIqeHOPXSUQan4Bd8QnafDcpovOSLnadDmKw== dependencies: "@babel/runtime" "^7.4.4" "@material-ui/utils" "^4.11.2" @@ -1258,14 +1344,14 @@ react-transition-group "^4.0.0" rifm "^0.7.0" -"@material-ui/styles@^4.11.3": - version "4.11.3" - resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.3.tgz#1b8d97775a4a643b53478c895e3f2a464e8916f2" - integrity sha512-HzVzCG+PpgUGMUYEJ2rTEmQYeonGh41BYfILNFb/1ueqma+p1meSdu4RX6NjxYBMhf7k+jgfHFTTz+L1SXL/Zg== +"@material-ui/styles@^4.11.4": + version "4.11.4" + resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.4.tgz#eb9dfccfcc2d208243d986457dff025497afa00d" + integrity sha512-KNTIZcnj/zprG5LW0Sao7zw+yG3O35pviHzejMdcSGCdWbiO8qzRgOYL8JAxAsWBKOKYwVZxXtHWaB5T2Kvxew== dependencies: "@babel/runtime" "^7.4.4" "@emotion/hash" "^0.8.0" - "@material-ui/types" "^5.1.0" + "@material-ui/types" "5.1.0" "@material-ui/utils" "^4.11.2" clsx "^1.0.4" csstype "^2.5.2" @@ -1290,7 +1376,7 @@ csstype "^2.5.2" prop-types "^15.7.2" -"@material-ui/types@^5.1.0": +"@material-ui/types@5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@material-ui/types/-/types-5.1.0.tgz#efa1c7a0b0eaa4c7c87ac0390445f0f88b0d88f2" integrity sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A== @@ -1312,18 +1398,18 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" -"@nodelib/fs.scandir@2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" - integrity sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA== +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== dependencies: - "@nodelib/fs.stat" "2.0.4" + "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@2.0.4", "@nodelib/fs.stat@^2.0.2": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz#a3f2dd61bab43b8db8fa108a121cfffe4c676655" - integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q== +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== "@nodelib/fs.stat@^1.1.2": version "1.1.3" @@ -1331,11 +1417,11 @@ integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== "@nodelib/fs.walk@^1.2.3": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz#cce9396b30aa5afe9e3756608f5831adcb53d063" - integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== + version "1.2.7" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.7.tgz#94c23db18ee4653e129abd26fb06f870ac9e1ee2" + integrity sha512-BTIhocbPBSrRmHxOAJFtR18oLhxTtAFDAvL8hY1S3iU8k+E60W/YFs4jrixGzQjMpF4qPXxIQHcjVD9dz1C2QA== dependencies: - "@nodelib/fs.scandir" "2.1.4" + "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" "@react-dnd/asap@^4.0.0": @@ -1353,7 +1439,7 @@ resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a" integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg== -"@restart/hooks@^0.3.25": +"@restart/hooks@^0.3.25", "@restart/hooks@^0.3.26": version "0.3.26" resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.3.26.tgz#ade155a7b0b014ef1073391dda46972c3a14a129" integrity sha512-7Hwk2ZMYm+JLWcb7R9qIXk1OoUg1Z+saKWqZXlrvFwT3w6UArVNWgxYOzf+PJoK9zZejp8okPAKTctthhXLt5g== @@ -1489,9 +1575,9 @@ integrity sha512-apaLDiEABRaxYaFehRa8h478tyYiJWAFuCaaW8d0r/IN6ZigQbS6SGhGAfunnrugdjXxLxl6GjQ0X9cVHeqmNQ== "@types/bluebird@^3.5.27": - version "3.5.33" - resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.33.tgz#d79c020f283bd50bd76101d7d300313c107325fc" - integrity sha512-ndEo1xvnYeHxm7I/5sF6tBvnsA4Tdi3zj1keRKRs12SP+2ye2A27NDJ1B6PqkfMbGAcT+mqQVqbZRIrhfOp5PQ== + version "3.5.35" + resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.35.tgz#3964c48372bf62d60616d8673dd77a9719ebac9b" + integrity sha512-2WeeXK7BuQo7yPI4WGOBum90SzF/f8rqlvpaXx4rjeTmNssGRDHWf7fgDUH90xMB3sUOu716fUK5d+OVx0+ncQ== "@types/glob@^7.1.1": version "7.1.3" @@ -1510,19 +1596,24 @@ hoist-non-react-statics "^3.3.0" "@types/lodash@^4.14.165": - version "4.14.168" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008" - integrity sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q== + version "4.14.170" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6" + integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q== "@types/minimatch@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" - integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21" + integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA== "@types/node@*", "@types/node@>=6": - version "14.14.20" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.20.tgz#f7974863edd21d1f8a494a73e8e2b3658615c340" - integrity sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A== + version "15.12.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.2.tgz#1f2b42c4be7156ff4a6f914b2fb03d05fa84e38d" + integrity sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww== + +"@types/node@^14.14.27": + version "14.17.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.3.tgz#6d327abaa4be34a74e421ed6409a0ae2f47f4c3d" + integrity sha512-e6ZowgGJmTuXa3GyaPbTGxX17tnThl2aSSizrFthQ7m9uLGZBXiGhgE55cjRZTF5kjZvYn9EOPOMljdjwbflxw== "@types/parse-json@^4.0.0": version "4.0.0" @@ -1546,23 +1637,15 @@ dependencies: "@types/react" "*" -"@types/react@*": - version "17.0.3" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79" - integrity sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg== +"@types/react@*", "@types/react@>=16.14.0", "@types/react@>=16.9.11": + version "17.0.11" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.11.tgz#67fcd0ddbf5a0b083a0f94e926c7d63f3b836451" + integrity sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@>=16.14.0", "@types/react@>=16.9.11": - version "17.0.2" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.2.tgz#3de24c4efef902dd9795a49c75f760cbe4f7a5a8" - integrity sha512-Xt40xQsrkdvjn1EyWe1Bc0dJLcil/9x2vAuW7ya+PuQip4UYUaXyhzWmAbwRsdMgwOFHpfp7/FFZebDU6Y8VHA== - dependencies: - "@types/prop-types" "*" - csstype "^3.0.2" - "@types/scheduler@*": version "0.16.1" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" @@ -1574,9 +1657,9 @@ integrity sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg== "@types/sizzle@^2.3.2": - version "2.3.2" - resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" - integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== + version "2.3.3" + resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef" + integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ== "@types/styled-jsx@^2.2.8": version "2.2.8" @@ -1596,47 +1679,47 @@ integrity sha512-HrCIVMLjE1MOozVoD86622S7aunluLb2PJdPfb3nYiEtohm8mIB/vyv0Fd37AdeMFrTUQXEunw78YloMA3Qilg== "@typescript-eslint/parser@^4.9.0": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.15.1.tgz#4c91a0602733db63507e1dbf13187d6c71a153c4" - integrity sha512-V8eXYxNJ9QmXi5ETDguB7O9diAXlIyS+e3xzLoP/oVE4WCAjssxLIa0mqCLsCGXulYJUfT+GV70Jv1vHsdKwtA== - dependencies: - "@typescript-eslint/scope-manager" "4.15.1" - "@typescript-eslint/types" "4.15.1" - "@typescript-eslint/typescript-estree" "4.15.1" - debug "^4.1.1" - -"@typescript-eslint/scope-manager@4.15.1": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.15.1.tgz#f6511eb38def2a8a6be600c530c243bbb56ac135" - integrity sha512-ibQrTFcAm7yG4C1iwpIYK7vDnFg+fKaZVfvyOm3sNsGAerKfwPVFtYft5EbjzByDJ4dj1WD8/34REJfw/9wdVA== - dependencies: - "@typescript-eslint/types" "4.15.1" - "@typescript-eslint/visitor-keys" "4.15.1" - -"@typescript-eslint/types@4.15.1": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.15.1.tgz#da702f544ef1afae4bc98da699eaecd49cf31c8c" - integrity sha512-iGsaUyWFyLz0mHfXhX4zO6P7O3sExQpBJ2dgXB0G5g/8PRVfBBsmQIc3r83ranEQTALLR3Vko/fnCIVqmH+mPw== - -"@typescript-eslint/typescript-estree@4.15.1": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.15.1.tgz#fa9a9ff88b4a04d901ddbe5b248bc0a00cd610be" - integrity sha512-z8MN3CicTEumrWAEB2e2CcoZa3KP9+SMYLIA2aM49XW3cWIaiVSOAGq30ffR5XHxRirqE90fgLw3e6WmNx5uNw== - dependencies: - "@typescript-eslint/types" "4.15.1" - "@typescript-eslint/visitor-keys" "4.15.1" - debug "^4.1.1" - globby "^11.0.1" + version "4.27.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.27.0.tgz#85447e573364bce4c46c7f64abaa4985aadf5a94" + integrity sha512-XpbxL+M+gClmJcJ5kHnUpBGmlGdgNvy6cehgR6ufyxkEJMGP25tZKCaKyC0W/JVpuhU3VU1RBn7SYUPKSMqQvQ== + dependencies: + "@typescript-eslint/scope-manager" "4.27.0" + "@typescript-eslint/types" "4.27.0" + "@typescript-eslint/typescript-estree" "4.27.0" + debug "^4.3.1" + +"@typescript-eslint/scope-manager@4.27.0": + version "4.27.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.27.0.tgz#b0b1de2b35aaf7f532e89c8e81d0fa298cae327d" + integrity sha512-DY73jK6SEH6UDdzc6maF19AHQJBFVRf6fgAXHPXCGEmpqD4vYgPEzqpFz1lf/daSbOcMpPPj9tyXXDPW2XReAw== + dependencies: + "@typescript-eslint/types" "4.27.0" + "@typescript-eslint/visitor-keys" "4.27.0" + +"@typescript-eslint/types@4.27.0": + version "4.27.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.27.0.tgz#712b408519ed699baff69086bc59cd2fc13df8d8" + integrity sha512-I4ps3SCPFCKclRcvnsVA/7sWzh7naaM/b4pBO2hVxnM3wrU51Lveybdw5WoIktU/V4KfXrTt94V9b065b/0+wA== + +"@typescript-eslint/typescript-estree@4.27.0": + version "4.27.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.27.0.tgz#189a7b9f1d0717d5cccdcc17247692dedf7a09da" + integrity sha512-KH03GUsUj41sRLLEy2JHstnezgpS5VNhrJouRdmh6yNdQ+yl8w5LrSwBkExM+jWwCJa7Ct2c8yl8NdtNRyQO6g== + dependencies: + "@typescript-eslint/types" "4.27.0" + "@typescript-eslint/visitor-keys" "4.27.0" + debug "^4.3.1" + globby "^11.0.3" is-glob "^4.0.1" - semver "^7.3.2" - tsutils "^3.17.1" + semver "^7.3.5" + tsutils "^3.21.0" -"@typescript-eslint/visitor-keys@4.15.1": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.15.1.tgz#c76abbf2a3be8a70ed760f0e5756bf62de5865dd" - integrity sha512-tYzaTP9plooRJY8eNlpAewTOqtWW/4ff/5wBjNVaJ0S0wC4Gpq/zDVRTJa5bq2v1pCNQ08xxMCndcvR+h7lMww== +"@typescript-eslint/visitor-keys@4.27.0": + version "4.27.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.27.0.tgz#f56138b993ec822793e7ebcfac6ffdce0a60cb81" + integrity sha512-es0GRYNZp0ieckZ938cEANfEhsfHrzuLrePukLKtY3/KPXcq1Xd555Mno9/GOgXhKzn0QfkDLVgqWO3dGY80bg== dependencies: - "@typescript-eslint/types" "4.15.1" + "@typescript-eslint/types" "4.27.0" eslint-visitor-keys "^2.0.0" "@vendia/serverless-express@^3.4.0": @@ -1899,11 +1982,16 @@ acorn@^6.0.1, acorn@^6.0.7: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== -address@1.0.3, address@^1.0.1: +address@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9" integrity sha512-z55ocwKBRLryBs394Sm3ushTtBeg6VAeuku7utSoSnsJKvKcnXFIyC6vh27n3rXyxSgkJBBCAvyOn7gSUcTYjg== +address@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" + integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== + agent-base@4, agent-base@^4.2.0, agent-base@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" @@ -1911,6 +1999,13 @@ agent-base@4, agent-base@^4.2.0, agent-base@^4.3.0: dependencies: es6-promisify "^5.0.0" +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + agent-base@~4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" @@ -2075,10 +2170,10 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" @@ -2280,11 +2375,6 @@ array-equal@^1.0.0: resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= -array-filter@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" - integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM= - array-filter@~0.0.0: version "0.0.1" resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" @@ -2306,14 +2396,14 @@ array-flatten@^2.1.0: integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== array-includes@^3.0.3, array-includes@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.2.tgz#a8db03e0b88c8c6aeddc49cb132f9bcab4ebf9c8" - integrity sha512-w2GspexNQpx+PutG3QpT437/BenZBj0M/MZGn5mzv/MofYqo0xmRHzn4lFsoDlWJ+THYsGJmFlW68WlDFx7VRw== + version "3.1.3" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.3.tgz#c7f619b382ad2afaf5326cddfdc0afc61af7690a" + integrity sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" - get-intrinsic "^1.0.1" + es-abstract "^1.18.0-next.2" + get-intrinsic "^1.1.1" is-string "^1.0.5" array-map@~0.0.0: @@ -2358,6 +2448,17 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= +array.prototype.filter@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array.prototype.filter/-/array.prototype.filter-1.0.0.tgz#24d63e38983cdc6bf023a3c574b2f2a3f384c301" + integrity sha512-TfO1gz+tLm+Bswq0FBOXPqAchtCr2Rn48T8dLJoRFl8NoEosjZmzptmuo1X8aZBzZcqsR1W8U761tjACJtngTQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.18.0" + es-array-method-boxes-properly "^1.0.0" + is-string "^1.0.5" + array.prototype.find@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.1.tgz#3baca26108ca7affb08db06bf0be6cb3115a969c" @@ -2488,12 +2589,12 @@ atob@^2.1.2: integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== auth0-js@^9.14.3: - version "9.14.3" - resolved "https://registry.yarnpkg.com/auth0-js/-/auth0-js-9.14.3.tgz#1b6feee3c8a131906694713c82c68f46eabc953c" - integrity sha512-UO/fGv9641PUpYjz2nkPaUHzzrhNaJKupJOqt8blj1pD6wBgpZtxUSXBox6Y8md3eTBzpxeWxV+6RKzzERvr1g== + version "9.16.2" + resolved "https://registry.yarnpkg.com/auth0-js/-/auth0-js-9.16.2.tgz#7c4ca32add3d8f7419ce33deb8f4839179606779" + integrity sha512-cF1nRjmMDezmhJ+ZwwYp23F0gPqU0zNmF/VvTpcwvCrEMl9lAvkCd4iburN1I7G8SYaaIYEfcGedCphpDZw6OQ== dependencies: base64-js "^1.3.0" - idtoken-verifier "^2.0.3" + idtoken-verifier "^2.1.2" js-cookie "^2.2.0" qs "^6.7.0" superagent "^5.3.1" @@ -2514,9 +2615,9 @@ autoprefixer@^9.4.2: postcss-value-parser "^4.1.0" aws-sdk@^2.6.3: - version "2.828.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.828.0.tgz#6aa599c3582f219568f41fb287eb65753e4a9234" - integrity sha512-JoDujGdncSIF9ka+XFZjop/7G+fNGucwPwYj7OHYMmFIOV5p7YmqomdbVmH/vIzd988YZz8oLOinWc4jM6vvhg== + version "2.929.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.929.0.tgz#fa0bfc0e97f0b38b2086f83eb1546d946b6215c9" + integrity sha512-rJ36UbkGhB8qhR4eH0D+TgNPA6wwwKh4885fN/v9uToNd8/Fz8HdgNLw9uy0QYOFOgqK99eWfpMGQyOR6DL+Bg== dependencies: buffer "4.9.2" events "1.1.1" @@ -2888,6 +2989,30 @@ babel-plugin-named-asset-import@^0.3.1: resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.7.tgz#156cd55d3f1228a5765774340937afc8398067dd" integrity sha512-squySRkf+6JGnvjoUtDEjSREJEBirnXi9NqP6rjSYsylxQxqBTz+pkmf395i9E2zsvmYUaI40BHo6SqZUdydlw== +babel-plugin-polyfill-corejs2@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.2.tgz#e9124785e6fd94f94b618a7954e5693053bf5327" + integrity sha512-kISrENsJ0z5dNPq5eRvcctITNHYXWOA4DUZRFYCz3jYCcvTb/A546LIddmoGNMVYg2U38OyFeNosQwI9ENTqIQ== + dependencies: + "@babel/compat-data" "^7.13.11" + "@babel/helper-define-polyfill-provider" "^0.2.2" + semver "^6.1.1" + +babel-plugin-polyfill-corejs3@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.2.tgz#7424a1682ee44baec817327710b1b094e5f8f7f5" + integrity sha512-l1Cf8PKk12eEk5QP/NQ6TH8A1pee6wWDJ96WjxrMXFLHLOBFzYM4moG80HFgduVhTqAFez4alnZKEhP/bYHg0A== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.2.2" + core-js-compat "^3.9.1" + +babel-plugin-polyfill-regenerator@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.2.tgz#b310c8d642acada348c1fa3b3e6ce0e851bee077" + integrity sha512-Goy5ghsc21HgPDFtzRkSirpZVW35meGoTmTOb2bxqdl60ghub4xOidgNTHaZfQ2FaxQsKmwvXtOAkcIS4SMBWg== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.2.2" + babel-plugin-syntax-async-functions@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" @@ -3511,9 +3636,9 @@ babylon@^6.0.18, babylon@^6.18.0: integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base64-js@^1.0.2, base64-js@^1.3.0: version "1.5.1" @@ -3607,15 +3732,15 @@ bluebird@~2.10.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.10.2.tgz#024a5517295308857f14f91f1106fc3b555f446b" integrity sha1-AkpVFylTCIV/FPkfEQb8O1VfRGs= -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== bn.js@^5.0.0, bn.js@^5.1.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" - integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== + version "5.2.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" + integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== body-parser@1.19.0, body-parser@^1.15.2: version "1.19.0" @@ -3704,7 +3829,7 @@ braces@^3.0.1, braces@~3.0.2: dependencies: fill-range "^7.0.1" -brorand@^1.0.1: +brorand@^1.0.1, brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= @@ -3799,16 +3924,16 @@ browserslist@^3.2.6: caniuse-lite "^1.0.30000844" electron-to-chromium "^1.3.47" -browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.16.0, browserslist@^4.3.4, browserslist@^4.3.5: - version "4.16.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.1.tgz#bf757a2da376b3447b800a16f0f1c96358138766" - integrity sha512-UXhDrwqsNcpTYJBTZsbGATDxZbiVDsx6UjpmRUmtnP10pr8wAYr5LgFoEFw9ixriQH2mv/NX2SfGzE/o8GndLA== +browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.16.6, browserslist@^4.3.4, browserslist@^4.3.5: + version "4.16.6" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" + integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== dependencies: - caniuse-lite "^1.0.30001173" - colorette "^1.2.1" - electron-to-chromium "^1.3.634" + caniuse-lite "^1.0.30001219" + colorette "^1.2.2" + electron-to-chromium "^1.3.723" escalade "^3.1.1" - node-releases "^1.1.69" + node-releases "^1.1.71" bser@2.1.1: version "2.1.1" @@ -4078,10 +4203,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30000918, caniuse-lite@^1.0.30000929, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001173: - version "1.0.30001177" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001177.tgz#2c3b384933aafda03e29ccca7bb3d8c3389e1ece" - integrity sha512-6Ld7t3ifCL02jTj3MxPMM5wAYjbo4h/TAQGFTgv1inihP1tWnWp8mxxT4ut4JBEHLbpFXEXJJQ119JCJTBkYDw== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30000918, caniuse-lite@^1.0.30000929, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001219: + version "1.0.30001237" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001237.tgz#4b7783661515b8e7151fc6376cfd97f0e427b9e5" + integrity sha512-pDHgRndit6p1NR2GhzMbQ6CkRrp4VKuSsqbcLeOQppYPKOYkKT/6ZvZDvKJUqcmtyWIAHuZq3SVS2vc1egCZzw== capture-exit@^1.2.0: version "1.2.0" @@ -4142,9 +4267,9 @@ chalk@^3.0.0: supports-color "^7.1.0" chalk@^4.0.0, chalk@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" - integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + version "4.1.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" + integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" @@ -4192,29 +4317,29 @@ check-types@^7.3.0: resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.4.0.tgz#0378ec1b9616ec71f774931a3c6516fad8c152f4" integrity sha512-YbulWHdfP99UfZ73NcUDlNJhEIDgm9Doq9GhpyXbF+7Aegi3CVV7qqMCKTTqJxlvEvnQBp9IA+dxsGN6xK/nSg== -cheerio-select-tmp@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/cheerio-select-tmp/-/cheerio-select-tmp-0.1.1.tgz#55bbef02a4771710195ad736d5e346763ca4e646" - integrity sha512-YYs5JvbpU19VYJyj+F7oYrIE2BOll1/hRU7rEy/5+v9BzkSo3bK81iAeeQEMI92vRIxz677m72UmJUiVwwgjfQ== +cheerio-select@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.5.0.tgz#faf3daeb31b17c5e1a9dabcee288aaf8aafa5823" + integrity sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg== dependencies: - css-select "^3.1.2" - css-what "^4.0.0" - domelementtype "^2.1.0" - domhandler "^4.0.0" - domutils "^2.4.4" + css-select "^4.1.3" + css-what "^5.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + domutils "^2.7.0" cheerio@^1.0.0-rc.3: - version "1.0.0-rc.5" - resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.5.tgz#88907e1828674e8f9fee375188b27dadd4f0fa2f" - integrity sha512-yoqps/VCaZgN4pfXtenwHROTp8NG6/Hlt4Jpz2FEP0ZJQ+ZUkVDd0hAPDNKhj3nakpfPt/CNs57yEtxD1bXQiw== - dependencies: - cheerio-select-tmp "^0.1.0" - dom-serializer "~1.2.0" - domhandler "^4.0.0" - entities "~2.1.0" - htmlparser2 "^6.0.0" - parse5 "^6.0.0" - parse5-htmlparser2-tree-adapter "^6.0.0" + version "1.0.0-rc.10" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.10.tgz#2ba3dcdfcc26e7956fc1f440e61d51c643379f3e" + integrity sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw== + dependencies: + cheerio-select "^1.5.0" + dom-serializer "^1.3.2" + domhandler "^4.2.0" + htmlparser2 "^6.1.0" + parse5 "^6.0.1" + parse5-htmlparser2-tree-adapter "^6.0.1" + tslib "^2.2.0" chokidar@^1.6.1: version "1.7.0" @@ -4252,19 +4377,19 @@ chokidar@^2.0.0, chokidar@^2.0.4, chokidar@^2.1.2, chokidar@^2.1.8: fsevents "^1.2.7" chokidar@^3.2.2, chokidar@^3.4.1: - version "3.5.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.0.tgz#458a4816a415e9d3b3caa4faec2b96a6935a9e65" - integrity sha512-JgQM9JS92ZbFR4P90EvmzNpSGhpPBGBSj10PILeDyYFwp4h2/D9OM03wsJ4zW1fEp4ka2DGrnUeD7FuvQ2aZ2Q== + version "3.5.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" + integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== dependencies: - anymatch "~3.1.1" + anymatch "~3.1.2" braces "~3.0.2" - glob-parent "~5.1.0" + glob-parent "~5.1.2" is-binary-path "~2.1.0" is-glob "~4.0.1" normalize-path "~3.0.0" - readdirp "~3.5.0" + readdirp "~3.6.0" optionalDependencies: - fsevents "~2.3.1" + fsevents "~2.3.2" chownr@^1.0.1, chownr@^1.1.1: version "1.1.4" @@ -4272,11 +4397,9 @@ chownr@^1.0.1, chownr@^1.1.1: integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== chrome-trace-event@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" - integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== - dependencies: - tslib "^1.9.0" + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== ci-info@^1.5.0: version "1.6.0" @@ -4312,9 +4435,9 @@ class-utils@^0.3.5: static-extend "^0.1.1" classnames@^2.2.5: - version "2.2.6" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" - integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== + version "2.3.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" + integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== clean-css@4.2.x: version "4.2.3" @@ -4358,12 +4481,11 @@ cli-table3@~0.6.0: colors "^1.1.2" cli-table@^0.3.1: - version "0.3.4" - resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.4.tgz#5b37fd723751f1a6e9e70d55953a75e16eab958e" - integrity sha512-1vinpnX/ZERcmE443i3SZTmU5DF0rPO9DrL4I2iVAllhxzCM9SzPlHnz19fsZB78htkKZvYBvj6SZ6vXnaxmTA== + version "0.3.6" + resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.6.tgz#e9d6aa859c7fe636981fd3787378c2a20bce92fc" + integrity sha512-ZkNZbnZjKERTY5NwC2SeMeLeifSPq/pubeRoTpdr3WchLlnZg6hEgvHkK5zL7KNFdd9PmHN8lxrENUwI3cE8vQ== dependencies: - chalk "^2.4.1" - string-width "^4.2.0" + colors "1.0.3" cli-truncate@^0.2.1: version "0.2.1" @@ -4490,9 +4612,9 @@ color-name@^1.0.0, color-name@~1.1.4: integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== color-string@^1.5.4: - version "1.5.4" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6" - integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw== + version "1.5.5" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014" + integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg== dependencies: color-name "^1.0.0" simple-swizzle "^0.2.2" @@ -4515,10 +4637,15 @@ colorette@1.1.0: resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.1.0.tgz#1f943e5a357fac10b4e0f5aaef3b14cdc1af6ec7" integrity sha512-6S062WDQUXi6hOfkO/sBPVwE5ASXY4G2+b4atvhJfSsuUUhIaUKlkjLe9692Ipyt5/a+IPF5aVTu3V5gvXq5cg== -colorette@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" - integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== +colorette@^1.2.1, colorette@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" + integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== + +colors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" + integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= colors@^1.1.2: version "1.4.0" @@ -4577,7 +4704,7 @@ compare-versions@^3.6.0: resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== -component-emitter@^1.2.1, component-emitter@^1.3.0: +component-emitter@^1.2.0, component-emitter@^1.2.1, component-emitter@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== @@ -4703,7 +4830,7 @@ cookie@0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== -cookiejar@^2.1.2: +cookiejar@^2.1.0, cookiejar@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA== @@ -4733,18 +4860,18 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -core-js-compat@^3.8.0: - version "3.8.2" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.8.2.tgz#3717f51f6c3d2ebba8cbf27619b57160029d1d4c" - integrity sha512-LO8uL9lOIyRRrQmZxHZFl1RV+ZbcsAkFWTktn5SmH40WgLtSNYN4m4W2v9ONT147PxBY/XrRhrWq8TlvObyUjQ== +core-js-compat@^3.14.0, core-js-compat@^3.9.1: + version "3.14.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.14.0.tgz#b574dabf29184681d5b16357bd33d104df3d29a5" + integrity sha512-R4NS2eupxtiJU+VwgkF9WTpnSfZW4pogwKHd8bclWU2sp93Pr5S1uYJI84cMOubJRou7bcfL0vmwtLslWN5p3A== dependencies: - browserslist "^4.16.0" + browserslist "^4.16.6" semver "7.0.0" -core-js-pure@^3.0.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.10.0.tgz#dab9d6b141779b622b40567e7a536d2276646c15" - integrity sha512-CC582enhrFZStO4F8lGI7QL3SYx7/AIRc+IdSi3btrQGrVsTawo5K/crmKbRrQ+MOMhNX4v+PATn0k2NN6wI7A== +core-js-pure@^3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.14.0.tgz#72bcfacba74a65ffce04bf94ae91d966e80ee553" + integrity sha512-YVh+LN2FgNU0odThzm61BsdkwrbrchumFq3oztnE9vTKC4KS2fvnPmcx8t6jnqAyOTCTF4ZSiuK8Qhh7SNcL4g== core-js@2.6.4: version "2.6.4" @@ -4872,7 +4999,7 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" -crypto-js@^3.2.1: +crypto-js@3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.3.0.tgz#846dd1cce2f68aacfa156c8578f926a609b7976b" integrity sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q== @@ -4948,7 +5075,7 @@ css-select-base-adapter@^0.1.1: resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== -css-select@^2.0.0, css-select@^2.0.2: +css-select@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== @@ -4958,15 +5085,15 @@ css-select@^2.0.0, css-select@^2.0.2: domutils "^1.7.0" nth-check "^1.0.2" -css-select@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-3.1.2.tgz#d52cbdc6fee379fba97fb0d3925abbd18af2d9d8" - integrity sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA== +css-select@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.3.tgz#a70440f70317f2669118ad74ff105e65849c7067" + integrity sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA== dependencies: boolbase "^1.0.0" - css-what "^4.0.0" - domhandler "^4.0.0" - domutils "^2.4.3" + css-what "^5.0.0" + domhandler "^4.2.0" + domutils "^2.6.0" nth-check "^2.0.0" css-selector-tokenizer@^0.7.0: @@ -4986,9 +5113,9 @@ css-tree@1.0.0-alpha.37: source-map "^0.6.1" css-tree@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.2.tgz#9ae393b5dafd7dae8a622475caec78d3d8fbd7b5" - integrity sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ== + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== dependencies: mdn-data "2.0.14" source-map "^0.6.1" @@ -5006,10 +5133,10 @@ css-what@^3.2.1: resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== -css-what@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233" - integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A== +css-what@^5.0.0, css-what@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad" + integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg== cssdb@^4.3.0: version "4.4.0" @@ -5026,10 +5153,10 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -cssnano-preset-default@^4.0.7: - version "4.0.7" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76" - integrity sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA== +cssnano-preset-default@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz#920622b1fc1e95a34e8838203f1397a504f2d3ff" + integrity sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ== dependencies: css-declaration-sorter "^4.0.1" cssnano-util-raw-cache "^4.0.1" @@ -5059,7 +5186,7 @@ cssnano-preset-default@^4.0.7: postcss-ordered-values "^4.1.2" postcss-reduce-initial "^4.0.3" postcss-reduce-transforms "^4.0.2" - postcss-svgo "^4.0.2" + postcss-svgo "^4.0.3" postcss-unique-selectors "^4.0.1" cssnano-util-get-arguments@^4.0.0: @@ -5085,12 +5212,12 @@ cssnano-util-same-parent@^4.0.0: integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q== cssnano@^4.1.0: - version "4.1.10" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.10.tgz#0ac41f0b13d13d465487e111b778d42da631b8b2" - integrity sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ== + version "4.1.11" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.11.tgz#c7b5f5b81da269cb1fd982cb960c1200910c9a99" + integrity sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g== dependencies: cosmiconfig "^5.0.0" - cssnano-preset-default "^4.0.7" + cssnano-preset-default "^4.0.8" is-resolvable "^1.0.0" postcss "^7.0.0" @@ -5114,14 +5241,14 @@ cssstyle@^1.0.0: cssom "0.3.x" csstype@^2.5.2: - version "2.6.16" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.16.tgz#544d69f547013b85a40d15bff75db38f34fe9c39" - integrity sha512-61FBWoDHp/gRtsoDkq/B1nWrCUG/ok1E3tUrcNbZjsE9Cxd9yzUirjS3+nAATB8U4cTtaQmAHbNndoFz5L6C9Q== + version "2.6.17" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.17.tgz#4cf30eb87e1d1a005d8b6510f95292413f6a1c0e" + integrity sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A== csstype@^3.0.2: - version "3.0.6" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.6.tgz#865d0b5833d7d8d40f4e5b8a6d76aea3de4725ef" - integrity sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw== + version "3.0.8" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340" + integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw== currently-unhandled@^0.4.1: version "0.4.1" @@ -5200,9 +5327,9 @@ d@1, d@^1.0.1: type "^1.0.1" damerau-levenshtein@^1.0.0, damerau-levenshtein@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791" - integrity sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug== + version "1.0.7" + resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz#64368003512a1a6992593741a09a9d31a836f55d" + integrity sha512-VvdQIPGdWP0SqFXghj79Wf/5LArmreyMsGLa6FG6iC4t3j7j5s71TrwWmT/4akbDQIqjfACkLZmjXhA7g2oUZw== dashdash@^1.12.0: version "1.14.1" @@ -5236,14 +5363,14 @@ date-fns@^1.27.2: integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== date-fns@^2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.19.0.tgz#65193348635a28d5d916c43ec7ce6fbd145059e1" - integrity sha512-X3bf2iTPgCAQp9wvjOQytnf5vO5rESYRXlPIVcgSbtT5OTScPcsf9eZU+B/YIkKAtYr5WeCii58BgATrNitlWg== + version "2.22.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.22.1.tgz#1e5af959831ebb1d82992bf67b765052d8f0efc4" + integrity sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg== dayjs@^1.8.29: - version "1.10.3" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.3.tgz#cf3357c8e7f508432826371672ebf376cb7d619b" - integrity sha512-/2fdLN987N8Ki7Id8BUN2nhuiRyxTLumQnSQf9CNncFCyqFsSKb9TNhzRYcC8K8eJSJOKvbvkImo/MKKhNi4iw== + version "1.10.5" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.5.tgz#5600df4548fc2453b3f163ebb2abbe965ccfb986" + integrity sha512-BUFis41ikLz+65iH6LHQCDm4YPMj5r1YFLdupPIyM4SGcXMmtiLQ7U37i+hGS8urIuqe7I/ou3IS1jVc4nbN4g== debug@2, debug@2.6.9, debug@^2.1.1, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9: version "2.6.9" @@ -5266,6 +5393,13 @@ debug@3.2.6: dependencies: ms "^2.1.1" +debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== + dependencies: + ms "2.1.2" + debug@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" @@ -5273,20 +5407,13 @@ debug@4.1.1: dependencies: ms "^2.1.1" -debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: +debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" -debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== - dependencies: - ms "2.1.2" - decache@^3.0.5: version "3.1.0" resolved "https://registry.yarnpkg.com/decache/-/decache-3.1.0.tgz#4f5036fbd6581fcc97237ac3954a244b9536c2da" @@ -5490,9 +5617,9 @@ detect-newline@^2.1.0: integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= detect-node@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" - integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== detect-port-alt@1.1.6: version "1.1.6" @@ -5551,9 +5678,9 @@ dns-equal@^1.0.0: integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= dns-packet@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a" - integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg== + version "1.3.4" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.4.tgz#e3455065824a2507ba886c55a89963bb107dec6f" + integrity sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA== dependencies: ip "^1.1.0" safe-buffer "^5.0.1" @@ -5588,7 +5715,7 @@ doctrine@^2.1.0: dependencies: esutils "^2.0.2" -dom-converter@^0.2: +dom-converter@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== @@ -5603,9 +5730,9 @@ dom-helpers@^3.2.0: "@babel/runtime" "^7.1.2" dom-helpers@^5.0.1, dom-helpers@^5.1.3: - version "5.2.0" - resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.0.tgz#57fd054c5f8f34c52a3eeffdb7e7e93cd357d95b" - integrity sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ== + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== dependencies: "@babel/runtime" "^7.8.7" csstype "^3.0.2" @@ -5618,13 +5745,13 @@ dom-serializer@0: domelementtype "^2.0.1" entities "^2.0.0" -dom-serializer@^1.0.1, dom-serializer@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1" - integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA== +dom-serializer@^1.0.1, dom-serializer@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" + integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== dependencies: domelementtype "^2.0.1" - domhandler "^4.0.0" + domhandler "^4.2.0" entities "^2.0.0" dom-walk@^0.1.0: @@ -5637,15 +5764,15 @@ domain-browser@^1.1.1: resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== -domelementtype@1, domelementtype@^1.3.1: +domelementtype@1: version "1.3.1" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== -domelementtype@^2.0.1, domelementtype@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" - integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" + integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== domexception@^1.0.1: version "1.0.1" @@ -5654,21 +5781,14 @@ domexception@^1.0.1: dependencies: webidl-conversions "^4.0.2" -domhandler@^2.3.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" - integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== - dependencies: - domelementtype "1" - -domhandler@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.0.0.tgz#01ea7821de996d85f69029e81fa873c21833098e" - integrity sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA== +domhandler@^4.0.0, domhandler@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059" + integrity sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA== dependencies: - domelementtype "^2.1.0" + domelementtype "^2.2.0" -domutils@^1.5.1, domutils@^1.7.0: +domutils@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== @@ -5676,14 +5796,14 @@ domutils@^1.5.1, domutils@^1.7.0: dom-serializer "0" domelementtype "1" -domutils@^2.4.3, domutils@^2.4.4: - version "2.4.4" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3" - integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA== +domutils@^2.5.2, domutils@^2.6.0, domutils@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.7.0.tgz#8ebaf0c41ebafcf55b0b72ec31c56323712c5442" + integrity sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg== dependencies: dom-serializer "^1.0.1" - domelementtype "^2.0.1" - domhandler "^4.0.0" + domelementtype "^2.2.0" + domhandler "^4.2.0" dot-prop@^5.2.0: version "5.3.0" @@ -5760,10 +5880,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.103, electron-to-chromium@^1.3.47, electron-to-chromium@^1.3.634: - version "1.3.639" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.639.tgz#0a27e3018ae3acf438a14a1dd4e41db4b8ab764e" - integrity sha512-bwl6/U6xb3d3CNufQU9QeO1L32ueouFwW4bWANSwdXR7LVqyLzWjNbynoKNfuC38QFB5Qn7O0l2KLqBkcXnC3Q== +electron-to-chromium@^1.3.103, electron-to-chromium@^1.3.47, electron-to-chromium@^1.3.723: + version "1.3.752" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.752.tgz#0728587f1b9b970ec9ffad932496429aef750d09" + integrity sha512-2Tg+7jSl3oPxgsBsWKh5H83QazTkmWG/cnNwJplmyZc7KcN61+I10oUgaXSVk/NwfvN3BdkKDR4FYuRBQQ2v0A== elegant-spinner@^1.0.1: version "1.0.1" @@ -5771,17 +5891,17 @@ elegant-spinner@^1.0.1: integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= elliptic@^6.5.3: - version "6.5.3" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" - integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== dependencies: - bn.js "^4.4.0" - brorand "^1.0.1" + bn.js "^4.11.9" + brorand "^1.1.0" hash.js "^1.0.0" - hmac-drbg "^1.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" emoji-regex@^6.5.1: version "6.5.1" @@ -5846,15 +5966,10 @@ enhanced-resolve@^4.1.0: memory-fs "^0.5.0" tapable "^1.0.0" -entities@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" - integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== - -entities@^2.0.0, entities@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" - integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== enzyme-adapter-react-16@^1.15.6: version "1.15.6" @@ -5941,10 +6056,10 @@ error-stack-parser@^2.0.4: dependencies: stackframe "^1.1.1" -es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4: - version "1.18.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0.tgz#ab80b359eecb7ede4c298000390bc5ac3ec7b5a4" - integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw== +es-abstract@^1.17.2, es-abstract@^1.17.4, es-abstract@^1.18.0, es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2: + version "1.18.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.3.tgz#25c4c3380a27aa203c44b2b685bba94da31b63e0" + integrity sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw== dependencies: call-bind "^1.0.2" es-to-primitive "^1.2.1" @@ -5954,32 +6069,19 @@ es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4: has-symbols "^1.0.2" is-callable "^1.2.3" is-negative-zero "^2.0.1" - is-regex "^1.1.2" - is-string "^1.0.5" - object-inspect "^1.9.0" + is-regex "^1.1.3" + is-string "^1.0.6" + object-inspect "^1.10.3" object-keys "^1.1.1" object.assign "^4.1.2" string.prototype.trimend "^1.0.4" string.prototype.trimstart "^1.0.4" - unbox-primitive "^1.0.0" + unbox-primitive "^1.0.1" -es-abstract@^1.18.0-next.1: - version "1.18.0-next.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" - integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-negative-zero "^2.0.0" - is-regex "^1.1.1" - object-inspect "^1.8.0" - object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" +es-array-method-boxes-properly@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" + integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== es-to-primitive@^1.2.1: version "1.2.1" @@ -6043,7 +6145,7 @@ es6-set@^0.1.4, es6-set@~0.1.5: es6-symbol "3.1.1" event-emitter "~0.3.5" -es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1: +es6-symbol@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" integrity sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc= @@ -6051,7 +6153,7 @@ es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1: d "1" es5-ext "~0.10.14" -es6-symbol@~3.1.3: +es6-symbol@^3.1.1, es6-symbol@~3.1.1, es6-symbol@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== @@ -6171,11 +6273,11 @@ eslint-loader@2.1.1: rimraf "^2.6.1" eslint-module-utils@^2.2.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6" - integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA== + version "2.6.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.1.tgz#b51be1e473dd0de1c5ea638e22429c2490ea8233" + integrity sha512-ZXI9B8cxAJIH4nfkhTwcRTEAnrVfobYqwjWy/QMCZ8rHkZHFjf9yO4BzpiF9kCSfNlMG54eKigISHpX0+AaT4A== dependencies: - debug "^2.6.9" + debug "^3.2.7" pkg-dir "^2.0.0" eslint-plugin-flowtype@2.50.1: @@ -6296,9 +6398,9 @@ eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== eslint-visitor-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" - integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== eslint@2.13.1: version "2.13.1" @@ -6415,9 +6517,9 @@ esprima@^4.0.0, esprima@^4.0.1: integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== esquery@^1.0.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" - integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== dependencies: estraverse "^5.1.0" @@ -6462,9 +6564,9 @@ event-target-shim@^5.0.0: integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== eventemitter2@^6.4.2: - version "6.4.3" - resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.3.tgz#35c563619b13f3681e7eb05cbdaf50f56ba58820" - integrity sha512-t0A2msp6BzOf+QAcI6z9XMktLj52OjGQg+8SJH6v5+3uxNpWYRR3wQmfA+6xtMU9kOC59qk9licus5dYcrYkMQ== + version "6.4.4" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.4.tgz#aa96e8275c4dbeb017a5d0e03780c65612a1202b" + integrity sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw== eventemitter3@^4.0.0: version "4.0.7" @@ -6477,9 +6579,9 @@ events@1.1.1: integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= events@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" - integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== eventsource@0.1.6: version "0.1.6" @@ -6489,9 +6591,9 @@ eventsource@0.1.6: original ">=0.0.5" eventsource@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" - integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ== + version "1.1.0" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.0.tgz#00e8ca7c92109e94b0ddf32dac677d841028cfaf" + integrity sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg== dependencies: original "^1.0.0" @@ -6817,9 +6919,9 @@ fastparse@^1.1.2: integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ== fastq@^1.6.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.10.0.tgz#74dbefccade964932cdf500473ef302719c652bb" - integrity sha512-NL2Qc5L3iQEsyYzweq7qfgy5OtXCmGzGvhElGEd/SoFWEMOEczNh5s5ocaF01HDetxz+p8ecjNPA6cZxxIHmzA== + version "1.11.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" + integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== dependencies: reusify "^1.0.4" @@ -6831,9 +6933,9 @@ faye-websocket@^0.10.0: websocket-driver ">=0.5.1" faye-websocket@~0.11.0, faye-websocket@~0.11.1: - version "0.11.3" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" - integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== dependencies: websocket-driver ">=0.5.1" @@ -7100,9 +7202,9 @@ flush-write-stream@^1.0.0: readable-stream "^2.3.6" follow-redirects@^1.0.0, follow-redirects@^1.10.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7" - integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg== + version "1.14.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43" + integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg== for-each@^0.3.3: version "0.3.3" @@ -7163,7 +7265,7 @@ fork-ts-checker-webpack-plugin@1.0.0-alpha.6: semver "^5.6.0" tapable "^1.0.0" -form-data@^2.3.2: +form-data@^2.3.1, form-data@^2.3.2: version "2.5.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== @@ -7173,9 +7275,9 @@ form-data@^2.3.2: mime-types "^2.1.12" form-data@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" - integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg== + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" @@ -7190,15 +7292,15 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -formidable@^1.2.2: +formidable@^1.2.0, formidable@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== fragment-cache@^0.2.1: version "0.2.1" @@ -7263,14 +7365,14 @@ fs-extra@^4.0.2: universalify "^0.1.0" fs-extra@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" - integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== dependencies: at-least-node "^1.0.0" graceful-fs "^4.2.0" jsonfile "^6.0.1" - universalify "^1.0.0" + universalify "^2.0.0" fs-minipass@^1.2.5: version "1.2.7" @@ -7320,10 +7422,10 @@ fsevents@^1.0.0, fsevents@^1.2.3, fsevents@^1.2.7: bindings "^1.5.0" nan "^2.12.1" -fsevents@~2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.1.tgz#b209ab14c61012636c8863507edf7fb68cc54e9f" - integrity sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw== +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== ftp@~0.3.10: version "0.3.10" @@ -7339,21 +7441,21 @@ function-bind@^1.1.1: integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== function.prototype.name@^1.1.2, function.prototype.name@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.3.tgz#0bb034bb308e7682826f215eb6b2ae64918847fe" - integrity sha512-H51qkbNSp8mtkJt+nyW1gyStBiKZxfRqySNUR99ylq6BPXHKI4SEvIlTKp4odLfjRKJV04DFWMU3G/YRlQOsag== + version "1.1.4" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.4.tgz#e4ea839b9d3672ae99d0efd9f38d9191c5eaac83" + integrity sha512-iqy1pIotY/RmhdFZygSSlW0wko2yxkSCKqsuv4pr8QESohpYyG/Z7B/XXvPRKTJS//960rgguE5mSRUsDdaJrQ== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" - functions-have-names "^1.2.1" + es-abstract "^1.18.0-next.2" + functions-have-names "^1.2.2" functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -functions-have-names@^1.2.1: +functions-have-names@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21" integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA== @@ -7409,7 +7511,7 @@ generaterr@^1.5.0: resolved "https://registry.yarnpkg.com/generaterr/-/generaterr-1.5.0.tgz#b0ceb6cc5164df2a061338cc340a8615395c52fc" integrity sha1-sM62zFFk3yoGEzjMNAqGFTlcUvw= -gensync@^1.0.0-beta.1: +gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== @@ -7419,16 +7521,7 @@ get-caller-file@^1.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== -get-intrinsic@^1.0.1, get-intrinsic@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.0.2.tgz#6820da226e50b24894e08859469dc68361545d49" - integrity sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - -get-intrinsic@^1.1.1: +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== @@ -7535,10 +7628,10 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^5.1.0, glob-parent@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== +glob-parent@^5.1.0, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" @@ -7548,9 +7641,9 @@ glob-to-regexp@^0.3.0: integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -7647,10 +7740,10 @@ globby@^10.0.1: merge2 "^1.2.3" slash "^3.0.0" -globby@^11.0.1: - version "11.0.2" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.2.tgz#1af538b766a3b540ebfb58a32b2e2d5897321d83" - integrity sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og== +globby@^11.0.3: + version "11.0.3" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb" + integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg== dependencies: array-union "^2.1.0" dir-glob "^3.0.1" @@ -7686,9 +7779,9 @@ google-auth-library@^3.0.0: semver "^5.5.0" google-libphonenumber@^3.0.0: - version "3.2.15" - resolved "https://registry.yarnpkg.com/google-libphonenumber/-/google-libphonenumber-3.2.15.tgz#3a01dc554dbf83c754f249c16df3605e5d154bb9" - integrity sha512-tbCIuzMoH34RdrbFRw5kijAZn/p6JMQvsgtr1glg2ugbwqrMPlOL8pHNK8cyGo9B6SXpcMm4hdyDqwomR+HPRg== + version "3.2.21" + resolved "https://registry.yarnpkg.com/google-libphonenumber/-/google-libphonenumber-3.2.21.tgz#6c01e037ef580dd5c580e6bf3129aa6c1581969f" + integrity sha512-d8dMePLPIZXHGEvyGM4PTEPBxXC29mhXtqruD11iZd9KzyKb216kJuBPZq6m3BTmiI5ZiIb4epzrZsatRJ5ZaA== google-p12-pem@^1.0.0: version "1.0.4" @@ -7736,9 +7829,9 @@ got@^9.6.0: url-parse-lax "^3.0.0" graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2: - version "4.2.4" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== + version "4.2.6" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== graphql-date@^1.0.3: version "1.0.3" @@ -7756,9 +7849,11 @@ graphql-extensions@^0.0.x, graphql-extensions@~0.0.9: source-map-support "^0.5.1" graphql-tag@^2.10.3: - version "2.11.0" - resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.11.0.tgz#1deb53a01c46a7eb401d6cb59dec86fa1cccbffd" - integrity sha512-VmsD5pJqWJnQZMUeRwrDhfgoyqcfwEkvtpANqcoUG8/tOLkwNgU9mzub/Mc78OJMhHjx7gfAMTxzdG43VGg3bA== + version "2.12.4" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.4.tgz#d34066688a4f09e72d6f4663c74211e9b4b7c4bf" + integrity sha512-VV1U4O+9x99EkNpNmCUV5RZwq6MnK4+pGbRYWG+lA/m3uo7TSqJF81OkcOP148gFP6fzdl7JWYBrwWVTS9jXww== + dependencies: + tslib "^2.1.0" graphql-tools@^2.8.0: version "2.24.0" @@ -7813,9 +7908,9 @@ handle-thing@^2.0.0: integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== handlebars@^4.0.3: - version "4.7.6" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" - integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== + version "4.7.7" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== dependencies: minimist "^1.2.5" neo-async "^2.6.0" @@ -7838,9 +7933,9 @@ har-validator@~5.1.3: har-schema "^2.0.0" harmony-reflect@^1.4.6: - version "1.6.1" - resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.1.tgz#c108d4f2bb451efef7a37861fdbdae72c9bdefa9" - integrity sha512-WJTeyp0JzGtHcuMsi7rw2VwtkvLa+JyfEKJCFyfcS0+CDkjQ5lHPu7zEhFZP+PDSRrEgXa5Ah0l1MbgbE41XjA== + version "1.6.2" + resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.2.tgz#31ecbd32e648a34d030d86adb67d4d47547fe710" + integrity sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g== has-ansi@^2.0.0: version "2.0.0" @@ -7874,12 +7969,7 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== - -has-symbols@^1.0.2: +has-symbols@^1.0.1, has-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== @@ -7974,7 +8064,7 @@ history@^3.0.0: query-string "^4.2.2" warning "^3.0.0" -hmac-drbg@^1.0.0: +hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= @@ -8021,9 +8111,9 @@ hoopy@^0.1.2: integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== hosted-git-info@^2.1.4: - version "2.8.8" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" - integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== hpack.js@^2.1.6: version "2.1.6" @@ -8045,17 +8135,13 @@ hsla-regex@^1.0.0: resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= -html-comment-regex@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" - integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== - html-element-map@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.2.0.tgz#dfbb09efe882806af63d990cf6db37993f099f22" - integrity sha512-0uXq8HsuG1v2TmQ8QkIhzbrqeskE4kn52Q18QJ9iAA/SnHoEKXWiUxHQtclRsCFWEUD2So34X+0+pZZu862nnw== + version "1.3.1" + resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.3.1.tgz#44b2cbcfa7be7aa4ff59779e47e51012e1c73c08" + integrity sha512-6XMlxrAFX4UEEGxctfFnmrFaaZFNf9i5fNuV5wZ3WWQ4FVaNP1aX1LkX9j2mfEx1NpjeE/rL3nmgEn23GdFmrg== dependencies: - array-filter "^1.0.0" + array.prototype.filter "^1.0.0" + call-bind "^1.0.2" html-encoding-sniffer@^1.0.2: version "1.0.2" @@ -8095,26 +8181,14 @@ html-webpack-plugin@4.0.0-alpha.2: tapable "^1.0.0" util.promisify "1.0.0" -htmlparser2@^3.10.1: - version "3.10.1" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" - integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== - dependencies: - domelementtype "^1.3.1" - domhandler "^2.3.0" - domutils "^1.5.1" - entities "^1.1.1" - inherits "^2.0.1" - readable-stream "^3.1.1" - -htmlparser2@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.0.tgz#c2da005030390908ca4c91e5629e418e0665ac01" - integrity sha512-numTQtDZMoh78zJpaNdJ9MXb2cv5G3jwUoe3dMQODubZvLoGvTE/Ofp6sHvH8OGKcN/8A47pGLi/k58xHP/Tfw== +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== dependencies: domelementtype "^2.0.1" domhandler "^4.0.0" - domutils "^2.4.4" + domutils "^2.5.2" entities "^2.0.0" http-cache-semantics@^4.0.0: @@ -8231,6 +8305,14 @@ https-proxy-agent@^3.0.0: agent-base "^4.3.0" debug "^3.1.0" +https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -8270,9 +8352,9 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: safer-buffer ">= 2.1.2 < 3" iconv-lite@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01" - integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ== + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== dependencies: safer-buffer ">= 2.1.2 < 3.0.0" @@ -8295,13 +8377,13 @@ identity-obj-proxy@3.0.0: dependencies: harmony-reflect "^1.4.6" -idtoken-verifier@^2.0.3: - version "2.1.0" - resolved "https://registry.yarnpkg.com/idtoken-verifier/-/idtoken-verifier-2.1.0.tgz#e61ea083be596390012aff6d9f12c2599af4847b" - integrity sha512-X0423UM4Rc5bFb39Ai0YHr35rcexlu4oakKdYzSGZxtoPy84P86hhAbzlpgbgomcLOFRgzgKRvhY7YjO5g8OPA== +idtoken-verifier@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/idtoken-verifier/-/idtoken-verifier-2.1.2.tgz#185ec29b70400b47a1d48b068e7b95d1bbf7dcef" + integrity sha512-YMHiP9zAMjB+pWreV4EHnIj3XCQ168+InWirVRFeRtlsMQIK61S+LLnyLGI8EL0wtlk/v7ya69Gjfio3P9/7Gw== dependencies: base64-js "^1.3.0" - crypto-js "^3.2.1" + crypto-js "3.3.0" es6-promise "^4.2.8" jsbn "^1.1.0" unfetch "^4.1.0" @@ -8328,9 +8410,9 @@ ignore-by-default@^1.0.1: integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= ignore-walk@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" - integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw== + version "3.0.4" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335" + integrity sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ== dependencies: minimatch "^3.0.4" @@ -8663,9 +8745,9 @@ is-arrayish@^0.3.1: integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== is-bigint@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.1.tgz#6923051dfcbc764278540b9ce0e6b3213aa5ebc2" - integrity sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg== + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a" + integrity sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA== is-binary-path@^1.0.0: version "1.0.1" @@ -8682,11 +8764,11 @@ is-binary-path@~2.1.0: binary-extensions "^2.0.0" is-boolean-object@^1.0.1, is-boolean-object@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.0.tgz#e2aaad3a3a8fca34c28f6eee135b156ed2587ff0" - integrity sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA== + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.1.tgz#3c0878f035cb821228d350d2e1e36719716a3de8" + integrity sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" is-buffer@^1.0.2, is-buffer@^1.1.5: version "1.1.6" @@ -8698,12 +8780,7 @@ is-buffer@~2.0.3: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" - integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== - -is-callable@^1.2.3: +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== @@ -8734,10 +8811,10 @@ is-color-stop@^1.0.0: rgb-regex "^1.0.1" rgba-regex "^1.0.0" -is-core-module@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" - integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== +is-core-module@^2.2.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1" + integrity sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A== dependencies: has "^1.0.3" @@ -8756,9 +8833,9 @@ is-data-descriptor@^1.0.0: kind-of "^6.0.0" is-date-object@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.4.tgz#550cfcc03afada05eea3dd30981c7b09551f73e5" + integrity sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A== is-descriptor@^0.1.0: version "0.1.6" @@ -8894,7 +8971,7 @@ is-my-json-valid@^2.10.0: jsonpointer "^4.0.0" xtend "^4.0.0" -is-negative-zero@^2.0.0, is-negative-zero@^2.0.1: +is-negative-zero@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== @@ -8905,9 +8982,9 @@ is-npm@^4.0.0: integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== is-number-object@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" - integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.5.tgz#6edfaeed7950cff19afedce9fbfca9ee6dd289eb" + integrity sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw== is-number@^2.1.0: version "2.1.0" @@ -8975,9 +9052,9 @@ is-path-inside@^1.0.0: path-is-inside "^1.0.1" is-path-inside@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" - integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" @@ -9006,20 +9083,13 @@ is-property@^1.0.0, is-property@^1.0.2: resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ= -is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" - integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== - dependencies: - has-symbols "^1.0.1" - -is-regex@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" - integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== +is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f" + integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ== dependencies: call-bind "^1.0.2" - has-symbols "^1.0.1" + has-symbols "^1.0.2" is-regexp@^1.0.0: version "1.0.0" @@ -9053,29 +9123,22 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== -is-string@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" - integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== +is-string@^1.0.5, is-string@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f" + integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w== is-subset@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY= -is-svg@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" - integrity sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ== - dependencies: - html-comment-regex "^1.1.0" - is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== dependencies: - has-symbols "^1.0.1" + has-symbols "^1.0.2" is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" @@ -9089,6 +9152,11 @@ is-unc-path@^1.0.0: dependencies: unc-path-regex "^0.1.2" +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + is-url@^1.2.2: version "1.2.4" resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" @@ -9790,9 +9858,9 @@ json5@^1.0.1: minimist "^1.2.0" json5@^2.1.0, json5@^2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" - integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== + version "2.2.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" + integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== dependencies: minimist "^1.2.5" @@ -9856,69 +9924,69 @@ jsprim@^1.2.2: verror "1.10.0" jss-plugin-camel-case@^10.5.1: - version "10.5.1" - resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.5.1.tgz#427b24a9951b4c2eaa7e3d5267acd2e00b0934f9" - integrity sha512-9+oymA7wPtswm+zxVti1qiowC5q7bRdCJNORtns2JUj/QHp2QPXYwSNRD8+D2Cy3/CEMtdJzlNnt5aXmpS6NAg== + version "10.6.0" + resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.6.0.tgz#93d2cd704bf0c4af70cc40fb52d74b8a2554b170" + integrity sha512-JdLpA3aI/npwj3nDMKk308pvnhoSzkW3PXlbgHAzfx0yHWnPPVUjPhXFtLJzgKZge8lsfkUxvYSQ3X2OYIFU6A== dependencies: "@babel/runtime" "^7.3.1" hyphenate-style-name "^1.0.3" - jss "10.5.1" + jss "10.6.0" jss-plugin-default-unit@^10.5.1: - version "10.5.1" - resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.5.1.tgz#2be385d71d50aee2ee81c2a9ac70e00592ed861b" - integrity sha512-D48hJBc9Tj3PusvlillHW8Fz0y/QqA7MNmTYDQaSB/7mTrCZjt7AVRROExoOHEtd2qIYKOYJW3Jc2agnvsXRlQ== + version "10.6.0" + resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.6.0.tgz#af47972486819b375f0f3a9e0213403a84b5ef3b" + integrity sha512-7y4cAScMHAxvslBK2JRK37ES9UT0YfTIXWgzUWD5euvR+JR3q+o8sQKzBw7GmkQRfZijrRJKNTiSt1PBsLI9/w== dependencies: "@babel/runtime" "^7.3.1" - jss "10.5.1" + jss "10.6.0" jss-plugin-global@^10.5.1: - version "10.5.1" - resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.5.1.tgz#0e1793dea86c298360a7e2004721351653c7e764" - integrity sha512-jX4XpNgoaB8yPWw/gA1aPXJEoX0LNpvsROPvxlnYe+SE0JOhuvF7mA6dCkgpXBxfTWKJsno7cDSCgzHTocRjCQ== + version "10.6.0" + resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.6.0.tgz#3e8011f760f399cbadcca7f10a485b729c50e3ed" + integrity sha512-I3w7ji/UXPi3VuWrTCbHG9rVCgB4yoBQLehGDTmsnDfXQb3r1l3WIdcO8JFp9m0YMmyy2CU7UOV6oPI7/Tmu+w== dependencies: "@babel/runtime" "^7.3.1" - jss "10.5.1" + jss "10.6.0" jss-plugin-nested@^10.5.1: - version "10.5.1" - resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.5.1.tgz#8753a80ad31190fb6ac6fdd39f57352dcf1295bb" - integrity sha512-xXkWKOCljuwHNjSYcXrCxBnjd8eJp90KVFW1rlhvKKRXnEKVD6vdKXYezk2a89uKAHckSvBvBoDGsfZrldWqqQ== + version "10.6.0" + resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.6.0.tgz#5f83c5c337d3b38004834e8426957715a0251641" + integrity sha512-fOFQWgd98H89E6aJSNkEh2fAXquC9aZcAVjSw4q4RoQ9gU++emg18encR4AT4OOIFl4lQwt5nEyBBRn9V1Rk8g== dependencies: "@babel/runtime" "^7.3.1" - jss "10.5.1" + jss "10.6.0" tiny-warning "^1.0.2" jss-plugin-props-sort@^10.5.1: - version "10.5.1" - resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.5.1.tgz#ab1c167fd2d4506fb6a1c1d66c5f3ef545ff1cd8" - integrity sha512-t+2vcevNmMg4U/jAuxlfjKt46D/jHzCPEjsjLRj/J56CvP7Iy03scsUP58Iw8mVnaV36xAUZH2CmAmAdo8994g== + version "10.6.0" + resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.6.0.tgz#297879f35f9fe21196448579fee37bcde28ce6bc" + integrity sha512-oMCe7hgho2FllNc60d9VAfdtMrZPo9n1Iu6RNa+3p9n0Bkvnv/XX5San8fTPujrTBScPqv9mOE0nWVvIaohNuw== dependencies: "@babel/runtime" "^7.3.1" - jss "10.5.1" + jss "10.6.0" jss-plugin-rule-value-function@^10.5.1: - version "10.5.1" - resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.5.1.tgz#37f4030523fb3032c8801fab48c36c373004de7e" - integrity sha512-3gjrSxsy4ka/lGQsTDY8oYYtkt2esBvQiceGBB4PykXxHoGRz14tbCK31Zc6DHEnIeqsjMUGbq+wEly5UViStQ== + version "10.6.0" + resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.6.0.tgz#3c1a557236a139d0151e70a82c810ccce1c1c5ea" + integrity sha512-TKFqhRTDHN1QrPTMYRlIQUOC2FFQb271+AbnetURKlGvRl/eWLswcgHQajwuxI464uZk91sPiTtdGi7r7XaWfA== dependencies: "@babel/runtime" "^7.3.1" - jss "10.5.1" + jss "10.6.0" tiny-warning "^1.0.2" jss-plugin-vendor-prefixer@^10.5.1: - version "10.5.1" - resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.5.1.tgz#45a183a3a0eb097bdfab0986b858d99920c0bbd8" - integrity sha512-cLkH6RaPZWHa1TqSfd2vszNNgxT1W0omlSjAd6hCFHp3KIocSrW21gaHjlMU26JpTHwkc+tJTCQOmE/O1A4FKQ== + version "10.6.0" + resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.6.0.tgz#e1fcd499352846890c38085b11dbd7aa1c4f2c78" + integrity sha512-doJ7MouBXT1lypLLctCwb4nJ6lDYqrTfVS3LtXgox42Xz0gXusXIIDboeh6UwnSmox90QpVnub7au8ybrb0krQ== dependencies: "@babel/runtime" "^7.3.1" css-vendor "^2.0.8" - jss "10.5.1" + jss "10.6.0" -jss@10.5.1, jss@^10.5.1: - version "10.5.1" - resolved "https://registry.yarnpkg.com/jss/-/jss-10.5.1.tgz#93e6b2428c840408372d8b548c3f3c60fa601c40" - integrity sha512-hbbO3+FOTqVdd7ZUoTiwpHzKXIo5vGpMNbuXH1a0wubRSWLWSBvwvaq4CiHH/U42CmjOnp6lVNNs/l+Z7ZdDmg== +jss@10.6.0, jss@^10.5.1: + version "10.6.0" + resolved "https://registry.yarnpkg.com/jss/-/jss-10.6.0.tgz#d92ff9d0f214f65ca1718591b68e107be4774149" + integrity sha512-n7SHdCozmxnzYGXBHe0NsO0eUf9TvsHVq2MXvi4JmTn3x5raynodDVE/9VQmBdWFyyj9HpHZ2B4xNZ7MMy7lkw== dependencies: "@babel/runtime" "^7.3.1" csstype "^3.0.2" @@ -9940,9 +10008,9 @@ jsx-ast-utils@^2.0.1: object.assign "^4.1.0" jszip@^3.1.3: - version "3.5.0" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.5.0.tgz#b4fd1f368245346658e781fec9675802489e15f6" - integrity sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA== + version "3.6.0" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.6.0.tgz#839b72812e3f97819cc13ac4134ffced95dd6af9" + integrity sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ== dependencies: lie "~3.3.0" pako "~1.0.2" @@ -10300,9 +10368,9 @@ locate-path@^6.0.0: p-locate "^5.0.0" lodash-es@^4.17.11, lodash-es@^4.17.15, lodash-es@^4.17.20: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.20.tgz#29f6332eefc60e849f869c264bc71126ad61e8f7" - integrity sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA== + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== lodash._reinterpolate@^3.0.0: version "3.0.0" @@ -10364,6 +10432,11 @@ lodash.findindex@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.findindex/-/lodash.findindex-4.6.0.tgz#a3245dee61fb9b6e0624b535125624bb69c11106" integrity sha1-oyRd7mH7m24GJLU1ElYku2nBEQY= +lodash.flatmap@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.flatmap/-/lodash.flatmap-4.5.0.tgz#ef8cbf408f6e48268663345305c6acc0b778702e" + integrity sha1-74y/QI9uSCaGYzRTBcaswLd4cC4= + lodash.flatten@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" @@ -10459,7 +10532,7 @@ lodash.tail@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" integrity sha1-0jM6NtnncXyK0vfKyv7HwytERmQ= -lodash.template@^4.4.0, lodash.template@^4.5.0: +lodash.template@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== @@ -10484,10 +10557,10 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.0.1, lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@~4.17.20: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.0.1, lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@~4.17.20: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== log-symbols@^1.0.2: version "1.0.2" @@ -10504,11 +10577,12 @@ log-symbols@^3.0.0: chalk "^2.4.2" log-symbols@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" - integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== dependencies: - chalk "^4.0.0" + chalk "^4.1.0" + is-unicode-supported "^0.1.0" log-update@^2.3.0: version "2.3.0" @@ -10725,9 +10799,9 @@ mem@^4.0.0: p-is-promise "^2.0.0" memoize-one@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" - integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== memory-fs@^0.4.0, memory-fs@~0.4.1: version "0.4.1" @@ -10797,7 +10871,7 @@ merge@^1.2.0: resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== -methods@^1.1.2, methods@~1.1.2: +methods@^1.1.1, methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= @@ -10846,12 +10920,12 @@ micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8, mic to-regex "^3.0.2" micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + version "4.0.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== dependencies: braces "^3.0.1" - picomatch "^2.0.5" + picomatch "^2.2.3" miller-rabin@^4.0.0: version "4.0.1" @@ -10861,27 +10935,27 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.45.0, "mime-db@>= 1.43.0 < 2": - version "1.45.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" - integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== +mime-db@1.48.0, "mime-db@>= 1.43.0 < 2": + version "1.48.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" + integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ== mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: - version "2.1.28" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd" - integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ== + version "2.1.31" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b" + integrity sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg== dependencies: - mime-db "1.45.0" + mime-db "1.48.0" -mime@1.6.0, mime@^1.5.0: +mime@1.6.0, mime@^1.4.1, mime@^1.5.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== mime@^2.0.3, mime@^2.2.0, mime@^2.3.1, mime@^2.4.4, mime@^2.4.6: - version "2.5.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.0.tgz#2b4af934401779806ee98026bb42e8c1ae1876b1" - integrity sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag== + version "2.5.2" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" + integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== mimic-fn@^1.0.0: version "1.2.0" @@ -10926,7 +11000,7 @@ minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: +minimalistic-crypto-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= @@ -11019,18 +11093,18 @@ mockdate@^2.0.2: integrity sha512-ST0PnThzWKcgSLyc+ugLVql45PvESt3Ul/wrdV/OPc/6Pr8dbLAIJsN1cIp41FLzbN+srVTNIRn+5Cju0nyV6A== moment-timezone@^0.5.14: - version "0.5.32" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.32.tgz#db7677cc3cc680fd30303ebd90b0da1ca0dfecc2" - integrity sha512-Z8QNyuQHQAmWucp8Knmgei8YNo28aLjJq6Ma+jy1ZSpSk5nyfRT8xgUbSQvD2+2UajISfenndwvFuH3NGS+nvA== + version "0.5.33" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.33.tgz#b252fd6bb57f341c9b59a5ab61a8e51a73bbd22c" + integrity sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w== dependencies: moment ">= 2.9.0" -moment@2.14.1, moment@2.x.x, "moment@>= 2.9.0", moment@^2.10.2: +moment@2.14.1: version "2.14.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.14.1.tgz#b35b27c47e57ed2ddc70053d6b07becdb291741c" integrity sha1-s1snxH5X7S3ccAU9awe+zbKRdBw= -moment@^2.27.0: +moment@2.x.x, "moment@>= 2.9.0", moment@^2.10.2, moment@^2.27.0: version "2.29.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== @@ -11234,9 +11308,9 @@ nock@11.9.1: propagate "^2.0.0" node-abort-controller@^1.0.4: - version "1.1.0" - resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-1.1.0.tgz#8a734a631b022af29963be7245c1483cbb9e070d" - integrity sha512-dEYmUqjtbivotqjraOe8UvhT/poFfog1BQRNsZm/MSEDDESk2cQ1tvD8kGyuN07TM/zoW+n42odL8zTeJupYdQ== + version "1.2.1" + resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-1.2.1.tgz#1eddb57eb8fea734198b11b28857596dc6165708" + integrity sha512-79PYeJuj6S9+yOHirR0JBLFOgjB6sQCir10uN6xRx25iD+ZD4ULqgRn3MwWBRaQGB0vEgReJzWwJo42T1R6YbQ== node-fetch@^1.0.1: version "1.7.3" @@ -11296,9 +11370,9 @@ node-libs-browser@^2.0.0: vm-browserify "^1.0.1" node-notifier@^5.2.1: - version "5.4.3" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.3.tgz#cb72daf94c93904098e28b9c590fd866e464bd50" - integrity sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q== + version "5.4.5" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.5.tgz#0cbc1a2b0f658493b4025775a13ad938e96091ef" + integrity sha512-tVbHs7DyTLtzOiN78izLA85zRqB9NvEXkAf014Vx3jtSvn/xBl6bR8ZYifj+dFcFrKI21huSQgJZ6ZtL3B4HfQ== dependencies: growly "^1.3.0" is-wsl "^1.1.0" @@ -11338,10 +11412,10 @@ node-pre-gyp@^0.11.0: semver "^5.3.0" tar "^4" -node-releases@^1.1.3, node-releases@^1.1.69: - version "1.1.69" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.69.tgz#3149dbde53b781610cd8b486d62d86e26c3725f6" - integrity sha512-DGIjo79VDEyAnRlfSqYTsy+yoHd2IOjJiKUozD2MV2D85Vso6Bug56mb9tT/fY5Urt0iqk01H7x+llAruDR2zA== +node-releases@^1.1.3, node-releases@^1.1.71: + version "1.1.73" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20" + integrity sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg== nodemailer@^4.3.1: version "4.7.0" @@ -11412,14 +11486,14 @@ normalize-url@^3.0.0: integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== normalize-url@^4.1.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" - integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== + version "4.5.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" + integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== npm-bundled@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" - integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA== + version "1.1.2" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.2.tgz#944c78789bd739035b70baa2ca5cc32b8d860bc1" + integrity sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ== dependencies: npm-normalize-package-bin "^1.0.1" @@ -11526,17 +11600,17 @@ object-hash@^1.1.4: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== -object-inspect@^1.7.0, object-inspect@^1.8.0, object-inspect@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" - integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== +object-inspect@^1.10.3, object-inspect@^1.7.0, object-inspect@^1.9.0: + version "1.10.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369" + integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw== object-is@^1.0.1, object-is@^1.0.2, object-is@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.4.tgz#63d6c83c00a43f4cbc9434eb9757c8a5b8565068" - integrity sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg== + version "1.1.5" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" + integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" object-keys@^1.0.12, object-keys@^1.1.1: @@ -11551,7 +11625,7 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@^4.1.0, object.assign@^4.1.1, object.assign@^4.1.2: +object.assign@^4.1.0, object.assign@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== @@ -11572,33 +11646,32 @@ object.defaults@^1.1.0: isobject "^3.0.0" object.entries@^1.1.1, object.entries@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.3.tgz#c601c7f168b62374541a07ddbd3e2d5e4f7711a6" - integrity sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg== + version "1.1.4" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.4.tgz#43ccf9a50bc5fd5b649d45ab1a579f24e088cafd" + integrity sha512-h4LWKWE+wKQGhtMjZEBud7uLGhqyLwj8fpHOarZhD2uY3C9cRtk57VQ89ke3moByLXMedqs3XCHzyb4AmA2DjA== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" - has "^1.0.3" + es-abstract "^1.18.2" object.fromentries@^2.0.0, object.fromentries@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.3.tgz#13cefcffa702dc67750314a3305e8cb3fad1d072" - integrity sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw== + version "2.0.4" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.4.tgz#26e1ba5c4571c5c6f0890cef4473066456a120b8" + integrity sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" + es-abstract "^1.18.0-next.2" has "^1.0.3" object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0, object.getownpropertydescriptors@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz#0dfda8d108074d9c563e80490c883b6661091544" - integrity sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng== + version "2.1.2" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz#1bd63aeacf0d5d2d2f31b5e393b03a7c601a23f7" + integrity sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" + es-abstract "^1.18.0-next.2" object.map@^1.0.0: version "1.0.1" @@ -11624,14 +11697,13 @@ object.pick@^1.2.0, object.pick@^1.3.0: isobject "^3.0.1" object.values@^1.1.0, object.values@^1.1.1, object.values@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.2.tgz#7a2015e06fcb0f546bd652486ce8583a4731c731" - integrity sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag== + version "1.1.4" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30" + integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" - has "^1.0.3" + es-abstract "^1.18.2" obuf@^1.0.0, obuf@^1.1.2: version "1.1.2" @@ -11688,13 +11760,20 @@ opencollective-postinstall@^2.0.2: resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q== -opn@5.4.0, opn@^5.1.0: +opn@5.4.0: version "5.4.0" resolved "https://registry.yarnpkg.com/opn/-/opn-5.4.0.tgz#cb545e7aab78562beb11aa3bfabc7042e1761035" integrity sha512-YF9MNdVy/0qvJvDtunAOzFw9iasOQHpVthTCvGzxt61Il64AYSGdK+rYwld7NAfk9qJ7dt+hymBNSc9LNYS+Sw== dependencies: is-wsl "^1.1.0" +opn@^5.1.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" + integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== + dependencies: + is-wsl "^1.1.0" + optimism@^0.10.0: version "0.10.3" resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.10.3.tgz#163268fdc741dea2fb50f300bedda80356445fd7" @@ -11931,9 +12010,9 @@ pako@~1.0.2, pako@~1.0.5: integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== papaparse@^5.1.1: - version "5.3.0" - resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.3.0.tgz#ab1702feb96e79ab4309652f36db9536563ad05a" - integrity sha512-Lb7jN/4bTpiuGPrYy4tkKoUS8sTki8zacB5ke1p5zolhcSE4TlWgrlsxjrDTbG/dFVh07ck7X36hUf/b5V68pg== + version "5.3.1" + resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.3.1.tgz#770b7a9124d821d4b2132132b7bd7dce7194b5b1" + integrity sha512-Dbt2yjLJrCwH2sRqKFFJaN5XgIASO9YOFeFP8rIBRG2Ain8mqk5r1M6DkfvqEVozVcz3r3HaUGw253hA1nLIcA== parallel-transform@^1.1.0: version "1.2.0" @@ -12004,9 +12083,9 @@ parse-json@^4.0.0: json-parse-better-errors "^1.0.1" parse-json@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.1.0.tgz#f96088cdf24a8faa9aea9a009f2d9d942c999646" - integrity sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ== + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== dependencies: "@babel/code-frame" "^7.0.0" error-ex "^1.3.1" @@ -12018,7 +12097,7 @@ parse-passwd@^1.0.0: resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= -parse5-htmlparser2-tree-adapter@^6.0.0: +parse5-htmlparser2-tree-adapter@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== @@ -12030,7 +12109,7 @@ parse5@4.0.0: resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== -parse5@^6.0.0, parse5@^6.0.1: +parse5@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== @@ -12159,9 +12238,9 @@ path-key@^3.0.0, path-key@^3.1.0: integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-parse@^1.0.5, path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-proxy@~1.0.0: version "1.0.0" @@ -12221,9 +12300,9 @@ pause@0.0.1: integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10= pbkdf2@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94" - integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg== + version "3.1.2" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" + integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== dependencies: create-hash "^1.1.2" create-hmac "^1.1.4" @@ -12251,10 +12330,10 @@ pg-connection-string@2.1.0: resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.1.0.tgz#e07258f280476540b24818ebb5dca29e101ca502" integrity sha512-bhlV7Eq09JrRIvo1eKngpwuqKtJnNhZdpdOlvrPrA4dxqXPjxSrbNrfnIDmTpwMyRszrcV4kU5ZA4mMsQUrjdg== -pg-connection-string@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.4.0.tgz#c979922eb47832999a204da5dbe1ebf2341b6a10" - integrity sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ== +pg-connection-string@^2.4.0, pg-connection-string@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" + integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== pg-cursor@1.3.0: version "1.3.0" @@ -12266,15 +12345,15 @@ pg-int8@1.0.1: resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== -pg-pool@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.2.2.tgz#a560e433443ed4ad946b84d774b3f22452694dff" - integrity sha512-ORJoFxAlmmros8igi608iVEbQNNZlp89diFVx6yV5v+ehmpMY9sK6QgpmgoXbmkNaBAx8cOOZh9g80kJv1ooyA== +pg-pool@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.3.0.tgz#12d5c7f65ea18a6e99ca9811bd18129071e562fc" + integrity sha512-0O5huCql8/D6PIRFAlmccjphLYWC+JIzvUhSzXSpGaf+tjTZc4nn+Lr7mLXBbFJfvwbP0ywDv73EiaBsxn7zdg== -pg-protocol@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.4.0.tgz#43a71a92f6fe3ac559952555aa3335c8cb4908be" - integrity sha512-El+aXWcwG/8wuFICMQjM5ZSAm6OWiJicFdNYo+VY3QP+8vI4SvLIWVe51PppTzMhikUJR+PsyIFKqfdXPz/yxA== +pg-protocol@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.5.0.tgz#b5dd452257314565e2d54ab3c132adc46565a6a0" + integrity sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ== pg-query-stream@^1.1.1: version "1.1.2" @@ -12295,15 +12374,15 @@ pg-types@^2.1.0: postgres-interval "^1.1.0" pg@^8.0.3: - version "8.5.1" - resolved "https://registry.yarnpkg.com/pg/-/pg-8.5.1.tgz#34dcb15f6db4a29c702bf5031ef2e1e25a06a120" - integrity sha512-9wm3yX9lCfjvA98ybCyw2pADUivyNWT/yIP4ZcDVpMN0og70BUWYEGXPCTAQdGTAqnytfRADb7NERrY1qxhIqw== + version "8.6.0" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.6.0.tgz#e222296b0b079b280cce106ea991703335487db2" + integrity sha512-qNS9u61lqljTDFvmk/N66EeGq3n6Ujzj0FFyNMGQr6XuEv4tgNTXvJQTfJdcvGit5p5/DWPu+wj920hAJFI+QQ== dependencies: buffer-writer "2.0.0" packet-reader "1.0.0" - pg-connection-string "^2.4.0" - pg-pool "^3.2.2" - pg-protocol "^1.4.0" + pg-connection-string "^2.5.0" + pg-pool "^3.3.0" + pg-protocol "^1.5.0" pg-types "^2.1.0" pgpass "1.x" @@ -12314,10 +12393,10 @@ pgpass@1.x: dependencies: split2 "^3.1.1" -picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" + integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== pify@^2.0.0, pify@^2.2.0: version "2.3.0" @@ -12638,11 +12717,10 @@ postcss-image-set-function@^3.0.1: postcss-values-parser "^2.0.0" postcss-initial@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-3.0.2.tgz#f018563694b3c16ae8eaabe3c585ac6319637b2d" - integrity sha512-ugA2wKonC0xeNHgirR4D3VWHs2JcU08WAi1KFLVcnb7IN89phID6Qtg2RIctWbnvp1TM2BOmDtX8GGLCKdR8YA== + version "3.0.4" + resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-3.0.4.tgz#9d32069a10531fe2ecafa0b6ac750ee0bc7efc53" + integrity sha512-3RLn6DIpMsK1l5UUy9jxQvoDeUN4gP939tDcKUHD/kM8SGSKbFAnvkpFpj3Bhtz3HGk1jWY5ZNWX6mPta5M9fg== dependencies: - lodash.template "^4.5.0" postcss "^7.0.2" postcss-lab-function@^2.0.1: @@ -13018,21 +13096,18 @@ postcss-selector-parser@^5.0.0-rc.3, postcss-selector-parser@^5.0.0-rc.4: uniq "^1.0.1" postcss-selector-parser@^6.0.2: - version "6.0.4" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" - integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== + version "6.0.6" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz#2c5bba8174ac2f6981ab631a42ab0ee54af332ea" + integrity sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg== dependencies: cssesc "^3.0.0" - indexes-of "^1.0.1" - uniq "^1.0.1" util-deprecate "^1.0.2" -postcss-svgo@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258" - integrity sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw== +postcss-svgo@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.3.tgz#343a2cdbac9505d416243d496f724f38894c941e" + integrity sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw== dependencies: - is-svg "^3.0.0" postcss "^7.0.0" postcss-value-parser "^3.0.0" svgo "^1.0.0" @@ -13075,9 +13150,9 @@ postcss@^6.0.1, postcss@^6.0.23: supports-color "^5.4.0" postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.35" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" - integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== + version "7.0.36" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.36.tgz#056f8cffa939662a8f5905950c07d5285644dfcb" + integrity sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw== dependencies: chalk "^2.4.2" source-map "^0.6.1" @@ -13131,9 +13206,9 @@ pretty-bytes@^4.0.2: integrity sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk= pretty-bytes@^5.4.1: - version "5.5.0" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.5.0.tgz#0cecda50a74a941589498011cf23275aa82b339e" - integrity sha512-p+T744ZyjjiaFlMUZZv6YPC5JrkNj8maRmPaQCWFJFplUAzpIUTRaTcS+7wmZtUoFXHtESJb23ISliaWyz3SHA== + version "5.6.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== pretty-error@^2.0.2: version "2.1.2" @@ -13233,17 +13308,17 @@ propagate@^2.0.0: resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== -property-expr@^2.0.2, property-expr@^2.0.4: +property-expr@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.4.tgz#37b925478e58965031bb612ec5b3260f8241e910" integrity sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg== proxy-addr@~2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" - integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== dependencies: - forwarded "~0.1.2" + forwarded "0.2.0" ipaddr.js "1.9.1" proxy-agent@~3.0.0: @@ -13363,10 +13438,12 @@ qs@6.7.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== -qs@^6.5.2, qs@^6.7.0, qs@^6.9.4: - version "6.9.6" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee" - integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ== +qs@^6.5.1, qs@^6.5.2, qs@^6.7.0, qs@^6.9.4: + version "6.10.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" + integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg== + dependencies: + side-channel "^1.0.4" qs@~6.5.2: version "6.5.2" @@ -13396,6 +13473,11 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== +queue-microtask@^1.2.2, queue-microtask@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + quick-lru@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" @@ -13522,9 +13604,9 @@ react-async-script@^0.6.0: integrity sha1-s3UT1i8lp/xxtaRYbDicOPGQSFM= react-chartjs-2@^2.11.1: - version "2.11.1" - resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-2.11.1.tgz#a78d0df05fc8bc8ffcd4c4ab5b89a25dd2ca3278" - integrity sha512-G7cNq/n2Bkh/v4vcI+GKx7Q1xwZexKYhOSj2HmrFXlvNeaURWXun6KlOUpEQwi1cv9Tgs4H3kGywDWMrX2kxfA== + version "2.11.2" + resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-2.11.2.tgz#156c0d2618600561efc23bef278bd48a335cadb6" + integrity sha512-hcPS9vmRJeAALPPf0uo02BiD8BDm0HNmneJYTZVR74UKprXOpql+Jy1rVuj93rKw0Jfx77mkcRfXPxTe5K83uw== dependencies: lodash "^4.17.19" prop-types "^15.7.2" @@ -13651,9 +13733,9 @@ react-is@^16.13.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1, react- integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== "react-is@^16.8.0 || ^17.0.0": - version "17.0.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" - integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4: version "3.0.4" @@ -13760,16 +13842,16 @@ react-test-renderer@^16.0.0-0: scheduler "^0.19.1" react-to-print@^2.8.0: - version "2.12.3" - resolved "https://registry.yarnpkg.com/react-to-print/-/react-to-print-2.12.3.tgz#1468f2c7ee92c097a80da29150352cd72c8906f2" - integrity sha512-JpjPh2WDo2nraxgyt5p3kHMMZZDs52uDcgJJi7BbgmE/j3v1RcTtr3s3uvDk82WRjLWggDm1Ro2J9/L9qkH4cQ== + version "2.12.6" + resolved "https://registry.yarnpkg.com/react-to-print/-/react-to-print-2.12.6.tgz#ac4537e12528ce865a1a652dcdfb19c5b20c9cce" + integrity sha512-O4hpQZX8pOd3W910n+WkT9Jvpd3tFcEWqTFHrm69bCGI/Nh5g2CoLrzaXObQW9BA9mK1K4+3qG88Q6DAJs+oWg== dependencies: prop-types "^15.7.2" react-tooltip@^4.2.13: - version "4.2.13" - resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-4.2.13.tgz#908db8a41dc10ae2ae9cc1864746cde939aaab0f" - integrity sha512-iAZ02wSxChLWb7Vnu0zeQMyAo/jiGHrwFNILWaR3pCKaFVRjKcv/B6TBI4+Xd66xLXVzLngwJ91Tf5D+mqAqVA== + version "4.2.21" + resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-4.2.21.tgz#840123ed86cf33d50ddde8ec8813b2960bfded7f" + integrity sha512-zSLprMymBDowknr0KVDiJ05IjZn9mQhhg4PRsqln0OZtURAJ1snt1xi5daZfagsh6vfsziZrc9pErPTDY1ACig== dependencies: prop-types "^15.7.2" uuid "^7.0.3" @@ -13786,9 +13868,9 @@ react-transition-group@^1.2.0: warning "^3.0.0" react-transition-group@^4.0.0, react-transition-group@^4.4.0: - version "4.4.1" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" - integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw== + version "4.4.2" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" + integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg== dependencies: "@babel/runtime" "^7.5.5" dom-helpers "^5.0.1" @@ -13857,7 +13939,7 @@ read-pkg@^2.0.0: normalize-package-data "^2.3.2" path-type "^2.0.0" -"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -13880,7 +13962,7 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^3.0.0, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.6.0: +readable-stream@^3.0.0, readable-stream@^3.0.6, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -13898,10 +13980,10 @@ readdirp@^2.0.0, readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" -readdirp@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== dependencies: picomatch "^2.2.1" @@ -13956,9 +14038,9 @@ redent@^1.0.0: strip-indent "^1.0.1" redis-commands@^1.1.0, redis-commands@^1.2.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.6.0.tgz#36d4ca42ae9ed29815cdb30ad9f97982eba1ce23" - integrity sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ== + version "1.7.0" + resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" + integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ== redis-parser@^1.2.0: version "1.3.0" @@ -13989,12 +14071,11 @@ redis@^2.8.0: redis-parser "^2.6.0" redux@^4.0.4: - version "4.0.5" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" - integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== + version "4.1.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4" + integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g== dependencies: - loose-envify "^1.4.0" - symbol-observable "^1.2.0" + "@babel/runtime" "^7.9.2" reflect.ownkeys@^0.2.0: version "0.2.0" @@ -14065,12 +14146,12 @@ regex-not@^1.0.0, regex-not@^1.0.2: safe-regex "^1.1.0" regexp.prototype.flags@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" - integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== + version "1.3.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" + integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" regexpp@^2.0.1: version "2.0.1" @@ -14130,9 +14211,9 @@ regjsparser@^0.1.4: jsesc "~0.5.0" regjsparser@^0.6.4: - version "0.6.6" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.6.tgz#6d8c939d1a654f78859b08ddcc4aa777f3fa800a" - integrity sha512-jjyuCp+IEMIm3N1H1LLTJW1EISEJV9+5oHdEyrt43Pg9cDSb6rrLZei2cVWpl0xTjmmlpec/lEQGYgM7xfpGCQ== + version "0.6.9" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.9.tgz#b489eef7c9a2ce43727627011429cf833a7183e6" + integrity sha512-ZqbNRz1SNjLAiYuwY0zoXW8Ne675IX5q+YHioAGbCw4X96Mjl2+dcX9B2ciaeyYjViDAfvIjFpQjJgLttTEERQ== dependencies: jsesc "~0.5.0" @@ -14147,20 +14228,20 @@ remove-trailing-separator@^1.0.1: integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= renderkid@^2.0.4: - version "2.0.5" - resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.5.tgz#483b1ac59c6601ab30a7a596a5965cabccfdd0a5" - integrity sha512-ccqoLg+HLOHq1vdfYNm4TBeaCDIi1FLt3wGojTDSvdewUv65oTmI3cnT2E4hRjl1gzKZIPK+KZrXzlUYKnR+vQ== + version "2.0.7" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.7.tgz#464f276a6bdcee606f4a15993f9b29fc74ca8609" + integrity sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ== dependencies: - css-select "^2.0.2" - dom-converter "^0.2" - htmlparser2 "^3.10.1" - lodash "^4.17.20" - strip-ansi "^3.0.0" + css-select "^4.1.3" + dom-converter "^0.2.0" + htmlparser2 "^6.1.0" + lodash "^4.17.21" + strip-ansi "^3.0.1" repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + version "1.1.4" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" + integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== repeat-string@^1.5.2, repeat-string@^1.6.1: version "1.6.1" @@ -14300,12 +14381,12 @@ resolve@1.10.0: dependencies: path-parse "^1.0.6" -resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.13.1, resolve@^1.3.2, resolve@^1.6.0, resolve@^1.8.1, resolve@^1.9.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" - integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.3.2, resolve@^1.6.0, resolve@^1.8.1, resolve@^1.9.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== dependencies: - is-core-module "^2.1.0" + is-core-module "^2.2.0" path-parse "^1.0.6" responselike@^1.0.2: @@ -14412,9 +14493,9 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: inherits "^2.0.1" rollbar@^2.4.4: - version "2.19.4" - resolved "https://registry.yarnpkg.com/rollbar/-/rollbar-2.19.4.tgz#eda4145cb459203bd3e9ccd885bfb5c5c3f6f2a2" - integrity sha512-8ErcMfJE2zkhgrFtpGRyejHBhyO0hU7dZ3EvG2ylNZ7qSu4yw5PZfhGx+Qyq3ksKshLFeQh9Y/Bh2S1TOPIhvw== + version "2.22.0" + resolved "https://registry.yarnpkg.com/rollbar/-/rollbar-2.22.0.tgz#0115f29c762b7ea14bc3b7846f4b376d99eff9e6" + integrity sha512-fMTKtzSnoE2ODnMAqzH/Soocb/o7Qy7U5tdRjzljKoz1vMRKkMYbW7E0s0Qraoo3xaTzd5UxDNP3c8jmrlWgug== dependencies: async "~1.2.1" console-polyfill "0.3.0" @@ -14458,9 +14539,11 @@ run-async@^2.2.0: integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== run-parallel@^1.1.9: - version "1.1.10" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.10.tgz#60a51b2ae836636c81377df16cb107351bcd13ef" - integrity sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw== + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" run-queue@^1.0.0, run-queue@^1.0.3: version "1.0.3" @@ -14480,9 +14563,9 @@ rx@^4.1.0: integrity sha1-pfE/957zt0D+MKqAP7CfmIBdR4I= rxjs@^6.1.0, rxjs@^6.3.3, rxjs@^6.4.0: - version "6.6.3" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" - integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== dependencies: tslib "^1.9.0" @@ -14602,9 +14685,9 @@ selenium-webdriver@^3.6.0: xml2js "^0.4.17" selfsigned@^1.9.1: - version "1.10.8" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.8.tgz#0d17208b7d12c33f8eac85c41835f27fc3d81a30" - integrity sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w== + version "1.10.11" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.11.tgz#24929cd906fe0f44b6d01fb23999a739537acbe9" + integrity sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA== dependencies: node-forge "^0.10.0" @@ -14635,15 +14718,15 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.3.2: - version "7.3.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" - integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== +semver@^7.3.2, semver@^7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== dependencies: lru-cache "^6.0.0" @@ -14817,6 +14900,15 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -14978,9 +15070,9 @@ source-map-support@^0.5.1, source-map-support@^0.5.6, source-map-support@~0.5.10 source-map "^0.6.0" source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + version "0.4.1" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" + integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1: version "0.5.7" @@ -15019,9 +15111,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.7" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65" - integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ== + version "3.0.9" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz#8a595135def9592bda69709474f1cbeea7c2467f" + integrity sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ== spdy-transport@^3.0.0: version "3.0.0" @@ -15101,9 +15193,9 @@ ssri@^5.2.4: safe-buffer "^5.1.1" ssri@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" - integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== + version "6.0.2" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5" + integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q== dependencies: figgy-pudding "^3.5.1" @@ -15113,9 +15205,9 @@ stable@^0.1.8: integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== stack-utils@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.4.tgz#4b600971dcfc6aed0cbdf2a8268177cc916c87c8" - integrity sha512-IPDJfugEGbfizBwBZRZ3xpccMdRyP5lqsBWXGQWimVjua/ccLCeMOAVjlc1R7LxFjo5sEDhyNIXd8mo/AiDS9w== + version "1.0.5" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.5.tgz#a19b0b01947e0029c8e451d5d61a498f5bb1471b" + integrity sha512-KZiTzuV3CnSnSvgMRrARVCj+Ht7rMbauGDK0LdVFRGyenwdylpajAp4Q0i6SX8rEmbTpMMf6ryq2gb8pPq2WgQ== dependencies: escape-string-regexp "^2.0.0" @@ -15174,6 +15266,11 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== +streamifier@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/streamifier/-/streamifier-0.1.1.tgz#97e98d8fa4d105d62a2691d1dc07e820db8dfc4f" + integrity sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8= + strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" @@ -15224,30 +15321,22 @@ string-width@^3.0.0: strip-ansi "^5.1.0" string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + version "4.2.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" + integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== dependencies: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" string.prototype.trim@^1.2.1: - version "1.2.3" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.3.tgz#d23a22fde01c1e6571a7fadcb9be11decd8061a7" - integrity sha512-16IL9pIBA5asNOSukPfxX2W68BaBvxyiRK16H3RA/lWW9BDosh+w7f+LhomPHpXJ82QEe7w7/rY/S1CV97raLg== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" - -string.prototype.trimend@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz#a22bd53cca5c7cf44d7c9d5c732118873d6cd18b" - integrity sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw== + version "1.2.4" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.4.tgz#6014689baf5efaf106ad031a5fa45157666ed1bd" + integrity sha512-hWCk/iqf7lp0/AgTF7/ddO1IWtSNPASjlzCicV5irAVdE1grjsneK26YG6xACMBEdCvO8fUST0UzDMh/2Qy+9Q== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" + es-abstract "^1.18.0-next.2" string.prototype.trimend@^1.0.4: version "1.0.4" @@ -15257,14 +15346,6 @@ string.prototype.trimend@^1.0.4: call-bind "^1.0.2" define-properties "^1.1.3" -string.prototype.trimstart@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz#9b4cb590e123bb36564401d59824298de50fd5aa" - integrity sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - string.prototype.trimstart@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" @@ -15400,6 +15481,22 @@ stylehacks@^4.0.0: postcss "^7.0.0" postcss-selector-parser "^3.0.0" +superagent@^3.7.0: + version "3.8.3" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128" + integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA== + dependencies: + component-emitter "^1.2.0" + cookiejar "^2.1.0" + debug "^3.1.0" + extend "^3.0.0" + form-data "^2.3.1" + formidable "^1.2.0" + methods "^1.1.1" + mime "^1.4.1" + qs "^6.5.1" + readable-stream "^2.3.5" + superagent@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/superagent/-/superagent-5.3.1.tgz#d62f3234d76b8138c1320e90fa83dc1850ccabf1" @@ -15481,7 +15578,7 @@ svgo@^1.0.0, svgo@^1.2.2: unquote "~1.1.1" util.promisify "~1.0.0" -symbol-observable@1.2.0, symbol-observable@^1.0.2, symbol-observable@^1.0.4, symbol-observable@^1.1.0, symbol-observable@^1.2.0: +symbol-observable@1.2.0, symbol-observable@^1.0.2, symbol-observable@^1.0.4, symbol-observable@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== @@ -15681,18 +15778,18 @@ timers-browserify@^2.0.4: setimmediate "^1.0.4" timezonecomplete@^5.5.0: - version "5.11.2" - resolved "https://registry.yarnpkg.com/timezonecomplete/-/timezonecomplete-5.11.2.tgz#f4eae5bad22d309370e28a5bdb839aa9434f2dca" - integrity sha512-FdQO0u8uJ/cq4/+YR4GxA2zxA7D/dKefSWfRIW+eTdxzSbdrRAK2v6fP1Fw3qt3+F4HIo83gb09aabJAAw4ERw== + version "5.12.4" + resolved "https://registry.yarnpkg.com/timezonecomplete/-/timezonecomplete-5.12.4.tgz#5508107540ccfe32c7cbed97d7d0433a9d241e71" + integrity sha512-K+ocagBAl5wu9Ifh5oHKhRRLb0wP7j0VjAzjboZsT6bnVmtJNRe3Wnk2IPp0C4Uc8HpLly3gbfUrTlJ3M7vCPA== dependencies: - tzdata "^1.0.11" + tzdata "^1.0.25" timsort@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= -tiny-warning@^1.0.2: +tiny-warning@^1.0.2, tiny-warning@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== @@ -15786,14 +15883,14 @@ toidentifier@1.0.0: integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== topeka@^4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/topeka/-/topeka-4.4.1.tgz#7d4590671b7983abe9d0b8a63d8d792a965839e3" - integrity sha512-70ebgXUbADxJH9X9rcKfqKUYa33EkiCDVoKwqe22oahYup5lbuzl7/fn9m6zp9OIhOFHVbyLRjU6CXbXpwWPAw== + version "4.4.3" + resolved "https://registry.yarnpkg.com/topeka/-/topeka-4.4.3.tgz#fcf90401f14e4cf3ae84014bc6078420cba49e39" + integrity sha512-9w1hgZsznbAbk3AOZ+bhgoiPeFLAhDh5bhqMOhjkcdYjHAsxypucKfursHj0bFzAuVu/wAJdjG6dsswrnRTJ4Q== dependencies: - "@babel/runtime" "^7.8.7" - invariant "^2.2.4" - property-expr "^2.0.2" - uncontrollable "^7.1.1" + "@restart/hooks" "^0.3.26" + property-expr "^2.0.4" + queue-microtask "^1.2.3" + uncontrollable "^7.2.1" topo@2.x.x: version "2.0.2" @@ -15866,20 +15963,20 @@ tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" - integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== +tslib@^2.0.1, tslib@^2.1.0, tslib@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" + integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== tsscmp@1.0.6, tsscmp@~1.0.0: version "1.0.6" resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA== -tsutils@^3.17.1: - version "3.20.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.20.0.tgz#ea03ea45462e146b53d70ce0893de453ff24f698" - integrity sha512-RYbuQuvkhuqVeXweWT3tJLKOEJ/UUw9GjNEZGWdrLLlM+611o1gwLHBpxoFJKKl25fLprp2eVthtKs5JOrNeXg== +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== dependencies: tslib "^1.8.1" @@ -15901,19 +15998,20 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= twilio@^3.40.0: - version "3.55.0" - resolved "https://registry.yarnpkg.com/twilio/-/twilio-3.55.0.tgz#26cdedb38c5da1f1e280dd39ce2be42ddf69427a" - integrity sha512-CQpe4Hv1Bd7miaLXMlWmN28eVO3NOzPDokBXIVTfPFOUhx+RPsmvxrn3MVEYgNLUFbKwps7LKib0DWm3i0sbVA== + version "3.63.1" + resolved "https://registry.yarnpkg.com/twilio/-/twilio-3.63.1.tgz#20773c2ba66bb843a0174830a848c7f6cbfd577f" + integrity sha512-xwtOM78sO2jGxKg1AW+7XlJdrhTMW9dzr6665O+IB/VtNVQB7JQS48pLCZFnBaTvZOILVO0Q6t63wv24hIbr/A== dependencies: axios "^0.21.1" dayjs "^1.8.29" + https-proxy-agent "^5.0.0" jsonwebtoken "^8.5.1" lodash "^4.17.19" q "2.0.x" qs "^6.9.4" rootpath "^0.1.2" scmp "^2.1.0" - url-parse "^1.4.7" + url-parse "^1.5.0" xmlbuilder "^13.0.2" type-check@~0.3.2: @@ -15942,9 +16040,9 @@ type@^1.0.1: integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== type@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/type/-/type-2.1.0.tgz#9bdc22c648cf8cf86dd23d32336a41cfb6475e3f" - integrity sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA== + version "2.5.0" + resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d" + integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw== typedarray-to-buffer@^3.1.5: version "3.1.5" @@ -15958,15 +16056,15 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -tzdata@^1.0.11: - version "1.0.24" - resolved "https://registry.yarnpkg.com/tzdata/-/tzdata-1.0.24.tgz#48be23dabbbce947d786c1c2bc89638a796c19fc" - integrity sha512-ApiPigp1V5wEqwwaFPaUPUYatcR3L6+oRBECRKJzpsW+9fiQhBfrZSV4f9t7yuaH4PrcNt97aqd5Io18cfufWA== +tzdata@^1.0.25: + version "1.0.25" + resolved "https://registry.yarnpkg.com/tzdata/-/tzdata-1.0.25.tgz#e8839033c05761e04ef552242e779777becb13d0" + integrity sha512-yAZ/Tv/tBFIPHJGYrOexxW8Swyjszt7rDhIjnIPSqLaP8mzrr3T7D0w4cxQBtToXnQrlFqkEU0stGC/auz0JcQ== ua-parser-js@^0.7.18: - version "0.7.23" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.23.tgz#704d67f951e13195fbcd3d78818577f5bc1d547b" - integrity sha512-m4hvMLxgGHXG3O3fQVAyyAQpZzDOvwnhOTjYz5Xmr7r/+LpkNy3vJXdVRWgd1TkAb7NGROZuSy96CrlNVjA7KA== + version "0.7.28" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" + integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g== uglify-es@^3.3.4: version "3.3.9" @@ -15995,9 +16093,9 @@ uglify-js@^2.8.29: uglify-to-browserify "~1.0.0" uglify-js@^3.1.4: - version "3.12.4" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.12.4.tgz#93de48bb76bb3ec0fc36563f871ba46e2ee5c7ee" - integrity sha512-L5i5jg/SHkEqzN18gQMTWsZk3KelRsfD1wUVNqtq0kzqWQqcJjyL8yc1o8hJgRrWqrAl2mUFbhfznEIoi7zi2A== + version "3.13.9" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.9.tgz#4d8d21dcd497f29cfd8e9378b9df123ad025999b" + integrity sha512-wZbyTQ1w6Y7fHdt8sJnHfSIuWeDgk6B5rCb4E/AM6QNNPbOMIZph21PW5dRB3h7Df0GszN+t7RuUH6sWK5bF0g== uglify-to-browserify@~1.0.0: version "1.0.2" @@ -16032,7 +16130,7 @@ uid2@0.0.x: resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82" integrity sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I= -unbox-primitive@^1.0.0: +unbox-primitive@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== @@ -16047,7 +16145,7 @@ unc-path-regex@^0.1.2: resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= -uncontrollable@^7.1.1: +uncontrollable@^7.1.1, uncontrollable@^7.2.1: version "7.2.1" resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-7.2.1.tgz#1fa70ba0c57a14d5f78905d533cf63916dc75738" integrity sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ== @@ -16138,11 +16236,6 @@ universalify@^0.1.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== -universalify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" - integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== - universalify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" @@ -16233,10 +16326,10 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url-parse@^1.1.8, url-parse@^1.4.3, url-parse@^1.4.7: - version "1.4.7" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" - integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== +url-parse@^1.1.8, url-parse@^1.4.3, url-parse@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.1.tgz#d5fa9890af8a5e1f274a2c98376510f6425f6e3b" + integrity sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q== dependencies: querystringify "^2.1.1" requires-port "^1.0.0" @@ -16715,9 +16808,9 @@ whatwg-fetch@3.0.0: integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q== whatwg-fetch@>=0.10.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.5.0.tgz#605a2cd0a7146e5db141e29d1c62ab84c0c4c868" - integrity sha512-jXkLtsR42xhXg7akoDKvKWE40eJeI+2KZqcp2h3NsOrRnDvtWX36KcKl30dy+hxECivdk2BVUHVNrPtoMBUx6A== + version "3.6.2" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" + integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0: version "2.3.0" @@ -17008,9 +17101,9 @@ write@^0.2.1: mkdirp "^0.5.1" ws@^5.2.0: - version "5.2.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" - integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA== + version "5.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.3.tgz#05541053414921bc29c63bee14b8b0dd50b07b3d" + integrity sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA== dependencies: async-limiter "~1.0.0" @@ -17032,7 +17125,7 @@ xml2js@0.4.19: sax ">=0.6.0" xmlbuilder "~9.0.1" -xml2js@^0.4.17: +xml2js@^0.4.17, xml2js@^0.4.4: version "0.4.23" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== @@ -17076,9 +17169,9 @@ y18n@^3.2.1: integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ== "y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" - integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== yallist@^2.1.2: version "2.1.2" @@ -17096,9 +17189,9 @@ yallist@^4.0.0: integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yaml@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" - integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== yargs-parser@^10.1.0: version "10.1.0" @@ -17239,14 +17332,14 @@ yup@0.32.3: toposort "^2.0.2" yup@>=0.32.3: - version "0.32.8" - resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.8.tgz#16e4a949a86a69505abf99fd0941305ac9adfc39" - integrity sha512-SZulv5FIZ9d5H99EN5tRCRPXL0eyoYxWIP1AacCrjC9d4DfP13J1dROdKGfpfRHT3eQB6/ikBl5jG21smAfCkA== + version "0.32.9" + resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.9.tgz#9367bec6b1b0e39211ecbca598702e106019d872" + integrity sha512-Ci1qN+i2H0XpY7syDQ0k5zKQ/DoxO0LzPg8PAR/X4Mpj6DqaeCoIYEEjDJwhArh3Fa7GWbQQVDZKeXYlSH4JMg== dependencies: "@babel/runtime" "^7.10.5" "@types/lodash" "^4.14.165" lodash "^4.17.20" - lodash-es "^4.17.11" + lodash-es "^4.17.15" nanoclone "^0.2.1" property-expr "^2.0.4" toposort "^2.0.2" From a5eb836f07cbd1fc85902af03d493db0a00c1d68 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Tue, 15 Jun 2021 22:47:15 -0400 Subject: [PATCH 096/191] bandwidth fixes: now can send a real message --- .../service-vendors/bandwidth/messaging.js | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/src/extensions/service-vendors/bandwidth/messaging.js b/src/extensions/service-vendors/bandwidth/messaging.js index dbf2c7a1c..553add300 100644 --- a/src/extensions/service-vendors/bandwidth/messaging.js +++ b/src/extensions/service-vendors/bandwidth/messaging.js @@ -1,4 +1,4 @@ -import BandwidthMessaging from "@bandwidth/messaging"; +import { ApiController, Client } from "@bandwidth/messaging"; import { log } from "../../../lib"; import { getFormattedPhoneNumber } from "../../../lib/phone-format"; @@ -35,17 +35,12 @@ export function errorDescription(errorCode) { } export async function getBandwidthController(organization, config) { - const password = await convertSecret( - "bandwidthPassword", - organization, - config.password - ); - const client = new BandwidthMessaging.Client({ + const client = new Client({ timeout: 0, basicAuthUserName: config.userName, - basicAuthPassword: password + basicAuthPassword: config.password }); - return new BandwidthMessaging.ApiController(client); + return new ApiController(client); } export async function sendMessage({ @@ -71,6 +66,13 @@ export async function sendMessage({ message.user_number || config.userNumber; + console.log( + "bandwidth.sendMessage", + applicationId, + userNumber, + message.contact_number + ); + const changes = {}; if (applicationId && applicationId != message.messageservice_sid) { changes.messageservice_sid = applicationId; @@ -89,6 +91,8 @@ export async function sendMessage({ from: userNumber, text: parsedMessage.body, tag + //, TODO: expires: .... = message.send_before + // example date: "2021-10-02T15:00:00Z" }; if (parsedMessage.mediaUrl) { bandwidthMessage.media = [parsedMessage.mediaUrl]; @@ -123,7 +127,13 @@ export async function sendMessage({ config.accountId, bandwidthMessage ); + console.log( + "bandwidth.sendMessage createMessage response", + response && response.statusCode, + response.result + ); } catch (err) { + console.log("bandwidth.sendMessage ERROR", err); await postMessageSend({ err, message, @@ -158,6 +168,21 @@ export async function postMessageSend({ ...changes } : {}; + if (response.statusCode === 202 && response.result) { + changesToSave.service_id = response.result.id; + } else { + // ERROR + changesToSave.send_status = "ERROR"; + // TODO: maybe there is sometimes an error response in the JSON? + changesToSave.error_code = response.statusCode; + } + let updateQuery = r.knex("message").where("id", message.id); + if (trx) { + updateQuery = updateQuery.transacting(trx); + } + await updateQuery.update(changesToSave); + // TODO: error_code or + // TODO: campaign_contact update if errorcode } export async function handleIncomingMessage(message, { orgId }) { @@ -170,6 +195,7 @@ export async function handleIncomingMessage(message, { orgId }) { log.error(`This is not an incoming message: ${JSON.stringify(message)}`); return; } + console.log("bandwidth.handleIncomingMessage", JSON.stringify(message)); const { id, from, text, applicationId, media } = message.message; const contactNumber = getFormattedPhoneNumber(from); const userNumber = message.to ? getFormattedPhoneNumber(message.to) : ""; @@ -204,7 +230,7 @@ export async function handleIncomingMessage(message, { orgId }) { export async function handleDeliveryReport(report, { orgId }) { // https://dev.bandwidth.com/messaging/callbacks/msgDelivered.html // https://dev.bandwidth.com/messaging/callbacks/messageFailed.html - + console.log("bandwidth.handleDeliveryReport", report); const { id, from, applicationId, tag } = report.message; const contactNumber = getFormattedPhoneNumber(report.to); const userNumber = from ? getFormattedPhoneNumber(from) : ""; From 0b0a3853c061519abb81e81ba73a30777fd98493 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Wed, 16 Jun 2021 09:56:55 -0400 Subject: [PATCH 097/191] bandwidth: fix response scope --- .../service-vendors/bandwidth/messaging.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/extensions/service-vendors/bandwidth/messaging.js b/src/extensions/service-vendors/bandwidth/messaging.js index 553add300..77f8e9835 100644 --- a/src/extensions/service-vendors/bandwidth/messaging.js +++ b/src/extensions/service-vendors/bandwidth/messaging.js @@ -73,7 +73,11 @@ export async function sendMessage({ message.contact_number ); - const changes = {}; + const changes = { + send_status: "SENT", + service: "bandwidth", + sent_at: new Date() + }; if (applicationId && applicationId != message.messageservice_sid) { changes.messageservice_sid = applicationId; } @@ -123,14 +127,14 @@ export async function sendMessage({ organization, config ); - const response = await messagingController.createMessage( + response = await messagingController.createMessage( config.accountId, bandwidthMessage ); console.log( "bandwidth.sendMessage createMessage response", response && response.statusCode, - response.result + response && response.result ); } catch (err) { console.log("bandwidth.sendMessage ERROR", err); @@ -168,7 +172,7 @@ export async function postMessageSend({ ...changes } : {}; - if (response.statusCode === 202 && response.result) { + if (response && response.statusCode === 202 && response.result) { changesToSave.service_id = response.result.id; } else { // ERROR From eba86fce76f7108b39e7d1adeac5f0a3baf025b4 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Wed, 16 Jun 2021 11:02:05 -0400 Subject: [PATCH 098/191] bandwidth: more bugfixes: imports fixed; use tag data for faster lookup --- .../service-vendors/bandwidth/messaging.js | 15 ++++++++-- .../models/cacheable_queries/message.js | 30 +++++++++++++------ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/extensions/service-vendors/bandwidth/messaging.js b/src/extensions/service-vendors/bandwidth/messaging.js index 77f8e9835..5bc1dce91 100644 --- a/src/extensions/service-vendors/bandwidth/messaging.js +++ b/src/extensions/service-vendors/bandwidth/messaging.js @@ -3,7 +3,7 @@ import { ApiController, Client } from "@bandwidth/messaging"; import { log } from "../../../lib"; import { getFormattedPhoneNumber } from "../../../lib/phone-format"; import { getConfig, hasConfig } from "../../../server/api/lib/config"; -import { cacheableData, Log, Message } from "../../../server/models"; +import { r, cacheableData, Log, Message } from "../../../server/models"; import { saveNewIncomingMessage, parseMessageText } from "../message-sending"; import { getMessageServiceConfig, getConfigKey } from "../service_map"; @@ -239,7 +239,7 @@ export async function handleDeliveryReport(report, { orgId }) { const contactNumber = getFormattedPhoneNumber(report.to); const userNumber = from ? getFormattedPhoneNumber(from) : ""; // FUTURE: tag should have: "||" - await cacheableData.message.deliveryReport({ + const deliveryReport = { contactNumber, userNumber, messageSid: id, @@ -247,5 +247,14 @@ export async function handleDeliveryReport(report, { orgId }) { messageServiceSid: applicationId, newStatus: report.type === "message-failed" ? "ERROR" : "DELIVERED", errorCode: Number(report.errorCode || 0) || 0 - }); + }; + if (tag && tag.match(/\|/g).length === 2) { + const [orgId, campaignId, contactId] = tag.split("|"); + Object.assign(deliveryReport, { + orgId, + campaignId, + contactId + }); + } + await cacheableData.message.deliveryReport(deliveryReport); } diff --git a/src/server/models/cacheable_queries/message.js b/src/server/models/cacheable_queries/message.js index 7d699e675..9a38ffcdf 100644 --- a/src/server/models/cacheable_queries/message.js +++ b/src/server/models/cacheable_queries/message.js @@ -148,7 +148,11 @@ const deliveryReport = async ({ service, messageServiceSid, newStatus, - errorCode + errorCode, + // not reliable: + contactId, + campaignId, + orgId }) => { const changes = { service_response_at: new Date(), @@ -158,15 +162,22 @@ const deliveryReport = async ({ changes.user_number = userNumber; } let lookup; + if (contactId) { + lookup = { + campaign_contact_id: contactId, + campaign_id: campaignId + }; + } if (newStatus === "ERROR") { changes.error_code = errorCode; - - lookup = await campaignContactCache.lookupByCell( - contactNumber, - service || "", - messageServiceSid, - userNumber - ); + if (!lookup) { + lookup = await campaignContactCache.lookupByCell( + contactNumber, + service || "", + messageServiceSid, + userNumber + ); + } if (lookup && lookup.campaign_contact_id) { await r .knex("campaign_contact") @@ -197,7 +208,8 @@ const deliveryReport = async ({ const campaignContact = await campaignContactCache.load( lookup.campaign_contact_id ); - const organizationId = await campaignContactCache.orgId(campaignContact); + const organizationId = + orgId || (await campaignContactCache.orgId(campaignContact)); const organization = await orgCache.load(organizationId); await processServiceManagers("onDeliveryReport", organization, { campaignContact, From 850958a6ca2fd16246e2d27344a4fe3f65146813 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Thu, 17 Jun 2021 11:29:22 -0400 Subject: [PATCH 099/191] service-managers: in-progress=carrier-lookup and sticky-sender (migration) --- migrations/20190207220000_init_db.js | 3 - .../20210220144200_organization_contacts.js | 1 + .../20210426112734_add_delivery_confirmed.js | 39 -------- .../service-managers/carrier-lookup/index.js | 74 +++++++++++++++ .../service-managers/numpicker-basic/index.js | 14 ++- .../service-managers/sticky-sender/index.js | 95 +++++++++++++++++++ .../service-vendors/bandwidth/index.js | 6 +- .../service-vendors/bandwidth/messaging.js | 42 ++++++++ .../bandwidth/setup-and-numbers.js | 2 +- .../service-vendors/twilio/index.js | 19 ---- .../models/cacheable_queries/message.js | 50 ++-------- .../cacheable_queries/organization-contact.js | 33 +++++-- src/server/models/message.js | 1 - src/server/models/organization-contact.js | 1 + 14 files changed, 263 insertions(+), 117 deletions(-) delete mode 100644 migrations/20210426112734_add_delivery_confirmed.js create mode 100644 src/extensions/service-managers/carrier-lookup/index.js create mode 100644 src/extensions/service-managers/sticky-sender/index.js diff --git a/migrations/20190207220000_init_db.js b/migrations/20190207220000_init_db.js index 6dd304be3..1c72f0ea8 100644 --- a/migrations/20190207220000_init_db.js +++ b/migrations/20190207220000_init_db.js @@ -359,9 +359,6 @@ const initialize = async knex => { "PAUSED", "NOT_ATTEMPTED" ]; - if (isSqlite) { - statuses.push("DELIVERED CONFIRMED"); - } t.enu("send_status", statuses).notNullable(); t.timestamp("created_at") .defaultTo(knex.fn.now()) diff --git a/migrations/20210220144200_organization_contacts.js b/migrations/20210220144200_organization_contacts.js index 782f32e49..de4579524 100644 --- a/migrations/20210220144200_organization_contacts.js +++ b/migrations/20210220144200_organization_contacts.js @@ -23,6 +23,7 @@ exports.up = async function up(knex) { .inTable("organization"); t.text("contact_number").notNullable(); t.text("user_number"); + t.text("service"); t.integer("subscribe_status").defaultTo(0); t.text("carrier"); t.timestamp("created_at").defaultTo(knex.fn.now()); diff --git a/migrations/20210426112734_add_delivery_confirmed.js b/migrations/20210426112734_add_delivery_confirmed.js deleted file mode 100644 index c59e8b1ce..000000000 --- a/migrations/20210426112734_add_delivery_confirmed.js +++ /dev/null @@ -1,39 +0,0 @@ -// Add DELIVERED CONFIRMED as a value in the `send_status` enumeration -exports.up = knex => { - const isSqlite = /sqlite/.test(knex.client.config.client); - if (isSqlite) { - return Promise.resolve(); - } - return knex.schema.raw(` - ALTER TABLE "message" DROP CONSTRAINT IF EXISTS "message_send_status_check"; - ALTER TABLE "message" ADD CONSTRAINT "message_send_status_check" CHECK (send_status IN ( - 'QUEUED'::text, - 'SENDING'::text, - 'SENT'::text, - 'DELIVERED'::text, - 'DELIVERED CONFIRMED'::text, - 'ERROR'::text, - 'PAUSED'::text, - 'NOT_ATTEMPTED'::text - )) - `); -}; - -exports.down = knex => { - const isSqlite = /sqlite/.test(knex.client.config.client); - if (isSqlite) { - return Promise.resolve(); - } - return knex.schema.raw(` - ALTER TABLE "message" DROP CONSTRAINT IF EXISTS "message_send_status_check"; - ALTER TABLE "message" ADD CONSTRAINT "message_send_status_check" CHECK (send_status IN ( - 'QUEUED'::text, - 'SENDING'::text, - 'SENT'::text, - 'DELIVERED'::text, - 'ERROR'::text, - 'PAUSED'::text, - 'NOT_ATTEMPTED'::text - )) - `); -}; diff --git a/src/extensions/service-managers/carrier-lookup/index.js b/src/extensions/service-managers/carrier-lookup/index.js new file mode 100644 index 000000000..37884daf6 --- /dev/null +++ b/src/extensions/service-managers/carrier-lookup/index.js @@ -0,0 +1,74 @@ +import { r, cacheableData } from "../../../server/models"; +import { getService } from "../../service-vendors"; + +export const name = "carrier-lookup"; + +export const metadata = () => ({ + // set canSpendMoney=true, if this extension can lead to (additional) money being spent + // if it can, which operations below can trigger money being spent? + displayName: "Carrier Lookup", + description: + "Gets carrier info of contacts when available from service-vendor", + canSpendMoney: true, + moneySpendingOperations: ["onDeliveryReport"], + // TODO: org config for: just errors, probabilistic sampling percent, on contact-load, always paid (w/names) + // TODO: org config for: only with campaigns with X prefix + supportsOrgConfig: false, + supportsCampaignConfig: false +}); + +// NOTE: this is somewhat expensive relatively what it usually is, +// so only implement this if it's important +export async function onDeliveryReport({ + contactNumber, + userNumber, + messageSid, + service, + messageServiceSid, + newStatus, + errorCode, + organization, + campaignContact, + lookup +}) { + const serviceVendor = getService(service); + const organizationId = organization.id; + const organizationContact = await cacheableData.organizationContact.query({ + organizationId, + contactNumber + }); + + if (!organizationContact || !organizationContact.carrier) { + const orgContact = { + organization_id: organizationId, + contact_number: contactNumber + }; + + let contactInfo; + if (serviceVendor.getFreeContactInfo) { + contactInfo = await serviceVendor.getFreeContactInfo({ + organization, + contactNumber, + messageSid, + messageServiceSid + }); + Object.assign(orgContact, contactInfo); + } else if (serviceVendor.getNonFreeContactInfo) { + contactInfo = await serviceVendor.getNonFreeContactInfo({ + organization, + contactNumber, + messageSid, + messageServiceSid + }); + Object.assign(orgContact, contactInfo); + } + + if (!organizationContact) { + await cacheableData.organizationContact.save(orgContact); + } else if (contactInfo) { + await cacheableData.organizationContact.save(orgContact, { + update: true + }); + } + } +} diff --git a/src/extensions/service-managers/numpicker-basic/index.js b/src/extensions/service-managers/numpicker-basic/index.js index 55636fa84..7e5cc8533 100644 --- a/src/extensions/service-managers/numpicker-basic/index.js +++ b/src/extensions/service-managers/numpicker-basic/index.js @@ -13,7 +13,7 @@ export const metadata = () => ({ "Picks a number available in owned_phone_number table for the service to send messages with. Defaults to basic rotation.", canSpendMoney: false, moneySpendingOperations: [], - supportsOrgConfig: true, + supportsOrgConfig: false, supportsCampaignConfig: false }); @@ -24,6 +24,12 @@ export async function onMessageSend({ campaign, serviceManagerData }) { + console.log( + "numpicker-basic.onMessageSend", + message.id, + message.user_number, + serviceManagerData + ); if ( message.user_number || (serviceManagerData && serviceManagerData.user_number) @@ -42,6 +48,7 @@ export async function onMessageSend({ .orderByRaw("random()") .select("phone_number") .first(); + console.log("numpicker-basic.onMessageSend selectedPhone", selectedPhone); // TODO: caching // TODO: something better than pure rotation -- maybe with caching we use metrics // based on sad deliveryreports @@ -49,5 +56,10 @@ export async function onMessageSend({ return { user_number: selectedPhone.phone_number }; } else { // TODO: what should we do if there's no result? + console.log( + "numpicker-basic.onMessageSend none found", + serviceName, + organization.id + ); } } diff --git a/src/extensions/service-managers/sticky-sender/index.js b/src/extensions/service-managers/sticky-sender/index.js new file mode 100644 index 000000000..50c6b86b2 --- /dev/null +++ b/src/extensions/service-managers/sticky-sender/index.js @@ -0,0 +1,95 @@ +// TODO +// 1. get carrierName in from +// 2. can we avoid queries -- what about inserts on-fail? +// 3. Maybe preload onCampaignStart? + +/// All functions are OPTIONAL EXCEPT metadata() and const name=. +/// DO NOT IMPLEMENT ANYTHING YOU WILL NOT USE -- the existence of a function adds behavior/UI (sometimes costly) + +import { cacheableData } from "../../../server/models"; + +export const name = "sticky-sender"; + +export const metadata = () => ({ + // set canSpendMoney=true, if this extension can lead to (additional) money being spent + // if it can, which operations below can trigger money being spent? + displayName: "Sticky Sender", + description: + "Tracks and maintains the same phone number for a particular contact's phone number.", + canSpendMoney: false, + moneySpendingOperations: [], + supportsOrgConfig: false, + supportsCampaignConfig: false +}); + +export async function onMessageSend({ + message, + contact, + organization, + campaign, + serviceManagerData +}) { + console.log( + "sticky-sender.onMessageSend", + message.id, + message.user_number, + serviceManagerData + ); + if ( + message.user_number || + (serviceManagerData && serviceManagerData.user_number) + ) { + // If another serviceManager already chose a phone number then don't change anything + return; + } + const serviceName = cacheableData.organization.getMessageService( + organization + ); + + const organizationContact = await cacheableData.organizationContact.query({ + organizationId: organization.id, + contactNumber: message.contact_number + }); + + if (organizationContact && organizationContact.user_number) { + return { user_number: organizationContact.user_number }; + } +} + +// NOTE: this is somewhat expensive relatively what it usually is, +// so only implement this if it's important +export async function onDeliveryReport({ + contactNumber, + userNumber, + messageSid, + service, + messageServiceSid, + newStatus, + errorCode, + organization, + campaignContact, + lookup +}) { + if (userNumber && newStatus === "DELIVERED" && !errorCode) { + const organizationId = organization.id; + const organizationContact = await cacheableData.organizationContact.query({ + organizationId, + contactNumber + }); + + const orgContact = { + organization_id: organizationId, + contact_number: contactNumber, + user_number: userNumber, + service + }; + + if (!organizationContact) { + await cacheableData.organizationContact.save(orgContact); + } else if (!organizationContact.user_number) { + await cacheableData.organizationContact.save(orgContact, { + update: true + }); + } + } +} diff --git a/src/extensions/service-vendors/bandwidth/index.js b/src/extensions/service-vendors/bandwidth/index.js index 6d8ac7e21..2f84568e2 100644 --- a/src/extensions/service-vendors/bandwidth/index.js +++ b/src/extensions/service-vendors/bandwidth/index.js @@ -19,14 +19,16 @@ import { sendMessage, handleIncomingMessage, handleDeliveryReport, - errorDescription + errorDescription, + getFreeContactInfo } from "./messaging"; export { sendMessage, handleIncomingMessage, handleDeliveryReport, - errorDescription + errorDescription, + getFreeContactInfo }; export const getMetadata = () => ({ diff --git a/src/extensions/service-vendors/bandwidth/messaging.js b/src/extensions/service-vendors/bandwidth/messaging.js index 5bc1dce91..d78c15640 100644 --- a/src/extensions/service-vendors/bandwidth/messaging.js +++ b/src/extensions/service-vendors/bandwidth/messaging.js @@ -258,3 +258,45 @@ export async function handleDeliveryReport(report, { orgId }) { } await cacheableData.message.deliveryReport(deliveryReport); } + +export async function getFreeContactInfo({ + organization, + contactNumber, + messageSid, + messageServiceSid +}) { + const config = await getMessageServiceConfig("bandwidth", organization, { + obscureSensitiveInformation: false, + ...serviceManagerData + }); + const messagingController = await getBandwidthController( + organization, + config + ); + + let messageData; + if (messageSid) { + messageData = await messagingController.getMessages( + config.accountId, + messageSid + ); + } else if (contactNumber) { + messageData = await messagingController.getMessages( + config.accountId, + null, + null, + contactNumber, // destinationTn + null, + null, + null, + null, + null, + 1 // limit + ); + } + console.log("carrier-lookup", messageData); + if (messageData && messageData.messages && messageData.messages.length) { + const carrier = messageData.messages[0].carrierName; + return { carrier }; + } +} diff --git a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js index 85b8b1cf3..eb917a7ce 100644 --- a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js +++ b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js @@ -97,7 +97,7 @@ export async function fullyConfigured(organization) { ) { return false; } - // TODO: also needs some number to send with numpicker in servicemanagers + // TODO: also needs some number to send with numpicker AND sticky-sender in servicemanagers return true; } diff --git a/src/extensions/service-vendors/twilio/index.js b/src/extensions/service-vendors/twilio/index.js index 94e1eb4f9..6731fe18f 100644 --- a/src/extensions/service-vendors/twilio/index.js +++ b/src/extensions/service-vendors/twilio/index.js @@ -230,19 +230,6 @@ async function getMessagingServiceSid( ); } -async function getOrganizationContactUserNumber(organization, contactNumber) { - const organizationContact = await cacheableData.organizationContact.query({ - organizationId: organization.id, - contactNumber - }); - - if (organizationContact && organizationContact.user_number) { - return organizationContact.user_number; - } - - return null; -} - export async function sendMessage({ message, contact, @@ -279,12 +266,6 @@ export async function sendMessage({ let userNumber = (serviceManagerData && serviceManagerData.user_number) || message.user_number; - if (process.env.EXPERIMENTAL_STICKY_SENDER && !userNumber) { - userNumber = await getOrganizationContactUserNumber( - organization, - contact.cell - ); - } return new Promise((resolve, reject) => { if (message.service !== "twilio") { diff --git a/src/server/models/cacheable_queries/message.js b/src/server/models/cacheable_queries/message.js index 9a38ffcdf..9dc97cfcf 100644 --- a/src/server/models/cacheable_queries/message.js +++ b/src/server/models/cacheable_queries/message.js @@ -205,11 +205,14 @@ const deliveryReport = async ({ messageServiceSid, userNumber )); - const campaignContact = await campaignContactCache.load( - lookup.campaign_contact_id - ); - const organizationId = - orgId || (await campaignContactCache.orgId(campaignContact)); + let organizationId = orgid; + let campaignContact; + if (!organizationId) { + campaignContact = await campaignContactCache.load( + lookup.campaign_contact_id + ); + organizationId = await campaignContactCache.orgId(campaignContact); + } const organization = await orgCache.load(organizationId); await processServiceManagers("onDeliveryReport", organization, { campaignContact, @@ -223,43 +226,6 @@ const deliveryReport = async ({ errorCode }); } - // TODO: move the below into a test for service-strategies if there are onDeliveryReport impls - // which uses campaignContactCache.lookupByCell above - if ( - userNumber && - process.env.EXPERIMENTAL_STICKY_SENDER && - newStatus === "DELIVERED" - ) { - lookup = - lookup || - (await campaignContactCache.lookupByCell( - contactNumber, - service || "", - messageServiceSid, - userNumber - )); - // FUTURE: maybe don't do anything if userNumber existed before above save in message? - if (lookup) { - // Assign user number to contact/organization - const campaignContact = await campaignContactCache.load( - lookup.campaign_contact_id - ); - - const organizationId = await campaignContactCache.orgId(campaignContact); - const organizationContact = await organizationContactCache.query({ - organizationId, - contactNumber - }); - - if (!organizationContact) { - await organizationContactCache.save({ - organizationId, - contactNumber, - userNumber - }); - } - } - } }; const messageCache = { diff --git a/src/server/models/cacheable_queries/organization-contact.js b/src/server/models/cacheable_queries/organization-contact.js index 25c1f51c6..1987ef584 100644 --- a/src/server/models/cacheable_queries/organization-contact.js +++ b/src/server/models/cacheable_queries/organization-contact.js @@ -34,21 +34,36 @@ const organizationContactCache = { return organizationContact; }, - save: async ({ organizationId, contactNumber, userNumber }) => { - const organizationContact = { - organization_id: organizationId, - contact_number: contactNumber, - user_number: userNumber - }; + save: async (organizationContact, options) => { + const organizationId = organizationContact.organization_id; + const contactNumber = organizationContact.contact_number; - await r.knex("organization_contact").insert(organizationContact); + if (options && options.update) { + await r + .knex("organization_contact") + .where({ + organization_id: organizationId, + contact_number: contactNumber + }) + .update(organizationContact); + } else { + await r.knex("organization_contact").insert(organizationContact); + } if (r.redis) { const cacheKey = getCacheKey(organizationId, contactNumber); - + const cachedContact = JSON.parse( + (await r.redis.getAsync(cacheKey)) || "{}" + ); await r.redis .multi() - .set(cacheKey, JSON.stringify(organizationContact)) + .set( + cacheKey, + JSON.stringify({ + ...cachedContact, + ...organizationContact + }) + ) .expire(cacheKey, 43200) // 12 hours .execAsync(); } diff --git a/src/server/models/message.js b/src/server/models/message.js index 56e610de2..daa616495 100644 --- a/src/server/models/message.js +++ b/src/server/models/message.js @@ -40,7 +40,6 @@ const Message = thinky.createModel( "SENDING", "SENT", "DELIVERED", - "DELIVERED CONFIRMED", "ERROR", "PAUSED", "NOT_ATTEMPTED" diff --git a/src/server/models/organization-contact.js b/src/server/models/organization-contact.js index a04dfc524..660204458 100644 --- a/src/server/models/organization-contact.js +++ b/src/server/models/organization-contact.js @@ -18,6 +18,7 @@ const OrganizationContact = thinky.createModel( organization_id: requiredString(), contact_number: requiredString(), user_number: optionalString(), + service: optionalString(), subscribe_status: type.integer().default(0), carrier: optionalString(), created_at: timestamp(), From fab1a358af1c90720921d5dd97e487f592bd5b5b Mon Sep 17 00:00:00 2001 From: Stefan Hayden Date: Fri, 18 Jun 2021 14:01:15 -0400 Subject: [PATCH 100/191] Cleanup setDefaults pattern for texteruiform --- src/components/CampaignTexterUIForm.jsx | 15 +++- .../react-component.js | 77 ++++++++++++------- .../default-editinitial/react-component.js | 41 ++++++---- .../react-component.js | 59 +++++++++----- .../freshworks-widget/react-component.js | 17 +++- .../mobilize-event-shifter/react-component.js | 23 ++++-- .../tag-contact/react-component.js | 26 ++++++- .../take-conversations/react-component.js | 9 ++- 8 files changed, 189 insertions(+), 78 deletions(-) diff --git a/src/components/CampaignTexterUIForm.jsx b/src/components/CampaignTexterUIForm.jsx index 831368ec1..83af25b7b 100644 --- a/src/components/CampaignTexterUIForm.jsx +++ b/src/components/CampaignTexterUIForm.jsx @@ -61,6 +61,16 @@ export default class CampaignTexterUIForm extends React.Component { }); }; + /** + * if a sidebox has default values they should emit them on mount + * and they will be properly set on mount of this component + */ + collectedDefaults = {}; + + componentDidMount() { + this.setState(this.collectedDefaults); + } + render() { const keys = Object.keys(sideboxes); const adminItems = []; @@ -92,7 +102,10 @@ export default class CampaignTexterUIForm extends React.Component { this.setState(defaults)} + setDefaultsOnMount={defaults => { + // collect default to setState on mount + Object.assign(this.collectedDefaults, defaults); + }} organization={this.props.organization} /> )} diff --git a/src/extensions/texter-sideboxes/default-dynamicassignment/react-component.js b/src/extensions/texter-sideboxes/default-dynamicassignment/react-component.js index ba5d4042d..edf071cd9 100644 --- a/src/extensions/texter-sideboxes/default-dynamicassignment/react-component.js +++ b/src/extensions/texter-sideboxes/default-dynamicassignment/react-component.js @@ -42,6 +42,10 @@ export const showSummary = ({ campaign, assignment, settingsData }) => !assignment.unmessagedCount && assignment.maxContacts !== 0; +const defaultDynamicAssignmentRequestMoreMessage = + "Finished sending all your messages, and want to send more?"; +const defaultDynamicAssignmentRequestMoreLabel = "Send more texts"; + export class TexterSideboxClass extends React.Component { requestNewContacts = async () => { const { assignment, messageStatusFilter } = this.props; @@ -97,15 +101,16 @@ export class TexterSideboxClass extends React.Component { assignment.allContactsCount === 0 ? "Start texting with your first batch" : settingsData.dynamicAssignmentRequestMoreMessage || - "Finished sending all your messages, and want to send more?"; + defaultDynamicAssignmentRequestMoreMessage; const nextBatchMoreLabel = assignment.allContactsCount === 0 ? "Start texting" - : settingsData.dynamicAssignmentRequestMoreLabel || "Send more texts"; + : settingsData.dynamicAssignmentRequestMoreLabel || + defaultDynamicAssignmentRequestMoreLabel; const headerStyle = messageStatusFilter ? { textAlign: "center" } : {}; return (
- {assignment.hasUnassignedContactsForTexter ? ( + {assignment.hasUnassignedContactsForTexter && (

{nextBatchMessage}

- ) : null} - {messageStatusFilter === "needsMessage" && assignment.unrepliedCount ? ( + )} + {messageStatusFilter === "needsMessage" && assignment.unrepliedCount && (
- ) : null} + )} {messageStatusFilter && - messageStatusFilter !== "needsMessage" && - assignment.unmessagedCount ? ( -
- - - -
- ) : null} - {contact /*the empty list*/ ? ( + messageStatusFilter !== "needsMessage" && + assignment.unmessagedCount && ( +
+ + + +
+ )} + {contact /*the empty list*/ && (
- ) : null} + )} {!assignment.hasUnassignedContactsForTexter && - !contact && - !assignment.unmessagedCount && - !assignment.unrepliedCount && - settingsData.dynamicAssignmentNothingToDoMessage ? ( - // assignment summary when there is nothing to do -
- {settingsData.dynamicAssignmentNothingToDoMessage} -
- ) : null} + !contact && + !assignment.unmessagedCount && + !assignment.unrepliedCount && + settingsData.dynamicAssignmentNothingToDoMessage && ( + // assignment summary when there is nothing to do +
+ {settingsData.dynamicAssignmentNothingToDoMessage} +
+ )}
); } @@ -217,6 +222,22 @@ export const adminSchema = () => ({ }); export class AdminConfig extends React.Component { + componentDidMount() { + const { settingsData } = this.props; + // set defaults + const defaults = {}; + if (!settingsData.dynamicAssignmentRequestMoreLabel) { + defaults.dynamicAssignmentRequestMoreLabel = defaultDynamicAssignmentRequestMoreLabel; + } + if (!settingsData.dynamicAssignmentRequestMoreMessage) { + defaults.dynamicAssignmentRequestMoreMessage = defaultDynamicAssignmentRequestMoreMessage; + } + + if (Object.values(defaults).length) { + this.props.setDefaultsOnMount(defaults); + } + } + render() { return (
@@ -225,14 +246,12 @@ export class AdminConfig extends React.Component { name="dynamicAssignmentRequestMoreLabel" label="Request More Label" fullWidth - hintText="default: Send more texts" /> contact && messageStatusFilter === "needsMessage"; const defaultExpandText = "Sending Initial Messages"; -const defaultMessagePre = ( - - It’s important to follow all training materials and campaign manager - guidance while texting. Please don’t - -); +const defaultMessagePre = + "It's important to follow all training materials and campaign manager guidance while texting. Please don't"; const defaultLinkText = "change the initial script"; const defaultMessagePost = "unless instructed by your campaign administrator. Making changes may flag your account for admins."; @@ -100,6 +96,28 @@ export const adminSchema = () => ({ }); export class AdminConfig extends React.Component { + componentDidMount() { + const { settingsData } = this.props; + // set defaults + const defaults = {}; + if (!settingsData.editInitialExpandText) { + defaults.editInitialExpandText = defaultExpandText; + } + if (!settingsData.editInitialMessagePre) { + defaults.editInitialMessagePre = defaultMessagePre; + } + if (!settingsData.editInitialLinkText) { + defaults.editInitialLinkText = defaultLinkText; + } + if (!settingsData.editInitialMessagePost) { + defaults.editInitialMessagePost = defaultMessagePost; + } + + if (Object.values(defaults).length) { + this.props.setDefaultsOnMount(defaults); + } + } + render() { return (
@@ -114,29 +132,25 @@ export class AdminConfig extends React.Component {
@@ -146,5 +160,6 @@ export class AdminConfig extends React.Component { AdminConfig.propTypes = { settingsData: type.object, - onToggle: type.func + onToggle: type.func, + setDefaultsOnMount: type.func }; diff --git a/src/extensions/texter-sideboxes/default-releasecontacts/react-component.js b/src/extensions/texter-sideboxes/default-releasecontacts/react-component.js index 15e7ee4c0..fc8af91a9 100644 --- a/src/extensions/texter-sideboxes/default-releasecontacts/react-component.js +++ b/src/extensions/texter-sideboxes/default-releasecontacts/react-component.js @@ -45,6 +45,11 @@ export const showSidebox = ({ export const showSummary = showSidebox; +const defaulReleaseContactsBatchTitle = "Can't send the rest of these texts?"; +const defaultReleaseContactsBatchLabel = "Done for the day"; +const defaultReleaseContactsConvosTitle = "Need to give up?"; +const defaultReleaseContactsConvosLabel = "Release all my contacts"; + export class TexterSideboxClass extends React.Component { handleReleaseContacts = async releaseConversations => { await this.props.mutations.releaseContacts(releaseConversations); @@ -69,17 +74,16 @@ export class TexterSideboxClass extends React.Component { (messageStatusFilter === "needsMessage" && assignment.hasUnmessaged) ? (
- {settingsData.releaseContactsBatchTitle ? ( - settingsData.releaseContactsBatchTitle - ) : ( - Can’t send the rest of these texts? - )} + {settingsData.releaseContactsBatchTitle + ? settingsData.releaseContactsBatchTitle + : defaulReleaseContactsBatchTitle}
) : null} @@ -88,18 +92,18 @@ export class TexterSideboxClass extends React.Component {
{settingsData.releaseContactsConvosTitle ? settingsData.releaseContactsConvosTitle - : "Need to give up?"} + : defaultReleaseContactsConvosTitle}
)} @@ -186,6 +190,28 @@ export const adminSchema = () => ({ }); export class AdminConfig extends React.Component { + componentDidMount() { + const { settingsData } = this.props; + // set defaults + const defaults = {}; + if (!settingsData.releaseContactsBatchTitle) { + defaults.releaseContactsBatchTitle = defaulReleaseContactsBatchTitle; + } + if (!settingsData.releaseContactsBatchLabel) { + defaults.releaseContactsBatchLabel = defaultReleaseContactsBatchLabel; + } + if (!settingsData.releaseContactsConvosTitle) { + defaults.releaseContactsConvosTitle = defaultReleaseContactsConvosTitle; + } + if (!settingsData.releaseContactsConvosLabel) { + defaults.releaseContactsConvosLabel = defaultReleaseContactsConvosLabel; + } + + if (Object.values(defaults).length) { + this.props.setDefaultsOnMount(defaults); + } + } + render() { return (
@@ -195,9 +221,7 @@ export class AdminConfig extends React.Component { control={ this.props.onToggle( "releaseContactsReleaseConvos", @@ -214,9 +238,7 @@ export class AdminConfig extends React.Component { control={ this.props.onToggle( "releaseContactsNonDynamicToo", @@ -231,28 +253,24 @@ export class AdminConfig extends React.Component { name="releaseContactsBatchTitle" label="Title for releasing contacts" fullWidth - hintText="default: Can't send the rest of these texts?" />
); @@ -261,5 +279,6 @@ export class AdminConfig extends React.Component { AdminConfig.propTypes = { enabled: type.bool, - data: type.string + data: type.string, + setDefaultsOnMount: type.func }; diff --git a/src/extensions/texter-sideboxes/freshworks-widget/react-component.js b/src/extensions/texter-sideboxes/freshworks-widget/react-component.js index 9c9a05503..a8f72e941 100644 --- a/src/extensions/texter-sideboxes/freshworks-widget/react-component.js +++ b/src/extensions/texter-sideboxes/freshworks-widget/react-component.js @@ -79,6 +79,19 @@ export const adminSchema = () => ({ }); export class AdminConfig extends React.Component { + componentDidMount() { + const { settingsData } = this.props; + // set defaults + const defaults = {}; + if (!settingsData.helpButtonLabel) { + defaults.helpButtonLabel = "Help"; + } + + if (Object.values(defaults).length) { + this.props.setDefaultsOnMount(defaults); + } + } + render() { return (
@@ -86,7 +99,6 @@ export class AdminConfig extends React.Component { as={GSTextField} name="helpButtonLabel" label="Help Button Label" - hintText="default: Help" fullWidth /> ({ }); export class AdminConfig extends React.Component { + componentDidMount() { + const { settingsData } = this.props; + // set defaults + const defaults = {}; + if (!settingsData.mobilizeEventShifterBaseUrl) { + defaults.mobilizeEventShifterBaseUrl = + window.MOBILIZE_EVENT_SHIFTER_URL || ""; + } + + if (Object.values(defaults).length) { + this.props.setDefaultsOnMount(defaults); + } + } + render() { return (
@@ -245,15 +259,12 @@ export class AdminConfig extends React.Component { name="mobilizeEventShifterBaseUrl" label="Set the Base Mobilize Url for the campaign." fullWidth - hintText={ - window.MOBILIZE_EVENT_SHIFTER_URL - ? "Default: " + window.MOBILIZE_EVENT_SHIFTER_URL - : "" - } />
); } } -AdminConfig.propTypes = {}; +AdminConfig.propTypes = { + setDefaultsOnMount: type.func +}; diff --git a/src/extensions/texter-sideboxes/tag-contact/react-component.js b/src/extensions/texter-sideboxes/tag-contact/react-component.js index 9c47307a4..89809fd1f 100644 --- a/src/extensions/texter-sideboxes/tag-contact/react-component.js +++ b/src/extensions/texter-sideboxes/tag-contact/react-component.js @@ -34,6 +34,9 @@ export const showSidebox = ({ contact, campaign, messageStatusFilter }) => { ); }; +const defaultTagHeaderText = "Tag a contact here:"; +const defaultTagButtonText = "Save tags"; + export class TexterSidebox extends React.Component { state = { newTags: {}, @@ -59,7 +62,7 @@ export class TexterSidebox extends React.Component { ); return (
-

{settingsData.tagHeaderText || "Tag a contact here:"}

+

{settingsData.tagHeaderText || defaultTagHeaderText}

{escalatedTags.map((tag, index) => ( = 1 } > - {settingsData.tagButtonText || "Save tags"} + {settingsData.tagButtonText || defaultTagButtonText}
); @@ -156,6 +159,22 @@ export const adminSchema = () => ({ }); export class AdminConfig extends React.Component { + componentDidMount() { + const { settingsData } = this.props; + // set defaults + const defaults = {}; + if (!settingsData.tagHeaderText) { + defaults.tagHeaderText = defaultTagHeaderText; + } + if (!settingsData.tagButtonText) { + defaults.tagButtonText = defaultTagButtonText; + } + + if (Object.values(defaults).length) { + this.props.setDefaultsOnMount(defaults); + } + } + render() { return (
@@ -191,5 +210,6 @@ export class AdminConfig extends React.Component { AdminConfig.propTypes = { settingsData: type.object, onToggle: type.func, - organization: type.object + organization: type.object, + setDefaultsOnMount: type.func }; diff --git a/src/extensions/texter-sideboxes/take-conversations/react-component.js b/src/extensions/texter-sideboxes/take-conversations/react-component.js index 7e00bf2b0..947daf2a0 100644 --- a/src/extensions/texter-sideboxes/take-conversations/react-component.js +++ b/src/extensions/texter-sideboxes/take-conversations/react-component.js @@ -165,16 +165,17 @@ export const adminSchema = () => ({ export class AdminConfig extends React.Component { componentDidMount() { + const { settingsData } = this.props; // set defaults const defaults = {}; - if (!this.props.settingsData.takeConversationsBatchType) { + if (!settingsData.takeConversationsBatchType) { defaults.takeConversationsBatchType = "vetted-takeconversations"; } - if (!this.props.settingsData.takeConversationsBatchSize) { + if (!settingsData.takeConversationsBatchSize) { defaults.takeConversationsBatchSize = 20; } if (Object.values(defaults).length) { - this.props.setDefaults(defaults); + this.props.setDefaultsOnMount(defaults); } } @@ -236,5 +237,5 @@ export class AdminConfig extends React.Component { AdminConfig.propTypes = { settingsData: type.object, onToggle: type.func, - setDefaults: type.func + setDefaultsOnMount: type.func }; From ff2bef49687d5acf7826ecf673ccdb8c6e741293 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Fri, 2 Jul 2021 09:20:18 -0400 Subject: [PATCH 101/191] conditionally signup actionkit during mobile-commons signup -- based on whether an external_id is present --- src/extensions/action-handlers/mobilecommons-signup.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/extensions/action-handlers/mobilecommons-signup.js b/src/extensions/action-handlers/mobilecommons-signup.js index 23de26d7c..ba2dc3f17 100644 --- a/src/extensions/action-handlers/mobilecommons-signup.js +++ b/src/extensions/action-handlers/mobilecommons-signup.js @@ -60,7 +60,10 @@ export async function processAction({ : defaultProfileOptInId; const cell = contact.cell.substring(1); - actionKitSignup(contact); + if (!contact.external_id) { + // if there is already an external_id, then we have an existing user + actionKitSignup(contact); + } const extraFields = {}; const umcFields = getConfig("UMC_FIELDS", organization); From 51b2b8b579f4daa84a7d1363cdd45fa3a929c048 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Mon, 28 Jun 2021 22:40:47 -0400 Subject: [PATCH 102/191] ngpvan action phone context --- .../action-handlers/ngpvan-action.test.js | 9 +++++++-- .../action-handlers/ngpvan-action.js | 12 +++++++++++- src/lib/phone-format.js | 18 +++++++++++++++++- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/__test__/extensions/action-handlers/ngpvan-action.test.js b/__test__/extensions/action-handlers/ngpvan-action.test.js index e923f03e8..06645540d 100644 --- a/__test__/extensions/action-handlers/ngpvan-action.test.js +++ b/__test__/extensions/action-handlers/ngpvan-action.test.js @@ -1125,6 +1125,7 @@ describe("ngpvn-action", () => { describe("when the contact has a VanPhoneId", () => { beforeEach(async () => { contact = { + cell: '+15555550990', custom_fields: JSON.stringify({ VanID: "8675309", VanPhoneId: "789" @@ -1133,13 +1134,17 @@ describe("ngpvn-action", () => { body = { canvassContext: { - phoneId: "789" + phoneId: "789", + phone: { + dialingPrefix: '1', + phoneNumber: '555-555-0990' + } }, willVote: true }; }); - it("calls the people endpoint and includes VanPhoneId in the canvass context", async () => { + it("calls the people endpoint and includes VanPhoneId and phone in the canvass context", async () => { const postPeopleCanvassResponsesNock = nock( "https://api.securevan.com:443", { diff --git a/src/extensions/action-handlers/ngpvan-action.js b/src/extensions/action-handlers/ngpvan-action.js index edff5c2d7..b024829fc 100644 --- a/src/extensions/action-handlers/ngpvan-action.js +++ b/src/extensions/action-handlers/ngpvan-action.js @@ -1,6 +1,6 @@ import { getConfig } from "../../server/api/lib/config"; import Van from "../contact-loaders/ngpvan/util"; - +import { getCountryCode, getDashedPhoneNumberDisplay } from "../../lib/phone-format"; import httpRequest from "../../server/lib/http-request.js"; export const name = "ngpvan-action"; @@ -64,9 +64,19 @@ export const postCanvassResponse = async (contact, organization, bodyInput) => { ...bodyInput }; + if (contact.cell) { + const phoneCountry = process.env.PHONE_NUMBER_COUNTRY || "US"; + + body.canvassContext.phone = { + dialingPrefix: getCountryCode(contact.cell, phoneCountry).toString(), + phoneNumber: getDashedPhoneNumberDisplay(contact.cell, phoneCountry) + }; + } + if (vanPhoneId) { body.canvassContext.phoneId = vanPhoneId; } + const url = Van.makeUrl(`v4/people/${vanId}/canvassResponses`, organization); // eslint-disable-next-line no-console diff --git a/src/lib/phone-format.js b/src/lib/phone-format.js index b54606de7..2cc0085c4 100644 --- a/src/lib/phone-format.js +++ b/src/lib/phone-format.js @@ -19,8 +19,24 @@ export const getFormattedPhoneNumber = (cell, country = "US") => { } }; -export const getDisplayPhoneNumber = (e164Number, country = "US") => { +const parsePhoneNumber = (e164Number, country = "US") => { const phoneUtil = PhoneNumberUtil.getInstance(); const parsed = phoneUtil.parse(e164Number, country); + return parsed; +}; + +export const getDisplayPhoneNumber = (e164Number, country = "US") => { + const parsed = parsePhoneNumber(e164Number, country); return phoneUtil.format(parsed, PhoneNumberFormat.NATIONAL); }; + +export const getDashedPhoneNumberDisplay = (e164Number, country = "US") => { + const parsed = parsePhoneNumber(e164Number, country); + return parsed.getNationalNumber().toString() + .replace(/(\d{3})(\d{3})(\d{4})/, '$1-$2-$3'); +}; + +export const getCountryCode = (e164Number, country = "US") => { + const parsed = parsePhoneNumber(e164Number, country); + return parsed.getCountryCode(); +}; From 1512950abed041cb16a872eefb8fb52e2860d073 Mon Sep 17 00:00:00 2001 From: Stefan Hayden Date: Mon, 5 Jul 2021 09:45:19 -0400 Subject: [PATCH 103/191] style fix for iphone 5 size devices --- src/components/AssignmentTexter/StyleControls.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/AssignmentTexter/StyleControls.js b/src/components/AssignmentTexter/StyleControls.js index c267b1eb5..ccfb5a6de 100644 --- a/src/components/AssignmentTexter/StyleControls.js +++ b/src/components/AssignmentTexter/StyleControls.js @@ -6,8 +6,7 @@ export const messageListStyles = { // passesd directly to messageList: { overflow: "hidden", - overflow: "-moz-scrollbars-vertical", - maxWidth: "574px" + overflow: "-moz-scrollbars-vertical" }, messageSent: { textAlign: "left", @@ -16,7 +15,9 @@ export const messageListStyles = { backgroundColor: "white", borderRadius: "16px", marginBottom: "10px", - fontSize: "95%" + fontSize: "95%", + width: "auto", + maxWidth: "500px" }, messageReceived: { marginRight: "20%", @@ -27,7 +28,9 @@ export const messageListStyles = { //fontWeight: "600", fontSize: "110%", lineHeight: "120%", - marginBottom: "10px" + marginBottom: "10px", + width: "auto", + maxWidth: "500px" } }; @@ -117,11 +120,11 @@ export const flexStyles = StyleSheet.create({ padding: "4px 10px 9px 10px", zIndex: 2000, backgroundColor: "white", + overflow: "visible", "@media (hover: hover) and (pointer: fine)": { // for touchpads and phones, the edge of the tablet is easier // vs for desktops, we want to maximize how far the mouse needs to travel - maxWidth: "554px", - overflow: "visible" + maxWidth: "554px" } }, subSectionOptOutDialogActions: { From 48682c9c360c59a9974e4e3f3762678e09aa7982 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Tue, 13 Jul 2021 18:25:02 -0400 Subject: [PATCH 104/191] cypress test issue: trying to fix error of conflicting graphql versions. using workaround described here: https://github.com/graphql/graphql-js/issues/1182#issuecomment-388563851 NODE_ENV=production --- .github/workflows/cypress-tests.yaml | 2 +- .../texter-sideboxes/take-conversations/react-component.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cypress-tests.yaml b/.github/workflows/cypress-tests.yaml index 72f3b6f36..cf0776ad5 100644 --- a/.github/workflows/cypress-tests.yaml +++ b/.github/workflows/cypress-tests.yaml @@ -28,7 +28,7 @@ jobs: - name: Cypress run uses: cypress-io/github-action@v2 env: - NODE_ENV: test + NODE_ENV: production PORT: 3001 OUTPUT_DIR: ./build ASSETS_DIR: ./build/client/assets diff --git a/src/extensions/texter-sideboxes/take-conversations/react-component.js b/src/extensions/texter-sideboxes/take-conversations/react-component.js index 7e00bf2b0..2dc522c1b 100644 --- a/src/extensions/texter-sideboxes/take-conversations/react-component.js +++ b/src/extensions/texter-sideboxes/take-conversations/react-component.js @@ -173,7 +173,7 @@ export class AdminConfig extends React.Component { if (!this.props.settingsData.takeConversationsBatchSize) { defaults.takeConversationsBatchSize = 20; } - if (Object.values(defaults).length) { + if (Object.values(defaults).length && this.props.setDefaults) { this.props.setDefaults(defaults); } } From 075ef518ef681aa2d0eaaa9a197be00f2964686c Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Tue, 13 Jul 2021 18:38:31 -0400 Subject: [PATCH 105/191] remove selenium-handler from components --- src/components/forms/GSTextField.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/forms/GSTextField.jsx b/src/components/forms/GSTextField.jsx index 1dde20185..e7866ad32 100644 --- a/src/components/forms/GSTextField.jsx +++ b/src/components/forms/GSTextField.jsx @@ -1,7 +1,6 @@ import React from "react"; import TextField from "@material-ui/core/TextField"; import GSFormField from "./GSFormField"; -import { addConsoleHandler } from "selenium-webdriver/lib/logging"; import theme from "../../styles/mui-theme"; export default class GSTextField extends GSFormField { From 43c21c8d1fafb87cdc643b7af2cebc589910a5ef Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Tue, 13 Jul 2021 18:51:11 -0400 Subject: [PATCH 106/191] cypress tests try again -- here we are stuck between graphql needing NODE_ENV=production and cypress needing to be installed from devDependencies --- .github/workflows/cypress-tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cypress-tests.yaml b/.github/workflows/cypress-tests.yaml index cf0776ad5..9e79350a7 100644 --- a/.github/workflows/cypress-tests.yaml +++ b/.github/workflows/cypress-tests.yaml @@ -44,7 +44,7 @@ jobs: PHONE_INVENTORY: 1 with: browser: chrome - build: npm run prod-build + build: npm install --only=dev && npm run prod-build start: npm start wait-on: 'http://localhost:3001' - uses: actions/upload-artifact@v2 From 6bd7211e4c94e2f18dea4b5a694c174358cfbeaa Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Tue, 13 Jul 2021 18:58:46 -0400 Subject: [PATCH 107/191] cypress-tests: trying more tricks --- .github/workflows/cypress-tests.yaml | 2 +- package.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cypress-tests.yaml b/.github/workflows/cypress-tests.yaml index 9e79350a7..406150bc3 100644 --- a/.github/workflows/cypress-tests.yaml +++ b/.github/workflows/cypress-tests.yaml @@ -44,7 +44,7 @@ jobs: PHONE_INVENTORY: 1 with: browser: chrome - build: npm install --only=dev && npm run prod-build + build: npm run prod-build-forcypress start: npm start wait-on: 'http://localhost:3001' - uses: actions/upload-artifact@v2 diff --git a/package.json b/package.json index a8285caa0..2a4c4fe0b 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "prod-build-client": "webpack --config ./webpack/config.js", "prod-maybe-build-client": "if [ \"$ASSETS_DIR_PREBUILT\" != \"\" ] ; then npm run install-prebuilt-assets; else npm run prod-build-client; fi", "prod-build-server": "babel ./src -d ./build/server/ --source-maps --copy-files --ignore \"src/components,src/containers,src/client,src/styles,./src/routes.jsx\"; babel ./migrations -d ./build/server/migrations/ --source-maps --copy-files", + "prod-build-forcypress": "npm install --only=dev && npm run prod-build", "prod-build": "npm run clean && npm run prod-build-client && npm run prod-build-server", "prod-static-upload": "if [ \"$S3_STATIC_PATH\" != \"\" ] ; then aws s3 sync --acl public-read ./build/client/assets/ $S3_STATIC_PATH ; fi", "postinstall": "if [ \"$NODE_ENV\" = production ] ; then npm run prod-build-server && npm run prod-maybe-build-client && npm run prod-static-upload && npm run install-config-file; npm run notify-slack; fi", From 3a9f4ddf63def7ac9c1e65043df5307ac03da633 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Tue, 13 Jul 2021 19:10:22 -0400 Subject: [PATCH 108/191] cypress tests: ok try a different hack --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2a4c4fe0b..a4e8a4458 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "prod-build-client": "webpack --config ./webpack/config.js", "prod-maybe-build-client": "if [ \"$ASSETS_DIR_PREBUILT\" != \"\" ] ; then npm run install-prebuilt-assets; else npm run prod-build-client; fi", "prod-build-server": "babel ./src -d ./build/server/ --source-maps --copy-files --ignore \"src/components,src/containers,src/client,src/styles,./src/routes.jsx\"; babel ./migrations -d ./build/server/migrations/ --source-maps --copy-files", - "prod-build-forcypress": "npm install --only=dev && npm run prod-build", + "prod-build-forcypress": "NODE_ENV=test yarn install && npm run prod-build", "prod-build": "npm run clean && npm run prod-build-client && npm run prod-build-server", "prod-static-upload": "if [ \"$S3_STATIC_PATH\" != \"\" ] ; then aws s3 sync --acl public-read ./build/client/assets/ $S3_STATIC_PATH ; fi", "postinstall": "if [ \"$NODE_ENV\" = production ] ; then npm run prod-build-server && npm run prod-maybe-build-client && npm run prod-static-upload && npm run install-config-file; npm run notify-slack; fi", From 1fcc5a345cac456a0743682d3015e822df1f5c82 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Wed, 14 Jul 2021 12:24:41 -0400 Subject: [PATCH 109/191] service-managers/per-campaign-messageservices: clean up some more updateService and some bandwidth fixes --- __test__/server/api/campaign/campaign.test.js | 5 ++- __test__/test_helpers.js | 44 +++++++++++++++++-- package.json | 4 +- .../service-managers/numpicker-basic/index.js | 1 - .../per-campaign-messageservices/index.js | 15 ++++++- .../react-component.js | 11 +---- .../service-vendors/bandwidth/messaging.js | 6 +++ .../bandwidth/react-component.js | 24 ++++------ .../bandwidth/setup-and-numbers.js | 13 ++++-- src/server/api/schema.js | 3 +- yarn.lock | 44 +++++++++++++------ 11 files changed, 119 insertions(+), 51 deletions(-) diff --git a/__test__/server/api/campaign/campaign.test.js b/__test__/server/api/campaign/campaign.test.js index 1bd458e9f..992fcb8ec 100644 --- a/__test__/server/api/campaign/campaign.test.js +++ b/__test__/server/api/campaign/campaign.test.js @@ -1287,8 +1287,9 @@ describe("per-campaign phone numbers", async () => { beforeAll(() => { process.env = { ...process.env, - EXPERIMENTAL_PHONE_INVENTORY: "1", - EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS: "1" + PHONE_INVENTORY: "1", + EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS: "1", + SERVICE_MANAGERS: "per-campaign-messageservices" }; }); diff --git a/__test__/test_helpers.js b/__test__/test_helpers.js index 7da6f9c52..511dd9233 100644 --- a/__test__/test_helpers.js +++ b/__test__/test_helpers.js @@ -323,6 +323,7 @@ export async function saveCampaign( const rootValue = {}; const description = "test description"; const organizationId = campaign.organizationId; + const campaignId = campaign.id; const context = getContext({ user }); const campaignQuery = `mutation editCampaign($campaignId: String!, $campaign: CampaignInput!) { @@ -337,11 +338,9 @@ export async function saveCampaign( campaign: { title, description, - organizationId, - useOwnMessagingService, - inventoryPhoneNumberCounts + organizationId }, - campaignId: campaign.id + campaignId }; const result = await graphql( mySchema, @@ -353,6 +352,43 @@ export async function saveCampaign( if (result.errors) { throw new Error("Create campaign failed " + JSON.stringify(result)); } + if (useOwnMessagingService !== "false" || inventoryPhoneNumberCounts) { + const serviceManagerQuery = `mutation updateServiceManager( + $organizationId: String! + $campaignId: String! + $serviceManagerName: String! + $updateData: JSON! + ) { + updateServiceManager( + organizationId: $organizationId + campaignId: $campaignId + serviceManagerName: $serviceManagerName + updateData: $updateData + ) { + id + name + data + fullyConfigured + } + }`; + const managerResult = await graphql( + mySchema, + serviceManagerQuery, + rootValue, + context, + { + organizationId, + serviceManagerName: "per-campaign-messageservices", + updateData: { + useOwnMessagingService, + inventoryPhoneNumberCounts + }, + campaignId + } + ); + console.log("managerResult", JSON.stringify(managerResult)); + } + return result.data.editCampaign; } diff --git a/package.json b/package.json index a8285caa0..6a64cb502 100644 --- a/package.json +++ b/package.json @@ -80,10 +80,10 @@ "homepage": "https://github.com/MoveOnOrg/Spoke/#readme", "dependencies": { "@aoberoi/passport-slack": "^1.0.5", - "@date-io/core": "^1.3.13", - "@date-io/date-fns": "^1.3.13", "@bandwidth/messaging": "^3.0.0", "@bandwidth/numbers": "^1.7.0", + "@date-io/core": "^1.3.13", + "@date-io/date-fns": "^1.3.13", "@trt2/gsm-charset-utils": "^1.0.13", "aphrodite": "^2.3.1", "apollo-cache-inmemory": "^1.6.6", diff --git a/src/extensions/service-managers/numpicker-basic/index.js b/src/extensions/service-managers/numpicker-basic/index.js index 7e5cc8533..2068a2dc6 100644 --- a/src/extensions/service-managers/numpicker-basic/index.js +++ b/src/extensions/service-managers/numpicker-basic/index.js @@ -55,7 +55,6 @@ export async function onMessageSend({ if (selectedPhone && selectedPhone.phone_number) { return { user_number: selectedPhone.phone_number }; } else { - // TODO: what should we do if there's no result? console.log( "numpicker-basic.onMessageSend none found", serviceName, diff --git a/src/extensions/service-managers/per-campaign-messageservices/index.js b/src/extensions/service-managers/per-campaign-messageservices/index.js index ccc9b04b7..96df9bc82 100644 --- a/src/extensions/service-managers/per-campaign-messageservices/index.js +++ b/src/extensions/service-managers/per-campaign-messageservices/index.js @@ -155,6 +155,18 @@ export async function onCampaignUpdateSignal({ await accessRequired(user, campaign.organization_id, "ADMIN"); const serviceName = getServiceNameFromOrganization(organization); + if (updateData.useOwnMessagingService) { + campaign.use_own_messaging_service = Boolean( + updateData.useOwnMessagingService && + updateData.useOwnMessagingService !== "false" + ); + await r + .knex("campaign") + .where("id", campaign.id) + .update("use_own_messaging_service", campaign.use_own_messaging_service); + await cacheableData.campaign.clear(campaign.id); + } + if (updateData.releaseCampaignNumbers) { if (!campaign.use_own_messaging_service) { throw new Error( @@ -213,7 +225,7 @@ export async function onCampaignUpdateSignal({ } }); } - + console.log("per-campaign-messageservices.udpateCampaignSignal", campaign); return await _editCampaignData(organization, campaign); } @@ -275,6 +287,7 @@ async function prepareTwilioCampaign(campaign, organization, trx) { } async function onCampaignStart({ organization, campaign, user }) { + console.log("per-campaign-messageservices.onCampaignStart", campaign.id); try { await r.knex.transaction(async trx => { const campaignTrx = await trx("campaign") diff --git a/src/extensions/service-managers/per-campaign-messageservices/react-component.js b/src/extensions/service-managers/per-campaign-messageservices/react-component.js index d735256b6..f93064586 100644 --- a/src/extensions/service-managers/per-campaign-messageservices/react-component.js +++ b/src/extensions/service-managers/per-campaign-messageservices/react-component.js @@ -30,6 +30,7 @@ import LoadingIndicator from "../../../components/LoadingIndicator"; import theme from "../../../styles/theme"; import CampaignMessagingServiceForm from "./react-component-campaignmessageservice"; +// TODO: replace with CampaignMessagingServiceForm when enabled on backend // import { dataTest } from "../lib/attributes"; @@ -79,18 +80,10 @@ const inlineStyles = { export class CampaignConfig extends React.Component { static propTypes = { - user: type.object, campaign: type.object, serviceManagerInfo: type.object, saveLabel: type.string, - onSubmit: type.func, - - formValues: type.object, - onChange: type.func, - phoneNumberCounts: type.array, - inventoryCounts: type.array, - isStarted: type.bool, - contactsAreaCodeCounts: type.array + onSubmit: type.func }; constructor(props) { diff --git a/src/extensions/service-vendors/bandwidth/messaging.js b/src/extensions/service-vendors/bandwidth/messaging.js index d78c15640..5c915bca8 100644 --- a/src/extensions/service-vendors/bandwidth/messaging.js +++ b/src/extensions/service-vendors/bandwidth/messaging.js @@ -73,6 +73,12 @@ export async function sendMessage({ message.contact_number ); + if (!userNumber) { + throw new Error( + "Bandwidth service-vendor requires a user_number. Make sure to install a numpicker service-manager" + ); + } + const changes = { send_status: "SENT", service: "bandwidth", diff --git a/src/extensions/service-vendors/bandwidth/react-component.js b/src/extensions/service-vendors/bandwidth/react-component.js index 82da81206..f12820b73 100644 --- a/src/extensions/service-vendors/bandwidth/react-component.js +++ b/src/extensions/service-vendors/bandwidth/react-component.js @@ -30,6 +30,13 @@ import GSSubmitButton from "../../../components/forms/GSSubmitButton"; export class OrgConfig extends React.Component { constructor(props) { super(props); + this.state = { ...this.props.config, country: "United States" }; + const allSet = this.isAllSet(); + this.props.onAllSetChanged(allSet); + console.log("constructor"); + } + + isAllSet() { const { userName, password, @@ -38,12 +45,9 @@ export class OrgConfig extends React.Component { sipPeerId, applicationId } = this.props.config; - const allSet = Boolean( + return Boolean( userName && password && accountId && siteId && sipPeerId && applicationId ); - this.state = { ...this.props.config, country: "United States" }; - this.props.onAllSetChanged(allSet); - console.log("constructor"); } UNSAFE_componentWillReceiveProps(nextProps) { @@ -109,17 +113,7 @@ export class OrgConfig extends React.Component { render() { const { organizationId, inlineStyles, styles, config } = this.props; - const { - userName, - password, - accountId, - siteId, - sipPeerId, - applicationId - } = config; - const allSet = Boolean( - userName && password && accountId && siteId && sipPeerId && applicationId - ); + const allSet = this.isAllSet(); let baseUrl = "http://base"; if (typeof window !== "undefined") { baseUrl = window.location.origin; diff --git a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js index eb917a7ce..4212add7f 100644 --- a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js +++ b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js @@ -81,23 +81,30 @@ export const getServiceConfig = async ( } } // FUTURE: should it be possible for a universal setting? maybe + const serviceManagers = getConfig("SERVICE_MANAGERS", organization) || ""; return { ...serviceConfig, - password + password, + serviceManagerNumPicker: /numpicker/.test(serviceManagers), + serviceManagerSticky: /sticky-sender/.test(serviceManagers) }; }; export async function fullyConfigured(organization) { const config = await getMessageServiceConfig("bandwidth", organization); + const serviceManagers = getConfig("SERVICE_MANAGERS", organization) || ""; if ( !config.password || !config.userName || !config.accountId || - !config.applicationId + !config.applicationId || + // serviceManagers tests purposefully avoid looking for delimeters, + // so different numpicker/sticky modules can be used + !/numpicker/.test(serviceManagers) || + !/sticky-sender/.test(serviceManagers) ) { return false; } - // TODO: also needs some number to send with numpicker AND sticky-sender in servicemanagers return true; } diff --git a/src/server/api/schema.js b/src/server/api/schema.js index c918793b2..6cc1dbdd6 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -188,7 +188,8 @@ async function editCampaign(id, campaign, loaders, user, origCampaignRecord) { textingHoursEnforced, textingHoursStart, textingHoursEnd, - timezone + timezone, + serviceManagers } = campaign; // some changes require ADMIN and we recheck below const organizationId = diff --git a/yarn.lock b/yarn.lock index 9333bb0bc..82bfa2559 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7770,11 +7770,16 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2: +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== +graceful-fs@^4.1.2: + version "4.2.6" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== + graphql-date@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/graphql-date/-/graphql-date-1.0.3.tgz#31ce05ae40ed8c8ceb040364060109771e712e91" @@ -8056,9 +8061,9 @@ hoopy@^0.1.2: integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== hosted-git-info@^2.1.4: - version "2.8.8" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" - integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== hpack.js@^2.1.6: version "2.1.6" @@ -8769,10 +8774,10 @@ is-color-stop@^1.0.0: rgb-regex "^1.0.1" rgba-regex "^1.0.0" -is-core-module@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" - integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== +is-core-module@^2.1.0, is-core-module@^2.2.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1" + integrity sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A== dependencies: has "^1.0.3" @@ -12198,11 +12203,16 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.5, path-parse@^1.0.6: +path-parse@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== +path-parse@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + path-proxy@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/path-proxy/-/path-proxy-1.0.0.tgz#18e8a36859fc9d2f1a53b48dee138543c020de5e" @@ -14347,7 +14357,7 @@ resolve@1.10.0: dependencies: path-parse "^1.0.6" -resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.13.1, resolve@^1.3.2, resolve@^1.6.0, resolve@^1.8.1, resolve@^1.9.0: +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.13.1, resolve@^1.3.2, resolve@^1.6.0, resolve@^1.8.1, resolve@^1.9.0: version "1.19.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== @@ -14355,6 +14365,14 @@ resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.13.1, resolve@^1.3.2 is-core-module "^2.1.0" path-parse "^1.0.6" +resolve@^1.10.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + responselike@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" @@ -15075,9 +15093,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.7" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65" - integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ== + version "3.0.9" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz#8a595135def9592bda69709474f1cbeea7c2467f" + integrity sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ== spdy-transport@^3.0.0: version "3.0.0" From b7171f0ece9331b22719579c093f49a46f8be3a6 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Fri, 16 Jul 2021 15:13:49 -0400 Subject: [PATCH 110/191] fix us-area-codes import error -- seems based on importing .json vs. .js --- .../service-managers/per-campaign-messageservices/index.js | 4 ++-- src/server/api/campaign.js | 5 ++--- src/server/api/lib/owned-phone-number.js | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/extensions/service-managers/per-campaign-messageservices/index.js b/src/extensions/service-managers/per-campaign-messageservices/index.js index 8281d93cc..b387c6f81 100644 --- a/src/extensions/service-managers/per-campaign-messageservices/index.js +++ b/src/extensions/service-managers/per-campaign-messageservices/index.js @@ -19,7 +19,7 @@ import { getConfig } from "../../../server/api/lib/config"; import { getServiceNameFromOrganization } from "../../service-vendors"; import * as twilio from "../../service-vendors/twilio"; import { camelizeKeys } from "humps"; -// import usAreaCodes from "us-area-codes"; +import usAreaCodes from "us-area-codes/data/codes.json"; export const name = "per-campaign-messageservices"; @@ -72,7 +72,7 @@ const _editCampaignData = async (organization, campaign) => { const contactsAreaCodeCounts = areaCodes.map(data => ({ areaCode: data.area_code, - // state: usAreaCodes.get(Number(data.area_code)), + state: usAreaCodes[data.area_code] || "N/A", count: parseInt(data.count, 10) })); // 2. phoneNumberCounts (for organization) diff --git a/src/server/api/campaign.js b/src/server/api/campaign.js index 307fe96a6..ce6bb1306 100644 --- a/src/server/api/campaign.js +++ b/src/server/api/campaign.js @@ -17,6 +17,7 @@ import { getConfig, getFeatures } from "./lib/config"; import ownedPhoneNumber from "./lib/owned-phone-number"; const title = 'lower("campaign"."title")'; import { camelizeKeys } from "humps"; +import usAreaCodes from "us-area-codes/data/codes.json"; export function addCampaignsFilterToQuery( queryParam, @@ -594,8 +595,6 @@ export const resolvers = { "SUPERVOLUNTEER", true ); - - const usAreaCodes = require("us-area-codes"); const areaCodes = await r .knex("campaign_contact") .select( @@ -609,7 +608,7 @@ export const resolvers = { return areaCodes.map(data => ({ areaCode: data.area_code, - state: usAreaCodes.get(Number(data.area_code)), + state: usAreaCodes[data.area_code] || "N/A", count: parseInt(data.count, 10) })); }, diff --git a/src/server/api/lib/owned-phone-number.js b/src/server/api/lib/owned-phone-number.js index 41de93d62..2edc5b80a 100644 --- a/src/server/api/lib/owned-phone-number.js +++ b/src/server/api/lib/owned-phone-number.js @@ -1,5 +1,6 @@ import { r } from "../../models"; import { getConfig } from "./config"; +import usAreaCodes from "us-area-codes/data/codes.json"; async function allocateCampaignNumbers( { organizationId, campaignId, areaCode, amount }, @@ -57,7 +58,6 @@ async function listCampaignNumbers(campaignId) { } async function listOrganizationCounts(organization) { - const usAreaCodes = require("us-area-codes"); const service = getConfig("service", organization) || getConfig("DEFAULT_SERVICE", organization); @@ -77,7 +77,7 @@ async function listOrganizationCounts(organization) { .groupBy("area_code"); return counts.map(row => ({ areaCode: row.area_code, - state: usAreaCodes.get(Number(row.area_code)), + state: usAreaCodes[row.area_code] || "N/A", allocatedCount: Number(row.allocated_count), availableCount: Number(row.available_count) })); From 9c68190e4ab281eee34878ddca2c57631c238c2f Mon Sep 17 00:00:00 2001 From: Stefan Hayden Date: Sat, 17 Jul 2021 15:28:18 -0400 Subject: [PATCH 111/191] Fix for people and campaign filters --- src/components/IncomingMessageFilter.jsx | 4 +++- src/components/PeopleList/SimpleRolesDropdown.jsx | 2 +- src/components/SelectedCampaigns.jsx | 4 ++-- src/containers/AdminPersonList.jsx | 12 ++++++------ 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/components/IncomingMessageFilter.jsx b/src/components/IncomingMessageFilter.jsx index f9c08d2ce..d9c3b81cd 100644 --- a/src/components/IncomingMessageFilter.jsx +++ b/src/components/IncomingMessageFilter.jsx @@ -178,7 +178,9 @@ class IncomingMessageFilter extends Component { campaign.key !== ALL_CAMPAIGNS; selectedCampaignIds = selectedCampaigns => - selectedCampaigns.map(campaign => parseInt(campaign.key, 10)); + selectedCampaigns.map(campaign => + parseInt(campaign.key || campaign.rawValue, 10) + ); campaignsNotAlreadySelected = campaign => { return !this.selectedCampaignIds(this.state.selectedCampaigns).includes( diff --git a/src/components/PeopleList/SimpleRolesDropdown.jsx b/src/components/PeopleList/SimpleRolesDropdown.jsx index f384069de..6c039f5e3 100644 --- a/src/components/PeopleList/SimpleRolesDropdown.jsx +++ b/src/components/PeopleList/SimpleRolesDropdown.jsx @@ -11,7 +11,7 @@ export const ALL_ROLES = "ALL ROLES"; const SimpleRolesDropdown = props => ( Date: Tue, 10 Aug 2021 14:14:47 -0400 Subject: [PATCH 142/191] fix accessRequired to allow supervols to reassign contacts --- src/server/api/schema.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/server/api/schema.js b/src/server/api/schema.js index d74cb5f43..da1290355 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -1283,7 +1283,12 @@ const rootMutations = { { user } ) => { // verify permissions - await accessRequired(user, organizationId, "ADMIN", /* superadmin*/ true); + await accessRequired( + user, + organizationId, + "SUPERVOLUNTEER", + /* superadmin*/ true + ); // group contactIds by campaign // group messages by campaign @@ -1316,7 +1321,12 @@ const rootMutations = { { user } ) => { // verify permissions - await accessRequired(user, organizationId, "ADMIN", /* superadmin*/ true); + await accessRequired( + user, + organizationId, + "SUPERVOLUNTEER", + /* superadmin*/ true + ); const { campaignIdContactIdsMap } = await getCampaignIdContactIdsMaps( organizationId, { From e0f9fc4301f5efdd30e9cb135f8d4b5edbb092d9 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Tue, 10 Aug 2021 15:16:55 -0400 Subject: [PATCH 143/191] fix MessageList which now has state --- src/components/AssignmentTexter/Demo.jsx | 9 +- .../AssignmentTexter/MessageList.jsx | 212 +++++++++--------- 2 files changed, 115 insertions(+), 106 deletions(-) diff --git a/src/components/AssignmentTexter/Demo.jsx b/src/components/AssignmentTexter/Demo.jsx index 4d674ef78..3aad3b43c 100644 --- a/src/components/AssignmentTexter/Demo.jsx +++ b/src/components/AssignmentTexter/Demo.jsx @@ -453,7 +453,14 @@ export const tests = testName => { id: "fake2", text: "Yes! We need to help save the world.", isFromContact: true, - createdAt: new Date(Number(new Date()) - 142 * 60 * 1000) + createdAt: new Date(Number(new Date()) - 142 * 60 * 1000), + media: [ + { + type: "image/png", + url: + "https://s3-us-west-1.amazonaws.com/spoke-public/spoke_logo.svg?demo" + } + ] }, { id: "fake3", diff --git a/src/components/AssignmentTexter/MessageList.jsx b/src/components/AssignmentTexter/MessageList.jsx index bba573674..85c052d82 100644 --- a/src/components/AssignmentTexter/MessageList.jsx +++ b/src/components/AssignmentTexter/MessageList.jsx @@ -69,108 +69,98 @@ function SecondaryText(props) { ); } -const MessageList = function MessageList(props) { - const { - contact, - styles, - review, - currentUser, - organizationId, - hideMedia - } = props; - const { optOut, messages } = contact; +export class MessageList extends React.Component { + state = { + expanded: false + }; - const received = (styles && styles.messageReceived) || defaultStyles.received; - const sent = (styles && styles.messageSent) || defaultStyles.sent; - const listStyle = (styles && styles.messageList) || {}; - - const optOutItem = optOut && ( -
- - - - - - - -
- ); + optOutItem = optOut => + !optOut ? null : ( +
+ + + + + + + +
+ ); - const renderMsg = message => ( -
-
{message.text}
- {!hideMedia && - message.media && - message.media.map(media => { - let type, icon, embed, subtitle; - if (media.type.startsWith("image")) { - type = "Image"; - icon = ; - embed = Media; - } else if (media.type.startsWith("video")) { - type = "Video"; - icon = ; - embed = ( - - ); - } else if (media.type.startsWith("audio")) { - type = "Audio"; - icon = ; - embed = ( - + renderMsg(message) { + const { hideMedia } = this.props; + return ( +
+
{message.text}
+ {!hideMedia && + message.media && + message.media.map(media => { + let type, icon, embed, subtitle; + if (media.type.startsWith("image")) { + type = "Image"; + icon = ; + embed = Media; + } else if (media.type.startsWith("video")) { + type = "Video"; + icon = ; + embed = ( + + ); + } else if (media.type.startsWith("audio")) { + type = "Audio"; + icon = ; + embed = ( + + ); + } else { + type = "Unsupprted media"; + icon = ; + subtitle = `Type: ${media.type}`; + } + return ( + + {icon}} + onClick={() => { + this.setState({ expanded: !this.state.expanded }); + }} + /> + + {embed && {embed}} + + ); - } else { - type = "Unsupprted media"; - icon = ; - subtitle = `Type: ${media.type}`; - } - return ( - - {icon}} - onClick={() => { - this.setState({ expanded: !this.state.expanded }); - }} - /> - - {embed && {embed}} - - - ); - })} -
- ); + })} +
+ ); + } - return ( - - {messages.map(message => ( - - } - > - + {messages.map(message => ( + } - /> - - ))} - {optOutItem} - - ); -}; + > + + } + /> + + ))} + {this.optOutItem(optOut)} + + ); + } +} MessageList.propTypes = { contact: PropTypes.object, From a9f8ae0d516a5482822f3e1e6c06c6b8ca03010f Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Tue, 10 Aug 2021 15:47:57 -0400 Subject: [PATCH 144/191] remove 0s -- need to go back to ternary pattern for some of these --- src/components/AssignmentSummary.jsx | 2 +- .../scrub-bad-mobilenums/react-component.js | 22 +++++++++---------- .../react-component.js | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/AssignmentSummary.jsx b/src/components/AssignmentSummary.jsx index ffa41bd98..3aff8f5f0 100644 --- a/src/components/AssignmentSummary.jsx +++ b/src/components/AssignmentSummary.jsx @@ -121,7 +121,7 @@ export class AssignmentSummary extends Component { const sideboxProps = { assignment, campaign, texter, settingsData }; const enabledSideboxes = getSideboxes(sideboxProps, "TexterTodoList"); // if there's a sidebox marked popup, then we will only show that sidebox and little else - const hasPopupSidebox = enabledSideboxes.popups.length; + const hasPopupSidebox = Boolean(enabledSideboxes.popups.length); const sideboxList = enabledSideboxes .filter(sb => hasPopupSidebox ? sb.name === enabledSideboxes.popups[0] : true diff --git a/src/extensions/service-managers/scrub-bad-mobilenums/react-component.js b/src/extensions/service-managers/scrub-bad-mobilenums/react-component.js index dd925eb3b..0200896f8 100644 --- a/src/extensions/service-managers/scrub-bad-mobilenums/react-component.js +++ b/src/extensions/service-managers/scrub-bad-mobilenums/react-component.js @@ -57,7 +57,7 @@ export class CampaignConfig extends React.Component { let scrubState = null; if (isStarted) { scrubState = states.F_CAMPAIGN_STARTED; - } else if (scrubBadMobileNumsFinished) { + } else if (scrubBadMobileNumsFinished || scrubBadMobileNumsCount === 0) { scrubState = states.E_PROCESS_COMPLETE; } else if (scrubJobs.length) { scrubState = states.D_PROCESSING; @@ -100,12 +100,12 @@ export class CampaignConfig extends React.Component { before starting the campaign. If you upload a new set of numbers, we’ll have to look them up again?!

- {scrubBadMobileNumsCount && ( + {scrubBadMobileNumsCount ? (

You will need to lookup {scrubBadMobileNumsCount} numbers (having leveraged past lookups).

- )} + ) : null}
)} {scrubState === states.D_PROCESSING && ( @@ -117,31 +117,31 @@ export class CampaignConfig extends React.Component {

)} {scrubState === states.E_PROCESS_COMPLETE &&

Lookups complete!

} - {scrubBadMobileNumsFinishedCount && ( + {scrubBadMobileNumsFinishedCount ? (

{scrubBadMobileNumsFinishedCount} numbers were looked up for this campaign.

- )} - {scrubBadMobileNumsFinishedDeleteCount && ( + ) : null} + {scrubBadMobileNumsFinishedDeleteCount ? (

{scrubBadMobileNumsFinishedDeleteCount} landlines were deleted from the campaign.

- )} - {scrubBadMobileNumsDeletedOnUpload && ( + ) : null} + {scrubBadMobileNumsDeletedOnUpload ? (

{scrubBadMobileNumsDeletedOnUpload} landlines were removed during campaign upload based on saved lookup data.

- )} + ) : null} - {scrubState === states.C_NEEDS_RUN && ( + {scrubState === states.C_NEEDS_RUN ? ( - )} + ) : null} ); } diff --git a/src/extensions/texter-sideboxes/default-dynamicassignment/react-component.js b/src/extensions/texter-sideboxes/default-dynamicassignment/react-component.js index edf071cd9..e4e847407 100644 --- a/src/extensions/texter-sideboxes/default-dynamicassignment/react-component.js +++ b/src/extensions/texter-sideboxes/default-dynamicassignment/react-component.js @@ -110,7 +110,7 @@ export class TexterSideboxClass extends React.Component { const headerStyle = messageStatusFilter ? { textAlign: "center" } : {}; return (
- {assignment.hasUnassignedContactsForTexter && ( + {assignment.hasUnassignedContactsForTexter ? (

{nextBatchMessage}

- )} + ) : null} {messageStatusFilter === "needsMessage" && assignment.unrepliedCount && (
From c59eaf7504a6198039fada9c0e0db844debb21d0 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Tue, 10 Aug 2021 16:53:48 -0400 Subject: [PATCH 145/191] correct multiLine => multiline to match new materialui attr --- docs/HOWTO_USE_POSTGRESQL.md | 1 + src/components/AssignmentTexter/Controls.jsx | 2 +- src/components/AssignmentTexter/Demo.jsx | 2 +- src/components/AssignmentTexter/OldControls.jsx | 4 ++-- src/components/CampaignBasicsForm.jsx | 2 +- src/components/CampaignCannedResponseForm.jsx | 2 +- src/components/CampaignInteractionStepsForm.jsx | 2 +- src/components/CannedResponseForm.jsx | 2 +- src/components/IncomingMessageList/MessageResponse.jsx | 2 +- .../texter-sideboxes/texter-feedback/react-component.js | 2 +- 10 files changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/HOWTO_USE_POSTGRESQL.md b/docs/HOWTO_USE_POSTGRESQL.md index 49248eb59..92cd1f9e7 100644 --- a/docs/HOWTO_USE_POSTGRESQL.md +++ b/docs/HOWTO_USE_POSTGRESQL.md @@ -5,6 +5,7 @@ To use Postgresql, follow these steps: 1. Either install docker (recommended) or postgresql on your machine: * If you installed docker run the database using: `docker-compose up` * If you installed postgres locally, create the spoke dev database: `psql -c "create database spokedev;"` + * Then create a spoke user to connect to the database with `createuser -P spoke` with password "spoke" (to match the credentials in the .env.example file) 1. In `.env` set `DB_TYPE=pg`. (Otherwise, you will use sqlite.) 2. Set `DB_PORT=5432`, which is the default port for Postgres. diff --git a/src/components/AssignmentTexter/Controls.jsx b/src/components/AssignmentTexter/Controls.jsx index b6bdcf4a7..9b17c8af0 100644 --- a/src/components/AssignmentTexter/Controls.jsx +++ b/src/components/AssignmentTexter/Controls.jsx @@ -691,7 +691,7 @@ export class AssignmentTexterContactControls extends React.Component { onBlur={() => { this.setState({ messageFocus: false }); }} - multiLine + multiline fullWidth rowsMax={6} /> diff --git a/src/components/AssignmentTexter/Demo.jsx b/src/components/AssignmentTexter/Demo.jsx index 3aad3b43c..2f4173b25 100644 --- a/src/components/AssignmentTexter/Demo.jsx +++ b/src/components/AssignmentTexter/Demo.jsx @@ -57,7 +57,7 @@ export const tests = testName => { { id: "13", script: - "Hi {firstName}, it's {texterAliasOrFirstName} a volunteer with MoveOn. There is an election in Arizona coming Tuesday. Will you vote progressive?", + "Hi {firstName}, it's {texterAliasOrFirstName} a volunteer with MoveOn. There is an election in Arizona coming Tuesday. Will you vote progressive? STOP2quit", question: { text: "", answerOptions: [] } } ], diff --git a/src/components/AssignmentTexter/OldControls.jsx b/src/components/AssignmentTexter/OldControls.jsx index 8693ec33c..9d4069177 100644 --- a/src/components/AssignmentTexter/OldControls.jsx +++ b/src/components/AssignmentTexter/OldControls.jsx @@ -527,7 +527,7 @@ export class AssignmentTexterContactControls extends React.Component { name="optOutMessageText" fullWidth autoFocus - multiLine + multiline />
) : null} - {messageStatusFilter === "needsMessage" && assignment.unrepliedCount && ( + {messageStatusFilter === "needsMessage" && assignment.unrepliedCount ? (
- )} + ) : null} {messageStatusFilter && - messageStatusFilter !== "needsMessage" && - assignment.unmessagedCount && ( -
- - - -
- )} - {contact /*the empty list*/ && ( + messageStatusFilter !== "needsMessage" && + assignment.unmessagedCount ? ( +
+ + + +
+ ) : null} + {contact /*the empty list*/ ? (
- )} + ) : null} {!assignment.hasUnassignedContactsForTexter && - !contact && - !assignment.unmessagedCount && - !assignment.unrepliedCount && - settingsData.dynamicAssignmentNothingToDoMessage && ( - // assignment summary when there is nothing to do -
- {settingsData.dynamicAssignmentNothingToDoMessage} -
- )} + !contact && + !assignment.unmessagedCount && + !assignment.unrepliedCount && + settingsData.dynamicAssignmentNothingToDoMessage ? ( + // assignment summary when there is nothing to do +
+ {settingsData.dynamicAssignmentNothingToDoMessage} +
+ ) : null}
); } From 9b753eb412ff1f82ca794088d249e185dd0187ad Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Tue, 10 Aug 2021 17:28:28 -0400 Subject: [PATCH 147/191] fight the 0s! From dde0b6b4549d3d951700467b0d0a632bd9a8a016 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Tue, 10 Aug 2021 17:55:04 -0400 Subject: [PATCH 148/191] report DUPLICATE_MESSAGE on dupes rather than SENDERR_SAVEFAIL --- src/server/api/mutations/sendMessage.js | 3 ++- src/server/models/cacheable_queries/message.js | 11 +++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/server/api/mutations/sendMessage.js b/src/server/api/mutations/sendMessage.js index 9c1b95cc8..05e9dc5f5 100644 --- a/src/server/api/mutations/sendMessage.js +++ b/src/server/api/mutations/sendMessage.js @@ -203,8 +203,9 @@ export const sendMessage = async ( throw newError( `Message send error ${saveResult.texterError || saveResult.matchError || + saveResult.error || ""}`, - "SENDERR_SAVEFAIL" + saveResult.error || "SENDERR_SAVEFAIL" ); } contact.message_status = saveResult.contactStatus; diff --git a/src/server/models/cacheable_queries/message.js b/src/server/models/cacheable_queries/message.js index a14ef79c4..f2e0e77df 100644 --- a/src/server/models/cacheable_queries/message.js +++ b/src/server/models/cacheable_queries/message.js @@ -290,7 +290,10 @@ const messageCache = { m => m.text === messageToSave.text && m.is_from_contact === false ); if (duplicate) { - matchError = "DUPLICATE MESSAGE"; + matchError = + messages.length > 2 + ? "DUPLICATE_REPLY_MESSAGE" + : "DUPLICATE_MESSAGE"; } } } @@ -352,7 +355,11 @@ const messageCache = { } } if (matchError) { - return { error: matchError }; + return { + error: matchError, + campaignId, + contactId: messageToSave.campaign_contact_id + }; } const savedMessage = await Message.save( messageToSave, From 564090daf313e2e20a33712aa014a1713d5f06f3 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Wed, 11 Aug 2021 13:32:40 -0400 Subject: [PATCH 149/191] upgrade Heroku/Docker to 12.22, upgrade min node version to 10.17 to be min version supported by sqlite3 module, and accomodate weird h: old heroku redis urls in new library --- .nvmrc | 2 +- Dockerfile | 4 ++-- package.json | 5 +++-- src/server/models/thinky.js | 6 +++++- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.nvmrc b/.nvmrc index f599e28b8..48082f72f 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -10 +12 diff --git a/Dockerfile b/Dockerfile index 0e0cf4632..3145a2deb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -ARG BUILDER_IMAGE=node:10.15 -ARG RUNTIME_IMAGE=node:10.15-alpine +ARG BUILDER_IMAGE=node:12.22 +ARG RUNTIME_IMAGE=node:12.22-alpine ARG PHONE_NUMBER_COUNTRY=US FROM ${BUILDER_IMAGE} as builder diff --git a/package.json b/package.json index 25c786cd2..63dc3dca9 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Spoke", "main": "src/server", "engines": { - "node": ">=10.3", + "node": ">=10.17", "npm": "3.10.10" }, "scripts": { @@ -229,6 +229,7 @@ "react-formal": "2.2.2", "react-router": "^3.2.0", "react-tooltip": "^4.2.13", - "recompose": "^0.30.0" + "recompose": "^0.30.0", + "webpack-cli": "^4.7.2" } } diff --git a/src/server/models/thinky.js b/src/server/models/thinky.js index 054a2382c..bc8be519b 100644 --- a/src/server/models/thinky.js +++ b/src/server/models/thinky.js @@ -47,7 +47,11 @@ thinkyConn.r.getCountDistinct = async (query, distinctConstraint) => const redisUrl = process.env.REDIS_TLS_URL || process.env.REDIS_URL; if (redisUrl) { - const redisSettings = { url: redisUrl }; + // new redis client doesn't respect username placeholders so replace it + // this is especially true for legacy Heroku instances which had redis://h:... + const redisSettings = { + url: redisUrl.replace(/redis:\/\/\w+:/, "redis://:") + }; if (/rediss/.test(redisSettings.url)) { // secure redis protocol for Redis 6.0+ // https://devcenter.heroku.com/articles/securing-heroku-redis#using-node-js From 934da20354b09c18c46aebf7975299ba6d78d733 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Wed, 11 Aug 2021 14:21:01 -0400 Subject: [PATCH 150/191] fix cypress e2e tests, post-fixing multiline input => textarea --- __test__/cypress/integration/basic-campaign-e2e.test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/__test__/cypress/integration/basic-campaign-e2e.test.js b/__test__/cypress/integration/basic-campaign-e2e.test.js index 79978aec2..a81f85d27 100644 --- a/__test__/cypress/integration/basic-campaign-e2e.test.js +++ b/__test__/cypress/integration/basic-campaign-e2e.test.js @@ -84,7 +84,9 @@ describe("End-to-end campaign flow", () => { cy.wait(400); // Interaction Steps - cy.get("[data-test=editorInteraction] input").click(); + // the editorInteraction selector might seem overly precise + // -- the problem is that multiline fields have two textareas, one hidden + cy.get("[data-test=editorInteraction] textarea[name=script]").click(); cy.wait(400); cy.get(".DraftEditor-root").type( "Hi {{}firstName{}} this is {{}texterFirstName{}}, how are you?" @@ -93,7 +95,7 @@ describe("End-to-end campaign flow", () => { cy.get("[data-test=questionText] input").type("How are you?"); cy.get("button[data-test=addResponse]").click(); cy.get("[data-test=answerOption] input").type("Good"); - cy.get("[data-test=editorInteraction] input") + cy.get("[data-test=editorInteraction] textarea[name=script]") .eq(1) .click(); cy.get(".DraftEditor-root").type("Great!"); From 525cee047944b858ad2db834118bd84c326ebd79 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Wed, 11 Aug 2021 15:53:26 -0400 Subject: [PATCH 151/191] update defaults to include some more tested extensions --- src/extensions/dynamicassignment-batches/index.js | 2 +- src/extensions/message-handlers/index.js | 4 ++++ src/extensions/service-managers/index.js | 11 +++++++++++ src/extensions/texter-sideboxes/components.js | 4 +++- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/extensions/dynamicassignment-batches/index.js b/src/extensions/dynamicassignment-batches/index.js index bfb3887a7..7aaedb8bd 100644 --- a/src/extensions/dynamicassignment-batches/index.js +++ b/src/extensions/dynamicassignment-batches/index.js @@ -9,7 +9,7 @@ export const getDynamicAssignmentBatchPolicies = ({ const configuredHandlers = campaignEnabled || getConfig(handlerKey, organization) || - "finished-replies,vetted-texters"; + "finished-replies-tz,vetted-texters,finished-replies"; const enabledHandlers = (configuredHandlers && configuredHandlers.split(",")) || []; if (!campaignEnabled) { diff --git a/src/extensions/message-handlers/index.js b/src/extensions/message-handlers/index.js index a0f94e759..b9b7c0527 100644 --- a/src/extensions/message-handlers/index.js +++ b/src/extensions/message-handlers/index.js @@ -6,6 +6,10 @@ export function getMessageHandlers(organization) { const enabledHandlers = (configuredHandlers && configuredHandlers.split(",")) || []; + if (typeof configuredHandlers === "undefined") { + enabledHandlers.push("auto-optout", "outbound-unassign"); + } + const handlers = []; enabledHandlers.forEach(name => { try { diff --git a/src/extensions/service-managers/index.js b/src/extensions/service-managers/index.js index f6027a785..5b56a05c5 100644 --- a/src/extensions/service-managers/index.js +++ b/src/extensions/service-managers/index.js @@ -6,6 +6,17 @@ export function getServiceManagers(organization) { const enabledHandlers = (configuredHandlers && configuredHandlers.split(",")) || []; + if ( + typeof configuredHandlers === "undefined" && + getConfig( + "EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE", + organization, + { truthy: 1 } + ) + ) { + enabledHandlers.push("per-campaign-messageservices"); + } + const handlers = []; enabledHandlers.forEach(name => { try { diff --git a/src/extensions/texter-sideboxes/components.js b/src/extensions/texter-sideboxes/components.js index 0ec6a3c98..82feaf955 100644 --- a/src/extensions/texter-sideboxes/components.js +++ b/src/extensions/texter-sideboxes/components.js @@ -12,7 +12,9 @@ function getComponents() { "default-releasecontacts", "contact-reference", "tag-contact", - "default-editinitial" + "default-editinitial", + "take-conversations", + "texter-feedback" ]; const components = {}; enabledComponents.forEach(componentName => { From 6bf6c97bce3d8f94343b142139db3f78a708a53b Mon Sep 17 00:00:00 2001 From: Kathy Nguyen Date: Mon, 16 Aug 2021 07:18:19 -0700 Subject: [PATCH 152/191] Fix unarchiveable reduce function --- src/containers/AdminCampaignStats.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/AdminCampaignStats.jsx b/src/containers/AdminCampaignStats.jsx index 2cb3ef3e2..fed340818 100644 --- a/src/containers/AdminCampaignStats.jsx +++ b/src/containers/AdminCampaignStats.jsx @@ -277,7 +277,7 @@ class AdminCampaignStats extends React.Component { campaign.isArchivedPermanently || campaign.serviceManagers .map(sm => sm.unArchiveable) - .reduce((a, b) => a && b) + .reduce((a, b) => a || b, false) } onClick={async () => await this.props.mutations.unarchiveCampaign( From ca0e41fcfbf2d26bfed834d77dcbdf84c19cbdea Mon Sep 17 00:00:00 2001 From: Kathy Nguyen Date: Mon, 16 Aug 2021 07:51:14 -0700 Subject: [PATCH 153/191] Fix "false" next to texter name in Message Review --- src/components/IncomingMessageList/index.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/IncomingMessageList/index.jsx b/src/components/IncomingMessageList/index.jsx index 38cf839d1..ab4bdb1eb 100644 --- a/src/components/IncomingMessageList/index.jsx +++ b/src/components/IncomingMessageList/index.jsx @@ -136,8 +136,9 @@ export class IncomingMessageList extends Component { {value.id !== null ? ( {value.displayName + - (getHighestRole(value.roles) === "SUSPENDED" && - " (Suspended)")}{" "} + (getHighestRole(value.roles) === "SUSPENDED" + ? " (Suspended)" + : "")}{" "} Date: Mon, 16 Aug 2021 12:50:44 -0400 Subject: [PATCH 154/191] campaign admin: allow clicking canned responses to show full canned response --- src/components/AssignmentTexter/Controls.jsx | 6 +++- src/components/AssignmentTexter/Survey.jsx | 1 + .../CampaignCannedResponsesForm.jsx | 30 +++++++++++++++---- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/components/AssignmentTexter/Controls.jsx b/src/components/AssignmentTexter/Controls.jsx index 9b17c8af0..0f7b976a3 100644 --- a/src/components/AssignmentTexter/Controls.jsx +++ b/src/components/AssignmentTexter/Controls.jsx @@ -328,7 +328,11 @@ export class AssignmentTexterContactControls extends React.Component { const { questionResponses } = this.state; const { interactionSteps } = this.props.campaign; questionResponses[interactionStep.id] = questionResponseValue; - + console.log( + "handleQuestionResponseChange", + questionResponseValue, + nextScript + ); const children = getChildren(interactionStep, interactionSteps); for (const childStep of children) { if (childStep.id in questionResponses) { diff --git a/src/components/AssignmentTexter/Survey.jsx b/src/components/AssignmentTexter/Survey.jsx index a1373625e..7ec37cc04 100644 --- a/src/components/AssignmentTexter/Survey.jsx +++ b/src/components/AssignmentTexter/Survey.jsx @@ -143,6 +143,7 @@ class AssignmentTexterSurveys extends Component { } renderCurrentStepOldStyle(step) { + console.log("renderCurrentStepOldStyle", step); return this.renderStep(step, 1); } diff --git a/src/components/CampaignCannedResponsesForm.jsx b/src/components/CampaignCannedResponsesForm.jsx index 6dec88f01..ac3bd6830 100644 --- a/src/components/CampaignCannedResponsesForm.jsx +++ b/src/components/CampaignCannedResponsesForm.jsx @@ -50,9 +50,7 @@ const styles = StyleSheet.create({ marginBottom: 8, display: "-webkit-box", WebkitBoxOrient: "vertical", - WebkitLineClamp: 2, - overflow: "hidden", - height: 32 + width: "90%" } }); @@ -60,7 +58,8 @@ export class CampaignCannedResponsesForm extends React.Component { state = { showForm: false, formButtonText: "", - responseId: null + responseId: null, + showFullTextId: null }; formSchema = yup.object({ @@ -152,9 +151,28 @@ export class CampaignCannedResponsesForm extends React.Component { value={response.text} key={response.id} > - + + this.setState({ + showFullTextId: + this.state.showFullTextId === response.id ? null : response.id + }) + } + >
{response.title}
-
{response.text}
+
+ {response.text} +
{response.tagIds && response.tagIds.length > 0 && ( Date: Mon, 16 Aug 2021 13:56:18 -0400 Subject: [PATCH 155/191] job start_campaign: delete campaign job_request before starting campaign cache loading --- src/workers/jobs.js | 48 +++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/src/workers/jobs.js b/src/workers/jobs.js index baaf5ba82..eda143485 100644 --- a/src/workers/jobs.js +++ b/src/workers/jobs.js @@ -950,32 +950,42 @@ export async function startCampaign(job) { } ); - if (!(serviceManagerData && serviceManagerData.blockCampaignStart)) { - await r - .knex("campaign") - .where("id", campaign.id) - .update({ is_started: true }); - const reloadedCampaign = await cacheableData.campaign.load(campaign.id, { - forceLoad: true - }); - await sendUserNotification({ - type: Notifications.CAMPAIGN_STARTED, - campaignId: campaign.id - }); - // TODO: Decide if we want/need this anymore, relying on FUTURE campaign-contact cache load changes - // We are already in an background job process, so invoke the task directly rather than - // kicking it off through the dispatcher - await invokeTaskFunction(Tasks.CAMPAIGN_START_CACHE, { - organization, - campaign: reloadedCampaign - }); + if (serviceManagerData && serviceManagerData.blockCampaignStart) { + console.log( + "campaign blocked from starting", + campaign.id, + serviceManagerData.blockCampaignStart + ); + return; } + + await r + .knex("campaign") + .where("id", campaign.id) + .update({ is_started: true }); + const reloadedCampaign = await cacheableData.campaign.load(campaign.id, { + forceLoad: true + }); + await sendUserNotification({ + type: Notifications.CAMPAIGN_STARTED, + campaignId: campaign.id + }); + if (job.id) { await r .knex("job_request") .where("id", job.id) .delete(); } + + // We delete the job before invoking this task in case this process times out. + // TODO: Decide if we want/need this anymore, relying on FUTURE campaign-contact cache load changes + // We are already in an background job process, so invoke the task directly rather than + // kicking it off through the dispatcher + await invokeTaskFunction(Tasks.CAMPAIGN_START_CACHE, { + organization, + campaign: reloadedCampaign + }); } export async function importScript(job) { From a23874466d066a944298693bfd2e485dab0bf6b8 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Mon, 16 Aug 2021 14:02:38 -0400 Subject: [PATCH 156/191] job start_campaign: show start_campaign job in Basics in case it fails to start --- src/containers/AdminCampaignEdit.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/containers/AdminCampaignEdit.jsx b/src/containers/AdminCampaignEdit.jsx index 92d456786..b0193322b 100644 --- a/src/containers/AdminCampaignEdit.jsx +++ b/src/containers/AdminCampaignEdit.jsx @@ -666,7 +666,11 @@ export class AdminCampaignEdit extends React.Component { let jobMessage = null; let jobId = null; if (pendingJobs.length > 0) { - if (section.title === "Contacts") { + if (section.title === "Basics") { + relatedJob = pendingJobs.filter( + job => job.jobType === "start_campaign" + )[0]; + } else if (section.title === "Contacts") { relatedJob = pendingJobs.filter(job => job.jobType.startsWith("ingest") )[0]; From d0d19e077074d1de92f6de2122851aee4f18f6b9 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Mon, 16 Aug 2021 15:20:50 -0400 Subject: [PATCH 157/191] campaign_start_cache: dispatchTask instead of run in same job since it could take additional time --- src/workers/jobs.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/workers/jobs.js b/src/workers/jobs.js index eda143485..becfff312 100644 --- a/src/workers/jobs.js +++ b/src/workers/jobs.js @@ -30,6 +30,7 @@ import { sendEmail } from "../server/mail"; import { Notifications, sendUserNotification } from "../server/notifications"; import { getConfig } from "../server/api/lib/config"; import { invokeTaskFunction, Tasks } from "./tasks"; +import { jobRunner } from "../extensions/job-runners"; import fs from "fs"; import path from "path"; @@ -980,9 +981,7 @@ export async function startCampaign(job) { // We delete the job before invoking this task in case this process times out. // TODO: Decide if we want/need this anymore, relying on FUTURE campaign-contact cache load changes - // We are already in an background job process, so invoke the task directly rather than - // kicking it off through the dispatcher - await invokeTaskFunction(Tasks.CAMPAIGN_START_CACHE, { + await jobRunner.dispatchTask(Tasks.CAMPAIGN_START_CACHE, { organization, campaign: reloadedCampaign }); From acc88bf42aa7bacbaa8c6f75115ec4fc10751c01 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Mon, 16 Aug 2021 16:26:52 -0400 Subject: [PATCH 158/191] contact-reference sidebox: fix react errors --- .../contact-reference/react-component.js | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/src/extensions/texter-sideboxes/contact-reference/react-component.js b/src/extensions/texter-sideboxes/contact-reference/react-component.js index 982f52b59..eac13a7e1 100644 --- a/src/extensions/texter-sideboxes/contact-reference/react-component.js +++ b/src/extensions/texter-sideboxes/contact-reference/react-component.js @@ -45,37 +45,35 @@ export class TexterSidebox extends React.Component { const { host, protocol } = document.location; const url = `${protocol}//${host}/app/${campaign.organization.id}/todos/review/${this.props.contact.id}`; - - const textContent = [ - - - - - , - Get, - " a ", - settingsData.contactReferenceClickable ? ( - - conversation link - - ) : ( - "conversation link" - ), - this.state.copiedStatus - ]; return (
-
{textContent}
+
+ + + + + + Get + {" a "} + {settingsData.contactReferenceClickable ? ( + + conversation link + + ) : ( + "conversation link" + )} + {this.state.copiedStatus} +
); From cf92f7d24664ad1e2bf840c0cb8fc4f631b6a5ac Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Mon, 16 Aug 2021 16:35:14 -0400 Subject: [PATCH 159/191] texter survey: fix All questions => to update to the correct script --- src/components/AssignmentTexter/Controls.jsx | 13 +++--- src/components/AssignmentTexter/Survey.jsx | 42 +++++++++++++------- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/components/AssignmentTexter/Controls.jsx b/src/components/AssignmentTexter/Controls.jsx index 0f7b976a3..5234ee11b 100644 --- a/src/components/AssignmentTexter/Controls.jsx +++ b/src/components/AssignmentTexter/Controls.jsx @@ -331,7 +331,9 @@ export class AssignmentTexterContactControls extends React.Component { console.log( "handleQuestionResponseChange", questionResponseValue, - nextScript + nextScript, + "interactionStep", + interactionStep ); const children = getChildren(interactionStep, interactionSteps); for (const childStep of children) { @@ -464,10 +466,11 @@ export class AssignmentTexterContactControls extends React.Component { campaign.interactionSteps ); - const otherResponsesLink = currentInteractionStep && + const otherResponsesLink = + currentInteractionStep && currentInteractionStep.question.filteredAnswerOptions.length > 6 && - filteredCannedResponses.length && ( -
+ filteredCannedResponses.length ? ( + - ); + ) : null; const searchBar = currentInteractionStep && currentInteractionStep.question.answerOptions.length + diff --git a/src/components/AssignmentTexter/Survey.jsx b/src/components/AssignmentTexter/Survey.jsx index 7ec37cc04..0071ba864 100644 --- a/src/components/AssignmentTexter/Survey.jsx +++ b/src/components/AssignmentTexter/Survey.jsx @@ -75,11 +75,26 @@ class AssignmentTexterSurveys extends Component { }); }; - handleSelectChange = async (interactionStep, answerIndex, value) => { + handleSelectChange = async (interactionStep, answerIndexInput, value) => { const { onQuestionResponseChange } = this.props; let questionResponseValue = null; let nextScript = null; - + let answerIndex = answerIndexInput; + if (answerIndexInput === null) { + // need to find the answerIndex in the answerOptions + const answerOptions = + interactionStep.question && interactionStep.question.answerOptions; + if (answerOptions && answerOptions.length) { + answerOptions.find((ans, i) => { + if (ans.value === value) { + answerIndex = i; + return true; + } + }); + } else { + answerIndex = 0; // shouldn't be empty + } + } if (value !== "clearResponse") { questionResponseValue = value; nextScript = this.getNextScript({ interactionStep, answerIndex }); @@ -92,10 +107,10 @@ class AssignmentTexterSurveys extends Component { }); }; - renderAnswers(step, currentStep) { + renderAnswers(step, currentStepKey) { const menuItems = step.question.answerOptions.map(answerOption => ( )); - menuItems.push(); + menuItems.push(); menuItems.push( - + Clear response ); @@ -116,12 +131,12 @@ class AssignmentTexterSurveys extends Component { return menuItems; } - renderStep(step, currentStep) { + renderStep(step, currentStepKey) { const { questionResponses, currentInteractionStep } = this.props; const isCurrentStep = step.id === currentInteractionStep.id; const responseValue = questionResponses[step.id] || ""; const { question } = step; - const key = `topdiv${currentStep || 0}_${step.id}_${question.text}`; + const key = `topdiv${currentStepKey || 0}_${step.id}_${question.text}`; return ( question.text && ( - this.handleSelectChange(step, currentStep, event.target.value) + this.handleSelectChange(step, null, event.target.value) } key={key} name={question.id} value={responseValue} helperText="Choose answer" > - {this.renderAnswers(step, currentStep || 0)} + {this.renderAnswers(step, currentStepKey || "0")} ) ); } renderCurrentStepOldStyle(step) { - console.log("renderCurrentStepOldStyle", step); return this.renderStep(step, 1); } @@ -234,7 +248,7 @@ class AssignmentTexterSurveys extends Component { {showAllQuestions - ? interactionSteps.map(step => this.renderStep(step, 0)) + ? interactionSteps.map((step, i) => this.renderStep(step, i)) : this.renderCurrentStepOldStyle(currentInteractionStep)} @@ -251,9 +265,9 @@ class AssignmentTexterSurveys extends Component { onChange={(evt, expanded) => this.handleExpandChange(expanded)} > All questions - {interactionSteps.map(step => ( + {interactionSteps.map((step, i) => ( - {this.renderStep(step, 0)} + {this.renderStep(step, i)} ))} From 2cef5258b8f3ed8d75cab53c795d2ed84699f30c Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Mon, 16 Aug 2021 18:02:39 -0400 Subject: [PATCH 160/191] contact-reference sidebox: fix copyToClipboard --- .../contact-reference/react-component.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/extensions/texter-sideboxes/contact-reference/react-component.js b/src/extensions/texter-sideboxes/contact-reference/react-component.js index eac13a7e1..15f72b106 100644 --- a/src/extensions/texter-sideboxes/contact-reference/react-component.js +++ b/src/extensions/texter-sideboxes/contact-reference/react-component.js @@ -27,13 +27,18 @@ export const showSidebox = ({ }; export class TexterSidebox extends React.Component { - state = { - copiedStatus: "" - }; + constructor(props) { + super(props); + this.state = { + copiedStatus: "" + }; + this.displayLink = React.createRef(); + } copyToClipboard = () => { - if (this.refs.displayLink) { - this.refs.displayLink.focus(); + if (this.displayLink && this.displayLink.current) { + this.displayLink.current.focus(); + this.displayLink.current.select(); document.execCommand("copy"); console.log("Copied"); this.setState({ copiedStatus: " (copied)" }); @@ -69,11 +74,12 @@ export class TexterSidebox extends React.Component { {this.state.copiedStatus}
); From 916c35ee4c139497d0e72b6a2524e564d742baa3 Mon Sep 17 00:00:00 2001 From: Cody Gordon Date: Tue, 17 Aug 2021 07:25:40 -0700 Subject: [PATCH 161/191] proper value input for script import url --- src/containers/AdminScriptImport.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/AdminScriptImport.jsx b/src/containers/AdminScriptImport.jsx index 28a82a0c7..5b223990f 100644 --- a/src/containers/AdminScriptImport.jsx +++ b/src/containers/AdminScriptImport.jsx @@ -43,7 +43,7 @@ export default class AdminScriptImport extends Component { this.props.onSubmit(); }; - handleUrlChange = (_eventId, newValue) => this.setState({ url: newValue }); + handleUrlChange = ({ target }) => this.setState({ url: target.value }); renderErrors = () => this.state.error && ( From 44254eac23bb05b7e8f980ee83fcb812a1a42a38 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Tue, 17 Aug 2021 16:26:33 -0400 Subject: [PATCH 162/191] small style fixes --- src/components/AssignmentTexter/ContactController.jsx | 4 +++- src/components/AssignmentTexter/StyleControls.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/AssignmentTexter/ContactController.jsx b/src/components/AssignmentTexter/ContactController.jsx index b3b731c6d..3b60b55e1 100644 --- a/src/components/AssignmentTexter/ContactController.jsx +++ b/src/components/AssignmentTexter/ContactController.jsx @@ -475,7 +475,9 @@ export class ContactController extends React.Component { title={emptyMessage} icon={} content={ - + } /> {sideboxList} diff --git a/src/components/AssignmentTexter/StyleControls.js b/src/components/AssignmentTexter/StyleControls.js index ccfb5a6de..53cace50a 100644 --- a/src/components/AssignmentTexter/StyleControls.js +++ b/src/components/AssignmentTexter/StyleControls.js @@ -187,7 +187,7 @@ export const flexStyles = StyleSheet.create({ subSubAnswerButtonsColumns: { height: "0px", "@media(min-height: 600px)": { - height: "40px" // TODO + height: "37px" // TODO }, display: "inline-block", //flex: "1 1 50%", From 149c60422ab5e1bfab54505f3bf64650cea5e167 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Tue, 17 Aug 2021 17:54:11 -0400 Subject: [PATCH 163/191] get last 100 saved lists --- .../contact-loaders/ngpvan/ngpvan.test.js | 36 ++++++++++++++ .../contact-loaders/ngpvan/index.js | 49 ++++++++++++------- 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/__test__/extensions/contact-loaders/ngpvan/ngpvan.test.js b/__test__/extensions/contact-loaders/ngpvan/ngpvan.test.js index 74ad7ecb7..7e6540a29 100644 --- a/__test__/extensions/contact-loaders/ngpvan/ngpvan.test.js +++ b/__test__/extensions/contact-loaders/ngpvan/ngpvan.test.js @@ -164,6 +164,42 @@ describe("ngpvan", () => { getSavedListsNock.done(); }); + it("gets extra when more than 100 count", async () => { + const savedListsNock = nock(`${fakeNgpVanBaseApiUrl}:443`, { + encodedQueryParams: true, + reqheaders: { + authorization: "Basic c3Bva2U6dG9wc2VjcmV0fDA=" + } + }); + + const getSavedListsNock = savedListsNock + .get( + `/v4/savedLists?$top=100&maxPeopleCount=${process.env.NGP_VAN_MAXIMUM_LIST_SIZE}` + ) + .reply(200, { + items: listItems.slice(0, 4), + nextPageLink: null, + count: 101 + }); + + const getExtraSavedListsNock = savedListsNock + .get( + `/v4/savedLists?$top=100&maxPeopleCount=${process.env.NGP_VAN_MAXIMUM_LIST_SIZE}&$skip=1` + ) + .reply(200, { + items: listItems.slice(4), + nextPageLink: null, + count: 1 + }); + + const savedListsResponse = await getClientChoiceData(); + + expect(JSON.parse(savedListsResponse.data).items).toEqual(listItems); + expect(savedListsResponse.expiresSeconds).toEqual(30); + getSavedListsNock.done(); + getExtraSavedListsNock.done(); + }); + describe("when there is an error retrieving the list", () => { it("returns what we expect", async () => { const getSavedListsNock = nock(`${fakeNgpVanBaseApiUrl}:443`, { diff --git a/src/extensions/contact-loaders/ngpvan/index.js b/src/extensions/contact-loaders/ngpvan/index.js index 09a147a27..80f67d8eb 100644 --- a/src/extensions/contact-loaders/ngpvan/index.js +++ b/src/extensions/contact-loaders/ngpvan/index.js @@ -99,30 +99,43 @@ export function clientChoiceDataCacheKey(campaign, user) { return ""; } +async function requestVanSavedLists(organization, skip) { + const maxPeopleCount = + Number(getConfig("NGP_VAN_MAXIMUM_LIST_SIZE", organization)) || + DEFAULT_NGP_VAN_MAXIMUM_LIST_SIZE; + + const url = Van.makeUrl( + `v4/savedLists?$top=100&maxPeopleCount=${maxPeopleCount}${skip ? "&$skip=" + skip : ""}`, + organization + ); + + // The savedLists endpoint supports pagination; we are ignoring pagination now + const response = await HttpRequest(url, { + method: "GET", + headers: { + Authorization: Van.getAuth(organization) + }, + retries: 0, + timeout: Van.getNgpVanTimeout(organization) + }); + + return await response.json(); +} + export async function getClientChoiceData(organization, campaign, user) { let responseJson; try { - const maxPeopleCount = - Number(getConfig("NGP_VAN_MAXIMUM_LIST_SIZE", organization)) || - DEFAULT_NGP_VAN_MAXIMUM_LIST_SIZE; + responseJson = await requestVanSavedLists(organization); - const url = Van.makeUrl( - `v4/savedLists?$top=100&maxPeopleCount=${maxPeopleCount}`, - organization - ); + if (responseJson.count > 100) { + // Hack to get most recently created 100 saved lists + const mostRecentListsJson = await requestVanSavedLists(organization, responseJson.count - 100); - // The savedLists endpoint supports pagination; we are ignoring pagination now - const response = await HttpRequest(url, { - method: "GET", - headers: { - Authorization: Van.getAuth(organization) - }, - retries: 0, - timeout: Van.getNgpVanTimeout(organization) - }); - - responseJson = await response.json(); + if (mostRecentListsJson.items && mostRecentListsJson.items.length) { + responseJson.items = responseJson.items.concat(mostRecentListsJson.items); + } + } } catch (error) { const message = `Error retrieving saved list metadata from VAN ${error}`; // eslint-disable-next-line no-console From d0355e4d3d84f19228b1c1a8ce42f501a32bf356 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Tue, 17 Aug 2021 20:07:33 -0400 Subject: [PATCH 164/191] fix reference error and indicate opted-out contacts in firstMessage mode --- src/components/AssignmentTexter/Controls.jsx | 7 +++++-- src/components/AssignmentTexter/MessageList.jsx | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/AssignmentTexter/Controls.jsx b/src/components/AssignmentTexter/Controls.jsx index 5234ee11b..5bef5d095 100644 --- a/src/components/AssignmentTexter/Controls.jsx +++ b/src/components/AssignmentTexter/Controls.jsx @@ -1042,19 +1042,22 @@ export class AssignmentTexterContactControls extends React.Component { } renderFirstMessage(enabledSideboxes) { + const { contact } = this.props; return [ this.renderToolbar(enabledSideboxes), this.renderMessageBox( } />, enabledSideboxes ), this.renderMessagingRowMessage(), - this.renderMessagingRowSendSkip(this.props.contact) + this.renderMessagingRowSendSkip(contact) ]; } diff --git a/src/components/AssignmentTexter/MessageList.jsx b/src/components/AssignmentTexter/MessageList.jsx index 85c052d82..99d353201 100644 --- a/src/components/AssignmentTexter/MessageList.jsx +++ b/src/components/AssignmentTexter/MessageList.jsx @@ -83,7 +83,7 @@ export class MessageList extends React.Component { From 3f7945f66dfe880e7cb12a2973dc39d8510caae7 Mon Sep 17 00:00:00 2001 From: Cody Gordon Date: Tue, 17 Aug 2021 20:45:08 -0700 Subject: [PATCH 165/191] remove some UI logging --- src/components/AdminCampaignList/CampaignTable.jsx | 2 +- src/components/IncomingMessageList/index.jsx | 1 - src/components/TagsSelector.jsx | 3 --- src/containers/PeopleList.jsx | 2 +- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/AdminCampaignList/CampaignTable.jsx b/src/components/AdminCampaignList/CampaignTable.jsx index 5662d9bfa..b9edee516 100644 --- a/src/components/AdminCampaignList/CampaignTable.jsx +++ b/src/components/AdminCampaignList/CampaignTable.jsx @@ -404,7 +404,7 @@ export class CampaignTable extends React.Component { case "propsUpdate": break; default: - console.log(`action not handled: ${action}`); + break; } } }; diff --git a/src/components/IncomingMessageList/index.jsx b/src/components/IncomingMessageList/index.jsx index ab4bdb1eb..203d73f72 100644 --- a/src/components/IncomingMessageList/index.jsx +++ b/src/components/IncomingMessageList/index.jsx @@ -393,7 +393,6 @@ export class IncomingMessageList extends Component { this.handleRowsSelected(ids); break; default: - console.log(`action not handled: ${action}`); break; } } diff --git a/src/components/TagsSelector.jsx b/src/components/TagsSelector.jsx index 2ac812c53..f86db1b6c 100644 --- a/src/components/TagsSelector.jsx +++ b/src/components/TagsSelector.jsx @@ -87,7 +87,6 @@ export class TagsSelector extends React.Component { }; handleClick = itemClicked => { - console.log("handleClick", itemClicked); let tagFilter = this.state.tagFilter; switch (itemClicked.id) { case IGNORE_TAGS.id: @@ -116,13 +115,11 @@ export class TagsSelector extends React.Component { delete tagFilter.suppressedTags[`s_${itemClicked.id}`]; } } - console.log("SET STATE", tagFilter); this.setState({ tagFilter }); this.props.onChange(tagFilter); }; createMenuItem = tagFilter => { - console.log("createMenuItem", tagFilter.id); return ( Date: Wed, 18 Aug 2021 10:20:36 -0400 Subject: [PATCH 166/191] wip: debug cypress test on github --- __test__/cypress/integration/basic-campaign-e2e.test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/__test__/cypress/integration/basic-campaign-e2e.test.js b/__test__/cypress/integration/basic-campaign-e2e.test.js index a81f85d27..0c1e59b90 100644 --- a/__test__/cypress/integration/basic-campaign-e2e.test.js +++ b/__test__/cypress/integration/basic-campaign-e2e.test.js @@ -140,6 +140,10 @@ describe("End-to-end campaign flow", () => { /Hi ContactFirst(\d) this is TexterFirst, how are you\?/ ); }); + console.log( + "cypress before data-test=send", + cy.get("button[data-test=send]") + ); cy.get("button[data-test=send]") .eq(0) .click(); From 67b936ebea1c2bf3af03124be79e1d00aa388f8f Mon Sep 17 00:00:00 2001 From: Cody Gordon Date: Wed, 18 Aug 2021 12:54:48 -0700 Subject: [PATCH 167/191] fix the abilty to delete phones --- src/containers/AdminPhoneNumberInventory.js | 23 +++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/containers/AdminPhoneNumberInventory.js b/src/containers/AdminPhoneNumberInventory.js index bae3e08e0..46fb365d1 100644 --- a/src/containers/AdminPhoneNumberInventory.js +++ b/src/containers/AdminPhoneNumberInventory.js @@ -138,11 +138,11 @@ class AdminPhoneNumberInventory extends React.Component { }); }; - handleDeleteNumbersOpen = row => { + handleDeleteNumbersOpen = ([areaCode, , , availableCount]) => { this.setState({ deleteNumbersDialogOpen: true, - deleteNumbersAreaCode: row.areaCode, - deleteNumbersCount: row.availableCount + deleteNumbersAreaCode: areaCode, + deleteNumbersCount: availableCount }); }; @@ -194,12 +194,17 @@ class AdminPhoneNumberInventory extends React.Component { label: " ", options: { sort: false, - customBodyRender: (value, tableMeta) => - this.props.params.ownerPerms && ( - this.handleDeleteNumbersOpen(row)}> - - - ) + customBodyRender: (value, { rowData }) => { + return ( + this.props.params.ownerPerms && ( + this.handleDeleteNumbersOpen(rowData)} + > + + + ) + ); + } } }, // TODO: display additional information here about pending and past jobs From 81d81a9605a44e4e6f18cfdab73f76b0b764eb48 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Wed, 18 Aug 2021 18:13:32 -0400 Subject: [PATCH 168/191] update is_opted_out for contacts after starting a campaign to avoid contacts that opted out between upload and campaign start --- src/workers/jobs.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/workers/jobs.js b/src/workers/jobs.js index becfff312..c7e78b395 100644 --- a/src/workers/jobs.js +++ b/src/workers/jobs.js @@ -979,6 +979,20 @@ export async function startCampaign(job) { .delete(); } + // One last update of is_opted_out during start in case contacts opted-out from running campaigns + const updateOptOuts = await cacheableData.optOut.updateIsOptedOuts(query => + query + .join("opt_out", { + "opt_out.cell": "campaign_contact.cell", + ...(!process.env.OPTOUTS_SHARE_ALL_ORGS + ? { "opt_out.organization_id": "campaign.organization_id" } + : {}) + }) + .where("campaign_contact.campaign_id", campaign.id) + ); + if (updateOptOuts) { + console.log("campaign start updated is_opted_out", updateOptOuts); + } // We delete the job before invoking this task in case this process times out. // TODO: Decide if we want/need this anymore, relying on FUTURE campaign-contact cache load changes await jobRunner.dispatchTask(Tasks.CAMPAIGN_START_CACHE, { From cd6b8c5cc02580a0fd1cd8397fc24bca6a4e9e39 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Thu, 19 Aug 2021 14:25:08 -0400 Subject: [PATCH 169/191] support fakeservice getContactInfo for easier testing --- .../scrub-bad-mobilenums/react-component.js | 9 ++++++++ .../service-vendors/fakeservice/index.js | 23 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/extensions/service-managers/scrub-bad-mobilenums/react-component.js b/src/extensions/service-managers/scrub-bad-mobilenums/react-component.js index 0200896f8..6f3220ad6 100644 --- a/src/extensions/service-managers/scrub-bad-mobilenums/react-component.js +++ b/src/extensions/service-managers/scrub-bad-mobilenums/react-component.js @@ -17,6 +17,15 @@ export class CampaignConfig extends React.Component { render() { console.log("scrub-bad-mobilenums CampaignConfig", this.props); + if (!this.props.serviceManagerInfo) { + return ( +

+ Your service vendor does not support loading contact info. Please + contact your administrator on either disabling this extension or + reconfiguring the vendor. +

+ ); + } const { scrubBadMobileNumsFreshStart, scrubBadMobileNumsFinished, diff --git a/src/extensions/service-vendors/fakeservice/index.js b/src/extensions/service-vendors/fakeservice/index.js index 3d3bcab41..65dbde213 100644 --- a/src/extensions/service-vendors/fakeservice/index.js +++ b/src/extensions/service-vendors/fakeservice/index.js @@ -172,6 +172,29 @@ export async function deleteNumbersInAreaCode(organization, areaCode) { return count; } +// Does a lookup for carrier and optionally the contact name +export async function getContactInfo({ + organization, + contactNumber, + // Boolean: maybe twilio-specific? + lookupName +}) { + if (!contactNumber) { + return {}; + } + const contactInfo = { + carrier: "FakeCarrier", + // -1 is a landline, 1 is a mobile number + // we test against one of the lower digits to randomly + // but deterministically vary on the landline + status_code: contactNumber[11] === "2" ? -1 : 1 + }; + if (lookupName) { + contactInfo.lookup_name = `Foo ${parseInt(Math.random() * 1000)}`; + } + return contactInfo; +} + export default { sendMessage, buyNumbersInAreaCode, From 18902df391b1768aa06c24da8b02aa4ad7533276 Mon Sep 17 00:00:00 2001 From: Cody Gordon Date: Thu, 19 Aug 2021 16:02:15 -0700 Subject: [PATCH 170/191] clear campaign cache when saving assigned phones --- .../service-managers/per-campaign-messageservices/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/extensions/service-managers/per-campaign-messageservices/index.js b/src/extensions/service-managers/per-campaign-messageservices/index.js index 9168c2ba7..642d48e5c 100644 --- a/src/extensions/service-managers/per-campaign-messageservices/index.js +++ b/src/extensions/service-managers/per-campaign-messageservices/index.js @@ -224,6 +224,7 @@ export async function onCampaignUpdateSignal({ } } }); + await cacheableData.campaign.clear(campaign.id); } return await _editCampaignData(organization, campaign); } From e38ef3967ed88c59c81d02763ed17c1f4e6e7084 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Thu, 19 Aug 2021 20:26:34 -0400 Subject: [PATCH 171/191] simplify clear() call to one place --- .../service-managers/per-campaign-messageservices/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/extensions/service-managers/per-campaign-messageservices/index.js b/src/extensions/service-managers/per-campaign-messageservices/index.js index 642d48e5c..cfd85131c 100644 --- a/src/extensions/service-managers/per-campaign-messageservices/index.js +++ b/src/extensions/service-managers/per-campaign-messageservices/index.js @@ -200,7 +200,6 @@ export async function onCampaignUpdateSignal({ } await ownedPhoneNumber.releaseCampaignNumbers(campaign.id, r.knex); - await cacheableData.campaign.clear(campaign.id); } else if (updateData.inventoryPhoneNumberCounts) { if (campaign.is_started) { throw new Error( @@ -224,8 +223,8 @@ export async function onCampaignUpdateSignal({ } } }); - await cacheableData.campaign.clear(campaign.id); } + await cacheableData.campaign.clear(campaign.id); return await _editCampaignData(organization, campaign); } From 17ef258c8adcfb3758138831f0d3b0ef367f2357 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Fri, 20 Aug 2021 09:53:02 -0400 Subject: [PATCH 172/191] fix Survey bugs around second question presenting options --- .../AssignmentTexter/Survey.test.js | 68 ++++++++++++---- src/components/AssignmentTexter/Demo.jsx | 25 ++++++ src/components/AssignmentTexter/Survey.jsx | 78 ++++++------------- 3 files changed, 98 insertions(+), 73 deletions(-) diff --git a/__test__/components/AssignmentTexter/Survey.test.js b/__test__/components/AssignmentTexter/Survey.test.js index 942926aaf..42b4112dd 100644 --- a/__test__/components/AssignmentTexter/Survey.test.js +++ b/__test__/components/AssignmentTexter/Survey.test.js @@ -2,6 +2,8 @@ import React from "react"; import { shallow } from "enzyme"; import Accordion from "@material-ui/core/Accordion"; import AccordionSummary from "@material-ui/core/AccordionSummary"; +import MenuItem from "@material-ui/core/MenuItem"; +import ListItem from "@material-ui/core/ListItem"; import Survey from "../../../src/components/AssignmentTexter/Survey"; describe("Survey component", () => { @@ -19,53 +21,85 @@ describe("Survey component", () => { }, { value: "Foo is a mineral", - interactionStepId: 3, + interactionStepId: 4, nextInteractionStep: { script: "bar" } }, { value: "Foo is a vegetable", - interactionStepId: 3, + interactionStepId: 5, nextInteractionStep: { script: "fizz" } } ], filteredAnswerOptions: [ { value: "Foo is a mineral", - interactionStepId: 3, + interactionStepId: 4, nextInteractionStep: { script: "bar" } } ] } }; - const interactionSteps = [currentInteractionStep]; + const secondStep = { + id: 4, + script: "bar", + question: { + text: "Is Foo bigger than a breadbox?", + answerOptions: [ + { + value: "Yes", + interactionStepId: "100", + nextInteractionStep: { + id: "100", + script: "Is it human-made?" + } + } + ] + } + }; + const wrapper1 = shallow( + + ); - const wrapper = shallow( + const wrapper2 = shallow( ); - test("Accordion started open with correct text", () => { - const accordion = wrapper.find(Accordion); - const accordionSummary = wrapper.find(AccordionSummary); - const cardHeader = wrapper.find("CardHeader"); - const cardText = wrapper.find("CardText").at(0); + test("Accordion started closed with correct text", () => { + const accordion = wrapper2.find(Accordion); + const accordionSummary = wrapper2.find(AccordionSummary); + const menuItem = wrapper2.find(MenuItem).at(1); // 2nd for mineral + const listItems = wrapper2.find(ListItem); - expect(accordion.prop("expanded")).toBe(true); - expect(accordionSummary.prop("children")).toContain("Current question"); + expect(accordion.prop("expanded")).toBe(false); + expect(accordionSummary.prop("children")).toContain("All questions"); + expect(menuItem.prop("value")).toContain("Foo is a mineral"); + // filtered list includes mineral + expect( + listItems.findWhere(x => x.prop("value") === "Foo is a mineral").length + ).toBe(1); + // filtered list does NOT include vegetable + expect( + listItems.findWhere(x => x.prop("value") === "Foo is a vegetable").length + ).toBe(0); }); test("handleExpandChange Function", () => { - expect(wrapper.state().showAllQuestions).toEqual(false); - wrapper.instance().handleExpandChange(true); - expect(wrapper.state().showAllQuestions).toEqual(true); + expect(wrapper1.state().showAllQuestions).toEqual(false); + wrapper1.instance().handleExpandChange(true); + expect(wrapper1.state().showAllQuestions).toEqual(true); }); test("getNextScript Function", () => { expect( - wrapper.instance().getNextScript({ + wrapper1.instance().getNextScript({ interactionStep: currentInteractionStep, answerIndex: 0 }) diff --git a/src/components/AssignmentTexter/Demo.jsx b/src/components/AssignmentTexter/Demo.jsx index 2f4173b25..77afd477f 100644 --- a/src/components/AssignmentTexter/Demo.jsx +++ b/src/components/AssignmentTexter/Demo.jsx @@ -355,6 +355,31 @@ export const tests = testName => { } ] } + }, + { + id: "20", + script: "Super, we'll add your +1", + question: { + text: "Is your friend part of your household?", + answerOptions: [ + { + value: "Yes, household", + interactionStepId: "45", + nextInteractionStep: { + id: "45", + script: "Thanks for telling us" + } + }, + { + value: "No, not household", + interactionStepId: "46", + nextInteractionStep: { + id: "46", + script: "Ok, thanks for telling us" + } + } + ] + } } ], customFields: ["donationLink", "vendor_id"], diff --git a/src/components/AssignmentTexter/Survey.jsx b/src/components/AssignmentTexter/Survey.jsx index 0071ba864..eb103f9b1 100644 --- a/src/components/AssignmentTexter/Survey.jsx +++ b/src/components/AssignmentTexter/Survey.jsx @@ -34,7 +34,6 @@ const styles = { padding: 0 }, pastQuestionsLink: { - marginTop: "5px", borderTop: `1px solid ${theme.components.popup.outline}`, borderBottom: `1px solid ${theme.components.popup.outline}` } @@ -157,35 +156,18 @@ class AssignmentTexterSurveys extends Component { ); } - renderCurrentStepOldStyle(step) { - return this.renderStep(step, 1); - } - renderCurrentStep(step) { const { onRequestClose, questionResponses, listHeader } = this.props; const responseValue = questionResponses[step.id]; return ( -

+

{listHeader}
What was their response to:
{step.question.text}

- {Object.keys(questionResponses).length ? ( - this.handleExpandChange(true)} - key={`pastquestions`} - style={styles.pastQuestionsLink} - > - - - - - - ) : null} {( step.question.filteredAnswerOptions || step.question.answerOptions ).map((answerOption, index) => ( @@ -236,41 +218,29 @@ class AssignmentTexterSurveys extends Component { ); } - renderOldStyle() { + renderMultipleQuestions() { const { interactionSteps, currentInteractionStep } = this.props; let { showAllQuestions } = this.state; - - return interactionSteps.length === 0 ? null : ( - - - - {showAllQuestions ? "All questions" : "Current question"} + return ( +
+ this.handleExpandChange(expanded)} + > + + All questions + - - {showAllQuestions - ? interactionSteps.map((step, i) => this.renderStep(step, i)) - : this.renderCurrentStepOldStyle(currentInteractionStep)} - + {interactionSteps.map((step, i) => ( + + {this.renderStep(step, i)} + + ))} - - ); - } - - renderMultipleQuestions() { - const { interactionSteps } = this.props; - let { showAllQuestions } = this.state; - return ( - this.handleExpandChange(expanded)} - > - All questions - {interactionSteps.map((step, i) => ( - - {this.renderStep(step, i)} - - ))} - + {currentInteractionStep + ? this.renderCurrentStep(currentInteractionStep) + : null} +
); } @@ -287,12 +257,8 @@ class AssignmentTexterSurveys extends Component { } render() { - const { interactionSteps } = this.props; - const oldStyle = typeof this.props.onRequestClose != "function"; - - if (oldStyle) { - return this.renderOldStyle(); - } else if (interactionSteps.length > 1) { + const { interactionSteps, currentInteractionStep } = this.props; + if (interactionSteps.length > 1) { return this.renderMultipleQuestions(); } else if (interactionSteps.length === 1) { return this.renderSingleQuestion(); From 48225853b285061d640bcc526857abe1a72b54c5 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Fri, 20 Aug 2021 10:49:13 -0400 Subject: [PATCH 173/191] fix mobile sideboxes popover to expand fully --- src/components/AssignmentTexter/Controls.jsx | 8 +++++--- src/components/AssignmentTexter/StyleControls.js | 13 +++++++++---- src/components/AssignmentTexter/Toolbar.jsx | 13 ++++++++++--- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/components/AssignmentTexter/Controls.jsx b/src/components/AssignmentTexter/Controls.jsx index 5bef5d095..96f9cbf44 100644 --- a/src/components/AssignmentTexter/Controls.jsx +++ b/src/components/AssignmentTexter/Controls.jsx @@ -1001,12 +1001,14 @@ export class AssignmentTexterContactControls extends React.Component { if (sideboxOpen) { return ( {sideboxList} diff --git a/src/components/AssignmentTexter/StyleControls.js b/src/components/AssignmentTexter/StyleControls.js index 53cace50a..3cbb2da6c 100644 --- a/src/components/AssignmentTexter/StyleControls.js +++ b/src/components/AssignmentTexter/StyleControls.js @@ -49,10 +49,6 @@ export const inlineStyles = { flatButtonLabel: { textTransform: "none", fontWeight: "bold" - }, - popoverSidebox: { - backgroundColor: "rgb(240, 240, 240)", - padding: "20px" } }; @@ -69,6 +65,15 @@ export const flexStyles = StyleSheet.create({ height: "100%", backgroundColor: bgGrey }, + popoverSideboxesInner: { + // expand to fill the whole popover + width: "100%", + height: "100%", + // show campaign header in-view + top: "50px", + left: "18px", + padding: "20px" + }, popover: { width: "85%", height: "85%", diff --git a/src/components/AssignmentTexter/Toolbar.jsx b/src/components/AssignmentTexter/Toolbar.jsx index 67ced7f69..c3b0aa394 100644 --- a/src/components/AssignmentTexter/Toolbar.jsx +++ b/src/components/AssignmentTexter/Toolbar.jsx @@ -49,6 +49,14 @@ const styles = StyleSheet.create({ maxWidth: "50%" // iphone 5 and X } }, + titleArea: { + // give room for the wrench sideboxes icon + maxWidth: "calc(100% - 100px)" + }, + contactArea: { + // give room for prev/next arrows + maxWidth: "calc(100% - 200px)" + }, titleSmall: { height: "18px", lineHeight: "18px", @@ -68,7 +76,6 @@ const styles = StyleSheet.create({ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" - // maxWidth: "90%" }, contactToolbarIconButton: { padding: "3px", @@ -157,7 +164,7 @@ const ContactToolbar = function ContactToolbar(props) { -
+
Campaign ID: {props.campaign.id}
@@ -192,7 +199,7 @@ const ContactToolbar = function ContactToolbar(props) { -
+
{formattedLocalTime} - {formattedLocation}
From f78126ea82415361e90eafee678a8c4e8d80a197 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Fri, 20 Aug 2021 17:40:19 -0400 Subject: [PATCH 174/191] redash contact-loader --- src/components/CampaignContactsChoiceForm.jsx | 2 +- .../contact-loaders/redash/index.js | 296 ++++++++++++++++++ .../contact-loaders/redash/react-component.js | 119 +++++++ src/server/lib/http-request.js | 13 +- src/workers/jobs.js | 9 + src/workers/tasks.js | 15 +- 6 files changed, 449 insertions(+), 5 deletions(-) create mode 100644 src/extensions/contact-loaders/redash/index.js create mode 100644 src/extensions/contact-loaders/redash/react-component.js diff --git a/src/components/CampaignContactsChoiceForm.jsx b/src/components/CampaignContactsChoiceForm.jsx index 64428aba2..58cc6d820 100644 --- a/src/components/CampaignContactsChoiceForm.jsx +++ b/src/components/CampaignContactsChoiceForm.jsx @@ -129,7 +129,7 @@ export class CampaignContactsChoiceForm extends React.Component { saveLabel={this.props.saveLabel} clientChoiceData={ingestMethod && ingestMethod.clientChoiceData} lastResult={lastResult} - jobResultMessage={null} + jobResultMessage={null /* use lastResult.result instead */} contactsPerPhoneNumber={contactsPerPhoneNumber} maxNumbersPerCampaign={maxNumbersPerCampaign} /> diff --git a/src/extensions/contact-loaders/redash/index.js b/src/extensions/contact-loaders/redash/index.js new file mode 100644 index 000000000..4dd8f891d --- /dev/null +++ b/src/extensions/contact-loaders/redash/index.js @@ -0,0 +1,296 @@ +import { parseCSV } from "../../../lib"; +import { finalizeContactLoad } from "../helpers"; +import { completeContactLoad, failedContactLoad } from "../../../workers/jobs"; +import { Tasks } from "../../../workers/tasks"; +import { jobRunner } from "../../job-runners"; +import { r, cacheableData } from "../../../server/models"; +import { getConfig, hasConfig } from "../../../server/api/lib/config"; +import httpRequest from "../../../server/lib/http-request"; + +export const name = "redash"; + +export function displayName() { + return "Redash"; +} + +export function serverAdministratorInstructions() { + return { + environmentVariables: ["REDASH_BASE_URL", "REDASH_USER_API_KEY"], + description: "", + setupInstructions: + "Set two environment variables REDASH_BASE_URL (e.g. 'https://example.com') and REDASH_USER_API_KEY" + }; +} + +export async function available(organization, user) { + /// return an object with two keys: result: true/false + /// these keys indicate if the ingest-contact-loader is usable + /// Sometimes credentials need to be setup, etc. + /// A second key expiresSeconds: should be how often this needs to be checked + /// If this is instantaneous, you can have it be 0 (i.e. always), but if it takes time + /// to e.g. verify credentials or test server availability, + /// then it's better to allow the result to be cached + const result = + getConfig("REDASH_BASE_URL", organization) && + getConfig("REDASH_USER_API_KEY", organization); + return { + result, + expiresSeconds: 0 + }; +} + +export function addServerEndpoints(expressApp) { + /// If you need to create API endpoints for server-to-server communication + /// this is where you would run e.g. app.post(....) + /// Be mindful of security and make sure there's + /// This is NOT where or how the client send or receive contact data + return; +} + +export function clientChoiceDataCacheKey(campaign, user) { + /// returns a string to cache getClientChoiceData -- include items that relate to cacheability + return ""; +} + +export async function getClientChoiceData(organization, campaign, user) { + /// data to be sent to the admin client to present options to the component or similar + /// The react-component will be sent this data as a property + /// return a json object which will be cached for expiresSeconds long + /// `data` should be a single string -- it can be JSON which you can parse in the client component + return { + data: getConfig("REDASH_BASE_URL", organization), + expiresSeconds: 0 + }; +} + +let baseTlsEnabled = process.env.NODE_TLS_REJECT_UNAUTHORIZED; +const tlsVerification = { + avoid: yesAvoid => { + if (yesAvoid) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0; + } + }, + restore: () => { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = baseTlsEnabled; + } +}; + +export async function downloadAndSaveResults( + { queryId, resultId, job, redashUrl, organizationId }, + contextVars +) { + const organization = cacheableData.organization.load(organizationId); + const userApiKey = getConfig("REDASH_USER_API_KEY", organization); + const baseUrl = getConfig("REDASH_BASE_URL", organization); + const tlsVerifyOff = getConfig("REDASH_TLS_VERIFY_OFF", organization); + + console.log( + "redash.downloadAndSaveResults", + queryId, + resultId, + job.campaign_id, + contextVars + ); + // 3. Download result csv + const resultsUrl = `${baseUrl}/api/queries/${queryId}/results/${resultId}.csv`; + tlsVerification.avoid(tlsVerifyOff); + const resultsDataResult = await httpRequest(resultsUrl, { + method: "get", + headers: { Authorization: `Key ${userApiKey}` }, + timeout: 900000 // 15 min + }); + tlsVerification.restore(); + + // 4. Parse CSV + const { contacts, customFields, validationStats, error } = await new Promise( + (resolve, reject) => { + parseCSV( + resultsDataResult.body, + parseResults => { + resolve(parseResults); + }, + { + headerTransformer: column => + column + .replace("first_name", "firstName") + .replace("last_name", "lastName") + } + ); + } + ); + if (error) { + await failedContactLoad( + job, + null, + { redashUrl }, + { errors: [`CSV parse error: ${error}`] } + ); + return; + } + + await finalizeContactLoad( + job, + contacts, + getConfig("MAX_CONTACTS", organization), + { redashUrl }, + JSON.stringify({ finalCount: contacts.length, validationStats }) + ); +} + +export async function processContactLoad(job, maxContacts, organization) { + /// Trigger processing -- this will likely be the most important part + /// you should load contacts into the contact table with the job.campaign_id + /// Since this might just *begin* the processing and other work might + /// need to be completed asynchronously after this is completed (e.g. to distribute loads) + /// After true contact-load completion, this (or another function) + /// MUST call src/workers/jobs.js::completeContactLoad(job) + /// The async function completeContactLoad(job) will + /// * delete contacts that are in the opt_out table, + /// * delete duplicate cells, + /// * clear/update caching, etc. + /// The organization parameter is an object containing the name and other + /// details about the organization on whose behalf this contact load + /// was initiated. It is included here so it can be passed as the + /// second parameter of getConfig in order to retrieve organization- + /// specific configuration values. + /// Basic responsibilities: + /// 1. Delete previous campaign contacts on a previous choice/upload + /// 2. Set campaign_contact.campaign_id = job.campaign_id on all uploaded contacts + /// 3. Set campaign_contact.message_status = "needsMessage" on all uploaded contacts + /// 4. Ensure that campaign_contact.cell is in the standard phone format "+15551234567" + /// -- do NOT trust your backend to ensure this + /// 5. If your source doesn't have timezone offset info already, then you need to + /// fill the campaign_contact.timezone_offset with getTimezoneByZip(contact.zip) (from "../../workers/jobs") + /// Things to consider in your implementation: + /// * Batching + /// * Error handling + /// * "Request of Doom" scenarios -- queries or jobs too big to complete + + const campaignId = job.campaign_id; + const userApiKey = getConfig("REDASH_USER_API_KEY", organization); + const baseUrl = getConfig("REDASH_BASE_URL", organization); + const tlsVerifyOff = getConfig("REDASH_TLS_VERIFY_OFF", organization); + const { redashUrl } = JSON.parse(job.payload); + + if (!userApiKey || !baseUrl) { + await failedContactLoad( + job, + null, + { redashUrl }, + { errors: ["misconfigured"] } + ); + return; + } + if (!redashUrl) { + await failedContactLoad( + job, + null, + { redashUrl }, + { errors: ["submit with a query url"] } + ); + return; + } + + await r + .knex("campaign_contact") + .where("campaign_id", campaignId) + .delete(); + + // /queries/9009/source + // e.g. https://foo.example.com/queries/9010/source?p_limit=20 + const matchUrl = redashUrl.match(/\/queries\/(\d+)[^?]*(\?.*)?/); + let queryId = null; + let params = ""; + if (matchUrl) { + queryId = matchUrl[1]; + params = matchUrl[2] || ""; + } else { + await failedContactLoad( + job, + null, + { redashUrl }, + { errors: [`Unrecognized URL format. Should have /queries/...`] } + ); + return; + } + // 1. start query + const startQueryUrl = `${baseUrl}/api/queries/${queryId}/refresh${params}`; + console.log("REDASH 1"); + tlsVerification.avoid(tlsVerifyOff); + let refreshRedashResult; + try { + refreshRedashResult = await httpRequest(startQueryUrl, { + method: "post", + headers: { Authorization: `Key ${userApiKey}` } + }); + } catch (err) { + await failedContactLoad( + job, + null, + { redashUrl }, + { errors: [`Failed to access Redash server. ${err.message}`] } + ); + return; + } + tlsVerification.restore(); + console.log( + "refreshRedashResult", + refreshRedashResult, + refreshRedashResult.status + ); + if (refreshRedashResult.status != 200) { + await failedContactLoad( + job, + null, + { redashUrl }, + { errors: [`request error: ${refreshRedashResult.body}`] } + ); + return; + } + const redashJobData = await refreshRedashResult.json(); + // 2. poll job status + const jobQueryId = redashJobData.job.id; + const redashPollStatusUrl = `${baseUrl}/api/jobs/${jobQueryId}`; + console.log("REDASH 2", redashJobData); + tlsVerification.avoid(tlsVerifyOff); + const redashQueryCompleted = await httpRequest(redashPollStatusUrl, { + method: "get", + headers: { Authorization: `Key ${userApiKey}` }, + bodyRetryFunction: async res => { + const json = await res.json(); + console.log("statusValidation", json); + const jobStatus = json && json.job && json.job.status; + return jobStatus === 3 || jobStatus === 4 ? json : { RETRY: 1 }; + }, + retries: 1000, + timeout: 900000, // 15 min + retryDelayMs: 3000 // 3 seconds + }); + tlsVerification.restore(); + console.log("refreshDataResult", redashQueryCompleted); + if (redashQueryCompleted.job.status === 4) { + await failedContactLoad( + job, + null, + { redashUrl }, + { + errors: [ + `query failed id:${jobQueryId} ${redashQueryCompleted.job.error}` + ] + } + ); + return; + } + const resultId = redashQueryCompleted.job.query_result_id; + + // 3. Download and upload result csv + await jobRunner.dispatchTask(Tasks.EXTENSION_TASK, { + method: "downloadAndSaveResults", + path: "extensions/contact-loaders/redash", + resultId, + queryId, + redashUrl, + job, + organizationId: organization.id + }); +} diff --git a/src/extensions/contact-loaders/redash/react-component.js b/src/extensions/contact-loaders/redash/react-component.js new file mode 100644 index 000000000..f68e6f55f --- /dev/null +++ b/src/extensions/contact-loaders/redash/react-component.js @@ -0,0 +1,119 @@ +import type from "prop-types"; +import React from "react"; +import GSForm from "../../../components/forms/GSForm"; +import GSSubmitButton from "../../../components/forms/GSSubmitButton"; +import GSTextField from "../../../components/forms/GSTextField"; +import Form from "react-formal"; + +import List from "@material-ui/core/List"; +import ListItem from "@material-ui/core/ListItem"; +import ListItemIcon from "@material-ui/core/ListItemIcon"; +import ListItemText from "@material-ui/core/ListItemText"; + +import * as yup from "yup"; + +export class CampaignContactsForm extends React.Component { + constructor(props) { + super(props); + const { lastResult } = props; + let cur = {}; + if (lastResult && lastResult.reference) { + try { + cur = JSON.parse(lastResult.reference); + } catch (err) { + // parse error should just stay empty + } + } + console.log("redash", lastResult, props); + this.state = { + redashUrl: cur.redashUrl || "" + }; + } + + render() { + const { campaignIsStarted, clientChoiceData, lastResult } = this.props; + if (campaignIsStarted) { + return ( +
+ ); + } + let errorMessage = ""; + if (lastResult && lastResult.result) { + const resultInfo = JSON.parse(lastResult.result); + console.log(resultInfo); + if (resultInfo && resultInfo.errors) { + errorMessage = resultInfo.errors[0]; + } + } + return ( + { + this.setState({ ...formValues }); + this.props.onChange(JSON.stringify(formValues)); + }} + onSubmit={formValues => { + // sets values locally + this.setState({ ...formValues }); + // triggers the parent to update values + this.props.onChange(JSON.stringify(formValues)); + // and now do whatever happens when clicking 'Next' + this.props.onSubmit(); + }} + > +

+ Provide a redash url that should look something like{" "} + {clientChoiceData}/queries/<digits> or{" "} + {clientChoiceData}/queries/<digits>?p_someparam=123 -- + make sure the parameters are not just the defaults but the ones you + want set for your query. +

+ + + {errorMessage && ( + + {this.props.icons.warning} + + + )} + + + +
+ ); + } +} + +CampaignContactsForm.propTypes = { + onChange: type.func, + onSubmit: type.func, + campaignIsStarted: type.bool, + + icons: type.object, + + saveDisabled: type.bool, + saveLabel: type.string, + + clientChoiceData: type.string, + jobResultMessage: type.string, + lastResult: type.object +}; + +CampaignContactsForm.prototype.renderAfterStart = true; diff --git a/src/server/lib/http-request.js b/src/server/lib/http-request.js index 90233df2f..0cbb5b398 100644 --- a/src/server/lib/http-request.js +++ b/src/server/lib/http-request.js @@ -20,8 +20,10 @@ const requestWithRetry = async ( { validStatuses, statusValidationFunction, + bodyRetryFunction, retries: retriesInput, timeout = 2000, + retryDelayMs = 50, ...props } = {} ) => { @@ -29,7 +31,7 @@ const requestWithRetry = async ( const requestId = uuid(); const retryDelay = () => { - const baseDelay = 50; + const baseDelay = retryDelayMs || 50; const randomDelay = Math.floor(Math.random() * (baseDelay / 2)); return baseDelay + randomDelay; }; @@ -115,7 +117,14 @@ const requestWithRetry = async ( error, response ); - + if (bodyRetryFunction) { + const bodyRetryResult = await bodyRetryFunction(response); + if (bodyRetryResult && bodyRetryResult.RETRY) { + retryReturnError = RetryReturnError.RETRY; + } else { + response = bodyRetryResult; + } + } if (retryReturnError === RetryReturnError.RETRY) { await setTimeout(() => {}, retryDelay()); continue; diff --git a/src/workers/jobs.js b/src/workers/jobs.js index c7e78b395..0fcabf028 100644 --- a/src/workers/jobs.js +++ b/src/workers/jobs.js @@ -289,6 +289,7 @@ export async function completeContactLoad( let deleteOptOutCells = null; let deleteDuplicateCells = null; + console.log("completeContactLoad", campaignId, job.id); const knexOptOutDeleteResult = await r .knex("campaign_contact") .whereIn("cell", getOptOutSubQuery(campaign.organization_id)) @@ -371,6 +372,14 @@ export async function completeContactLoad( } }); } + console.log( + "completeContactLoad completed", + campaignId, + job.id, + finalContactCount, + deleteOptOutCells, + deleteDuplicateCells + ); } export async function unzipPayload(job) { diff --git a/src/workers/tasks.js b/src/workers/tasks.js index ac4b683c0..09fc60335 100644 --- a/src/workers/tasks.js +++ b/src/workers/tasks.js @@ -7,10 +7,11 @@ import { r, cacheableData } from "../server/models"; import { processServiceManagers } from "../extensions/service-managers"; export const Tasks = Object.freeze({ - SEND_MESSAGE: "send_message", ACTION_HANDLER_QUESTION_RESPONSE: "action_handler:question_response", ACTION_HANDLER_TAG_UPDATE: "action_handler:tag_update", CAMPAIGN_START_CACHE: "campaign_start_cache", + EXTENSION_TASK: "extension_task", + SEND_MESSAGE: "send_message", SERVICE_MANAGER_TRIGGER: "service_manager_trigger" }); @@ -131,11 +132,21 @@ const startCampaignCache = async ({ campaign, organization }, contextVars) => { await loadOptOuts; }; +const extensionTask = async (taskData, contextVars) => { + if (taskData.path && taskData.method) { + const extension = require("../" + taskData.path); + if (extension && typeof extension[taskData.method] === "function") { + await extension[taskData.method](taskData, contextVars); + } + } +}; + const taskMap = Object.freeze({ - [Tasks.SEND_MESSAGE]: sendMessage, [Tasks.ACTION_HANDLER_QUESTION_RESPONSE]: questionResponseActionHandler, [Tasks.ACTION_HANDLER_TAG_UPDATE]: tagUpdateActionHandler, [Tasks.CAMPAIGN_START_CACHE]: startCampaignCache, + [Tasks.EXTENSION_TASK]: extensionTask, + [Tasks.SEND_MESSAGE]: sendMessage, [Tasks.SERVICE_MANAGER_TRIGGER]: serviceManagerTrigger }); From bed729745a1d4947d7151419277a59eaba4d2d75 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Fri, 20 Aug 2021 18:08:03 -0400 Subject: [PATCH 175/191] remove server from visibility --- src/extensions/contact-loaders/redash/index.js | 2 +- src/extensions/contact-loaders/redash/react-component.js | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/extensions/contact-loaders/redash/index.js b/src/extensions/contact-loaders/redash/index.js index 4dd8f891d..651ee86cb 100644 --- a/src/extensions/contact-loaders/redash/index.js +++ b/src/extensions/contact-loaders/redash/index.js @@ -58,7 +58,7 @@ export async function getClientChoiceData(organization, campaign, user) { /// return a json object which will be cached for expiresSeconds long /// `data` should be a single string -- it can be JSON which you can parse in the client component return { - data: getConfig("REDASH_BASE_URL", organization), + data: "nodata", expiresSeconds: 0 }; } diff --git a/src/extensions/contact-loaders/redash/react-component.js b/src/extensions/contact-loaders/redash/react-component.js index f68e6f55f..7bb75ff65 100644 --- a/src/extensions/contact-loaders/redash/react-component.js +++ b/src/extensions/contact-loaders/redash/react-component.js @@ -71,9 +71,11 @@ export class CampaignContactsForm extends React.Component { >

Provide a redash url that should look something like{" "} - {clientChoiceData}/queries/<digits> or{" "} - {clientChoiceData}/queries/<digits>?p_someparam=123 -- - make sure the parameters are not just the defaults but the ones you + http:{"//"}<redash server>/queries/<digits> or{" "} + + https:{"//"}<redash server>/queries/<digits>?p_someparam=123 + {" "} + -- make sure the parameters are not just the defaults but the ones you want set for your query.

Date: Fri, 20 Aug 2021 18:14:32 -0400 Subject: [PATCH 176/191] remove server from visibility --- src/extensions/contact-loaders/redash/react-component.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extensions/contact-loaders/redash/react-component.js b/src/extensions/contact-loaders/redash/react-component.js index 7bb75ff65..07aed22d4 100644 --- a/src/extensions/contact-loaders/redash/react-component.js +++ b/src/extensions/contact-loaders/redash/react-component.js @@ -71,9 +71,9 @@ export class CampaignContactsForm extends React.Component { >

Provide a redash url that should look something like{" "} - http:{"//"}<redash server>/queries/<digits> or{" "} + https:{"//"}<RedashServer>/queries/<digits> or{" "} - https:{"//"}<redash server>/queries/<digits>?p_someparam=123 + https:{"//"}<RedashServer>/queries/<digits>?p_someparam=123 {" "} -- make sure the parameters are not just the defaults but the ones you want set for your query. From 4a10aa2d7f5c3e8bbf3072d0ac7e168ffb8435bb Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Mon, 23 Aug 2021 14:58:23 -0400 Subject: [PATCH 177/191] GSDateField: wrap date in Date() since the value is coming in as text and cannot be compared --- src/components/CampaignBasicsForm.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CampaignBasicsForm.jsx b/src/components/CampaignBasicsForm.jsx index ae56187a9..0bd4aa002 100644 --- a/src/components/CampaignBasicsForm.jsx +++ b/src/components/CampaignBasicsForm.jsx @@ -20,7 +20,7 @@ const FormSchemaBeforeStarted = { .test( "in-future", "Due date should be in the future: when you expect the campaign to end", - val => val > new Date() + val => new Date(val) > new Date() ), logoImageUrl: yup .string() From d635304107440e05e2fd9612d35559c2e42cafe6 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Wed, 25 Aug 2021 10:13:41 -0400 Subject: [PATCH 178/191] webpack: [hash] is available in both dev and prod NODE_ENV contexts so use that --- webpack/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack/config.js b/webpack/config.js index cb29baee2..a6be845c9 100644 --- a/webpack/config.js +++ b/webpack/config.js @@ -23,7 +23,7 @@ const plugins = [ const jsxLoaders = [{ loader: "babel-loader" }]; const assetsDir = process.env.ASSETS_DIR; const assetMapFile = process.env.ASSETS_MAP_FILE; -const outputFile = DEBUG ? "[name].js" : "[name].[chunkhash].js"; +const outputFile = DEBUG ? "[name].js" : "[name].[hash].js"; console.log("Configuring Webpack with", { assetsDir, assetMapFile, From 2980a2ca16df4f6942941f64d50429117ffc07b0 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Wed, 25 Aug 2021 14:00:25 -0400 Subject: [PATCH 179/191] message review: fix selection after action --- src/components/IncomingMessageList/index.jsx | 28 +++++++++----------- src/containers/AdminIncomingMessageList.jsx | 21 +++++++++++---- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/components/IncomingMessageList/index.jsx b/src/components/IncomingMessageList/index.jsx index 203d73f72..d57ab86a9 100644 --- a/src/components/IncomingMessageList/index.jsx +++ b/src/components/IncomingMessageList/index.jsx @@ -92,20 +92,19 @@ export class IncomingMessageList extends Component { this.props.campaignsFilter.campaignIds.length === 1 && this.props.assignmentsFilter.texterId; }; - UNSAFE_componentWillReceiveProps = prevProps => { - if ( - this.props.clearSelectedMessages && - this.state.selectedRows.length > 0 - ) { - this.setState({ - selectedRows: [] - }); + UNSAFE_componentWillReceiveProps = nextProps => { + if (nextProps.clearSelectedMessages) { + if (this.state.selectedRows.length > 0) { + this.setState({ + selectedRows: [] + }); + } this.props.onConversationSelected([], []); } - let previousPageInfo = { total: 0 }; - if (prevProps.conversations.conversations) { - previousPageInfo = prevProps.conversations.conversations.pageInfo; + let nextPageInfo = { total: 0 }; + if (nextProps.conversations.conversations) { + nextPageInfo = nextProps.conversations.conversations.pageInfo; } let pageInfo = { total: 0 }; @@ -113,11 +112,8 @@ export class IncomingMessageList extends Component { pageInfo = this.props.conversations.conversations.pageInfo; } - if ( - previousPageInfo.total !== pageInfo.total || - (!previousPageInfo && pageInfo) - ) { - this.props.onConversationCountChanged(pageInfo.total); + if (nextPageInfo.total !== pageInfo.total || (!nextPageInfo && pageInfo)) { + this.props.onConversationCountChanged(nextPageInfo.total); } }; diff --git a/src/containers/AdminIncomingMessageList.jsx b/src/containers/AdminIncomingMessageList.jsx index 262849b0f..1b65ed6af 100644 --- a/src/containers/AdminIncomingMessageList.jsx +++ b/src/containers/AdminIncomingMessageList.jsx @@ -55,7 +55,7 @@ export class AdminIncomingMessageList extends Component { return true; }; - UNSAFE_componentWillReceiveProps = () => { + UNSAFE_componentWillReceiveProps = nextProps => { if (this.state.clearSelectedMessages) { this.setState({ clearSelectedMessages: false, @@ -230,19 +230,30 @@ export class AdminIncomingMessageList extends Component { }; handleRowSelection = async (selectedRows, data) => { + let updateState; if (this.state.previousSelectedRows === "all" && selectedRows !== "all") { - await this.setState({ + updateState = { previousSelectedRows: [], campaignIdsContactIds: [], needsRender: false - }); + }; } else { - await this.setState({ + updateState = { previousSelectedRows: selectedRows, campaignIdsContactIds: data, needsRender: false - }); + }; + } + // after sub-component clears its messages, we should reset the clearSelectedMessages value + if ( + this.state.clearSelectedMessages && + selectedRows && + selectedRows.length === 0 + ) { + updateState.clearSelectedMessages = false; + updateState.needsRender = true; } + await this.setState(updateState); }; handleCampaignsReceived = async campaigns => { From 9fb051d59fbc2dff9125e80cb7579f522d9823fc Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Wed, 25 Aug 2021 14:29:13 -0400 Subject: [PATCH 180/191] per-campaign-messageservice: implement front-end for Release Numbers --- src/api/campaign.js | 1 + .../AdminCampaignMessagingService.jsx | 12 ++-- .../per-campaign-messageservices/index.js | 23 +++++--- .../react-component.js | 45 +++++++++++++++ .../service-vendors/twilio/index.js | 8 +++ src/server/api/campaign.js | 56 ++++++++++++++++--- src/server/api/organization.js | 1 + 7 files changed, 127 insertions(+), 19 deletions(-) diff --git a/src/api/campaign.js b/src/api/campaign.js index 9edb6e6df..4a70ea5d1 100644 --- a/src/api/campaign.js +++ b/src/api/campaign.js @@ -134,6 +134,7 @@ export const schema = gql` messageserviceSid: String useOwnMessagingService: Boolean + messageServiceLink: String phoneNumbers: [String] inventoryPhoneNumberCounts: [CampaignPhoneNumberCount] } diff --git a/src/containers/AdminCampaignMessagingService.jsx b/src/containers/AdminCampaignMessagingService.jsx index cb0c36bd5..c6562798f 100644 --- a/src/containers/AdminCampaignMessagingService.jsx +++ b/src/containers/AdminCampaignMessagingService.jsx @@ -27,7 +27,6 @@ const styles = StyleSheet.create({ class AdminCampaignMessagingService extends React.Component { render() { const campaign = this.props.data.campaign; - const messagingServiceUrl = `https://www.twilio.com/console/sms/services/${campaign.messageserviceSid}/`; const phoneNumbers = campaign.phoneNumbers || []; return (

@@ -36,10 +35,12 @@ class AdminCampaignMessagingService extends React.Component {
Campaign ID: {campaign.id}
- + {campaign.messageServiceLink ? ( + + ) : null}
Total Phone Numbers: {phoneNumbers.length}
@@ -76,6 +77,7 @@ const queries = { id title messageserviceSid + messageServiceLink phoneNumbers } } diff --git a/src/extensions/service-managers/per-campaign-messageservices/index.js b/src/extensions/service-managers/per-campaign-messageservices/index.js index cfd85131c..662118453 100644 --- a/src/extensions/service-managers/per-campaign-messageservices/index.js +++ b/src/extensions/service-managers/per-campaign-messageservices/index.js @@ -80,13 +80,18 @@ const _editCampaignData = async (organization, campaign) => { organization ); // 3. fullyConfigured + const contactsCount = + campaign.contactsCount || + (await r.getCount( + r.knex("campaign_contact").where("campaign_id", campaign.id) + )); const contactsPerNum = _contactsPerPhoneNumber(organization); const numbersReserved = (inventoryPhoneNumberCounts || []).reduce( (acc, entry) => acc + entry.count, 0 ); const numbersNeeded = Math.ceil( - (campaign.contactsCount || 0) / contactsPerNum.contactsPerPhoneNumber + (contactsCount || 0) / contactsPerNum.contactsPerPhoneNumber ); // 4. which mode: // previously: EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS vs. EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE @@ -109,8 +114,10 @@ const _editCampaignData = async (organization, campaign) => { // Two mutually exclusive modes: EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS vs. EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE Boolean(campaign.messageservice_sid) || (numbersReserved >= numbersNeeded && !manualMessageServiceMode), - unArchiveable: - !campaign.use_own_messaging_service || campaign.messageservice_sid + unArchiveable: Boolean( + !campaign.use_own_messaging_service || + (campaign.messageservice_sid && counts.length) + ) }; }; @@ -125,14 +132,17 @@ export async function getCampaignData({ // called both from edit and stats contexts: editMode==true for edit page if (fromCampaignStatsPage) { // STATS: campaign.messageservice_sid (enabled) + const counts = await ownedPhoneNumber.listCampaignNumbers(campaign.id); return { data: { useOwnMessagingService: campaign.use_own_messaging_service, messageserviceSid: campaign.messageservice_sid || null, ..._contactsPerPhoneNumber(organization) }, - unArchiveable: - !campaign.use_own_messaging_service || campaign.messageservice_sid + unArchiveable: Boolean( + !campaign.use_own_messaging_service || + (campaign.messageservice_sid && counts.length) + ) }; } else { // EDIT @@ -148,8 +158,7 @@ export async function onCampaignUpdateSignal({ fromCampaignStatsPage }) { // TODO: - // 1. receive/process releaseCampaignNumbers button (also widget) -- from stats page - // 2. receive CampaignPhoneNumbers form (replace action on campaign save) + // 1. receive CampaignPhoneNumbers form (replace action on campaign save) // inventoryPhoneNumberCounts in schema.js // fullyConfigured ~= campaign.messageservice_sid && owned_phone_numbers await accessRequired(user, campaign.organization_id, "ADMIN"); diff --git a/src/extensions/service-managers/per-campaign-messageservices/react-component.js b/src/extensions/service-managers/per-campaign-messageservices/react-component.js index f93064586..8f2a46d29 100644 --- a/src/extensions/service-managers/per-campaign-messageservices/react-component.js +++ b/src/extensions/service-managers/per-campaign-messageservices/react-component.js @@ -955,3 +955,48 @@ export class CampaignConfig extends React.Component { ); } } + +export class CampaignStats extends React.Component { + static propTypes = { + campaign: type.object, + serviceManagerInfo: type.object, + saveLabel: type.string, + onSubmit: type.func + }; + state = { + releasingNumbers: false + }; + + render() { + const { campaign, serviceManagerInfo, onSubmit } = this.props; + if (!campaign.isArchived) { + return null; + } + return ( +
+ {!serviceManagerInfo.unArchiveable ? ( +
Phone numbers have been released
+ ) : null} + {campaign.isArchived && campaign.messageserviceSid ? ( + + ) : null} + {this.state.releasingNumbers ? : null} +
+ ); + } +} diff --git a/src/extensions/service-vendors/twilio/index.js b/src/extensions/service-vendors/twilio/index.js index 97a3cb492..c8697dab1 100644 --- a/src/extensions/service-vendors/twilio/index.js +++ b/src/extensions/service-vendors/twilio/index.js @@ -653,6 +653,13 @@ async function searchForAvailableNumbers( [numberType].list(criteria); } +/** + * Provide a Twilio console link to the messagingService object (for configuration) + */ +export function messageServiceLink(organization, messagingServiceSid) { + return `https://www.twilio.com/console/sms/services/${messagingserviceSid}/`; +} + /** * Fetch Phone Numbers assigned to Messaging Service */ @@ -1091,6 +1098,7 @@ export default { getTwilio, getServiceConfig, getMessageServiceSid, + messageServiceLink, updateConfig, getMetadata, fullyConfigured diff --git a/src/server/api/campaign.js b/src/server/api/campaign.js index 1442dbff4..5b905e674 100644 --- a/src/server/api/campaign.js +++ b/src/server/api/campaign.js @@ -1,8 +1,8 @@ import { accessRequired } from "./errors"; import { mapFieldsToModel, mapFieldsOrNull } from "./lib/utils"; import { - serviceMap, getServiceNameFromOrganization, + getServiceFromOrganization, errorDescription } from "../../extensions/service-vendors"; import { getServiceManagerData } from "../../extensions/service-managers"; @@ -578,6 +578,7 @@ export const resolvers = { ); }, contactsAreaCodeCounts: async (campaign, _, { user, loaders }) => { + // TODO: consider removal (moved to extensions/service-managers/per-campaign-messageservices const organization = await loaders.organization.load( campaign.organization_id ); @@ -717,20 +718,61 @@ export const resolvers = { ...r })); }, - phoneNumbers: async (campaign, _, { user }) => { + messageServiceLink: async (campaign, _, { user, loaders }) => { await accessRequired( user, campaign.organization_id, "SUPERVOLUNTEER", true ); - const phoneNumbers = await serviceMap.twilio.getPhoneNumbersForService( - campaign.organization, - campaign.messageservice_sid + if (!campaign.messageservice_sid) { + return null; + } + const organization = await loaders.organization.load( + campaign.organization_id + ); + const serviceClient = getServiceFromOrganization(organization); + if (serviceClient.messageServiceLink) { + return serviceClient.messageServiceLink( + organization, + campaign.messageservice_sid + ); + } + return null; + }, + phoneNumbers: async (campaign, _, { user, loaders }) => { + await accessRequired( + user, + campaign.organization_id, + "SUPERVOLUNTEER", + true + ); + if (!campaign.messageservice_sid) { + return []; + } + const organization = await loaders.organization.load( + campaign.organization_id ); - return phoneNumbers.map(phoneNumber => phoneNumber.phoneNumber); + const serviceClient = getServiceFromOrganization(organization); + if (serviceClient.getPhoneNumbersForService) { + const phoneNumbers = await serviceClient.getPhoneNumbersForService( + organization, + campaign.messageservice_sid + ); + return phoneNumbers.map(phoneNumber => phoneNumber.phoneNumber); + } else { + return r + .knex("owned_phone_number") + .where({ + organization_id: campaign.organization_id, + allocated_to_id: campaign.id + }) + .select("phone_number") + .pluck("phone_number"); + } }, inventoryPhoneNumberCounts: async (campaign, _, { user, loaders }) => { + // TODO: consider removal (moved to extensions/service-managers/per-campaign-messageservices await accessRequired( user, campaign.organization_id, @@ -743,7 +785,7 @@ export const resolvers = { creator: async (campaign, _, { loaders }) => campaign.creator_id ? loaders.user.load(campaign.creator_id) : null, isArchivedPermanently: campaign => { - // TODO: consider removal + // TODO: consider removal (moved to extensions/service-managers/per-campaign-messageservices // started campaigns that have had their message service sid deleted can't be restarted // NOTE: this will need to change if campaign phone numbers are extended beyond twilio and fakeservice return ( diff --git a/src/server/api/organization.js b/src/server/api/organization.js index a514b699d..2004fe40c 100644 --- a/src/server/api/organization.js +++ b/src/server/api/organization.js @@ -273,6 +273,7 @@ export const resolvers = { ); }, campaignPhoneNumbersEnabled: async (organization, _, { user }) => { + // TODO: consider removal (moved to extensions/service-managers/per-campaign-messageservices await accessRequired(user, organization.id, "SUPERVOLUNTEER"); const inventoryEnabled = getConfig("EXPERIMENTAL_PHONE_INVENTORY", organization, { From ddaee87d1597ad420832e34cd0e7c1681692b809 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Wed, 25 Aug 2021 14:57:48 -0400 Subject: [PATCH 181/191] fix envs as strings --- src/extensions/contact-loaders/redash/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/contact-loaders/redash/index.js b/src/extensions/contact-loaders/redash/index.js index 651ee86cb..7705a9cc9 100644 --- a/src/extensions/contact-loaders/redash/index.js +++ b/src/extensions/contact-loaders/redash/index.js @@ -67,7 +67,7 @@ let baseTlsEnabled = process.env.NODE_TLS_REJECT_UNAUTHORIZED; const tlsVerification = { avoid: yesAvoid => { if (yesAvoid) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0; + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; } }, restore: () => { From 673fe7b867969fac93975b82c4acae6151e72e0b Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Wed, 25 Aug 2021 17:28:45 -0400 Subject: [PATCH 182/191] cypress debugging failure --- __test__/cypress/integration/basic-campaign-e2e.test.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/__test__/cypress/integration/basic-campaign-e2e.test.js b/__test__/cypress/integration/basic-campaign-e2e.test.js index 0c1e59b90..94285ac70 100644 --- a/__test__/cypress/integration/basic-campaign-e2e.test.js +++ b/__test__/cypress/integration/basic-campaign-e2e.test.js @@ -129,6 +129,8 @@ describe("End-to-end campaign flow", () => { /Hi ContactFirst(\d) this is TexterFirst, how are you\?/ ); }); + const x = cy.get("button[data-test=send]"); + cy.log("cypress before data-test=send", x, x && x.length); cy.get("button[data-test=send]") .eq(0) @@ -140,10 +142,6 @@ describe("End-to-end campaign flow", () => { /Hi ContactFirst(\d) this is TexterFirst, how are you\?/ ); }); - console.log( - "cypress before data-test=send", - cy.get("button[data-test=send]") - ); cy.get("button[data-test=send]") .eq(0) .click(); From d13f560bd01df1e5469665b746d70a54e01d46c7 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Wed, 25 Aug 2021 18:31:45 -0400 Subject: [PATCH 183/191] redash: more reliable tlsVerification settings --- .../contact-loaders/redash/index.js | 48 +++++++++---------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/src/extensions/contact-loaders/redash/index.js b/src/extensions/contact-loaders/redash/index.js index 7705a9cc9..23626b9bb 100644 --- a/src/extensions/contact-loaders/redash/index.js +++ b/src/extensions/contact-loaders/redash/index.js @@ -6,6 +6,7 @@ import { jobRunner } from "../../job-runners"; import { r, cacheableData } from "../../../server/models"; import { getConfig, hasConfig } from "../../../server/api/lib/config"; import httpRequest from "../../../server/lib/http-request"; +import https from "https"; export const name = "redash"; @@ -63,26 +64,12 @@ export async function getClientChoiceData(organization, campaign, user) { }; } -let baseTlsEnabled = process.env.NODE_TLS_REJECT_UNAUTHORIZED; -const tlsVerification = { - avoid: yesAvoid => { - if (yesAvoid) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; - } - }, - restore: () => { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = baseTlsEnabled; - } -}; - export async function downloadAndSaveResults( { queryId, resultId, job, redashUrl, organizationId }, contextVars ) { const organization = cacheableData.organization.load(organizationId); - const userApiKey = getConfig("REDASH_USER_API_KEY", organization); const baseUrl = getConfig("REDASH_BASE_URL", organization); - const tlsVerifyOff = getConfig("REDASH_TLS_VERIFY_OFF", organization); console.log( "redash.downloadAndSaveResults", @@ -93,13 +80,11 @@ export async function downloadAndSaveResults( ); // 3. Download result csv const resultsUrl = `${baseUrl}/api/queries/${queryId}/results/${resultId}.csv`; - tlsVerification.avoid(tlsVerifyOff); const resultsDataResult = await httpRequest(resultsUrl, { method: "get", - headers: { Authorization: `Key ${userApiKey}` }, - timeout: 900000 // 15 min + timeout: 900000, // 15 min + ...httpsArgs(organization) }); - tlsVerification.restore(); // 4. Parse CSV const { contacts, customFields, validationStats, error } = await new Promise( @@ -137,6 +122,22 @@ export async function downloadAndSaveResults( ); } +const httpsAgentNoVerify = new https.Agent({ + rejectUnauthorized: false +}); + +const httpsArgs = organization => { + const userApiKey = getConfig("REDASH_USER_API_KEY", organization); + const tlsVerifyOff = getConfig("REDASH_TLS_VERIFY_OFF", organization); + const extraArgs = { + headers: { Authorization: `Key ${userApiKey}` } + }; + if (tlsVerifyOff) { + extraArgs.agent = httpsAgentNoVerify; + } + return extraArgs; +}; + export async function processContactLoad(job, maxContacts, organization) { /// Trigger processing -- this will likely be the most important part /// you should load contacts into the contact table with the job.campaign_id @@ -169,7 +170,6 @@ export async function processContactLoad(job, maxContacts, organization) { const campaignId = job.campaign_id; const userApiKey = getConfig("REDASH_USER_API_KEY", organization); const baseUrl = getConfig("REDASH_BASE_URL", organization); - const tlsVerifyOff = getConfig("REDASH_TLS_VERIFY_OFF", organization); const { redashUrl } = JSON.parse(job.payload); if (!userApiKey || !baseUrl) { @@ -216,12 +216,11 @@ export async function processContactLoad(job, maxContacts, organization) { // 1. start query const startQueryUrl = `${baseUrl}/api/queries/${queryId}/refresh${params}`; console.log("REDASH 1"); - tlsVerification.avoid(tlsVerifyOff); let refreshRedashResult; try { refreshRedashResult = await httpRequest(startQueryUrl, { method: "post", - headers: { Authorization: `Key ${userApiKey}` } + ...httpsArgs(organization) }); } catch (err) { await failedContactLoad( @@ -232,7 +231,6 @@ export async function processContactLoad(job, maxContacts, organization) { ); return; } - tlsVerification.restore(); console.log( "refreshRedashResult", refreshRedashResult, @@ -252,10 +250,8 @@ export async function processContactLoad(job, maxContacts, organization) { const jobQueryId = redashJobData.job.id; const redashPollStatusUrl = `${baseUrl}/api/jobs/${jobQueryId}`; console.log("REDASH 2", redashJobData); - tlsVerification.avoid(tlsVerifyOff); const redashQueryCompleted = await httpRequest(redashPollStatusUrl, { method: "get", - headers: { Authorization: `Key ${userApiKey}` }, bodyRetryFunction: async res => { const json = await res.json(); console.log("statusValidation", json); @@ -264,9 +260,9 @@ export async function processContactLoad(job, maxContacts, organization) { }, retries: 1000, timeout: 900000, // 15 min - retryDelayMs: 3000 // 3 seconds + retryDelayMs: 3000, // 3 seconds + ...httpsArgs(organization) }); - tlsVerification.restore(); console.log("refreshDataResult", redashQueryCompleted); if (redashQueryCompleted.job.status === 4) { await failedContactLoad( From 8265437ee736005394fb04bc00e4da1c2131e589 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Thu, 26 Aug 2021 10:44:47 -0400 Subject: [PATCH 184/191] debugging cypress tests --- .../integration/basic-campaign-e2e.test.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/__test__/cypress/integration/basic-campaign-e2e.test.js b/__test__/cypress/integration/basic-campaign-e2e.test.js index 94285ac70..93725d75c 100644 --- a/__test__/cypress/integration/basic-campaign-e2e.test.js +++ b/__test__/cypress/integration/basic-campaign-e2e.test.js @@ -47,27 +47,29 @@ describe("End-to-end campaign flow", () => { cy.get("div.MuiPickersCalendarHeader-switchHeader > button:nth-child(3)") .first() .click(); - + console.log("LOG basic-campaign-e2e-test 1"); + cy.log("cyLOG basic-campaign-e2e-test 1"); // Click first of the month cy.get(".MuiPickersCalendar-week button:not(.MuiPickersDay-hidden)") .eq(3) .click(); - + console.log("LOG basic-campaign-e2e-test 2"); // Click okay on calendar cy.get(".MuiDialogActions-root button") .eq(1) .click(); - + console.log("LOG basic-campaign-e2e-test 3"); // Wait for modal to close then submit // TODO: use cy.waitUntil() instead of wait() cy.wait(400); cy.get("[data-test=campaignBasicsForm]").submit(); - + console.log("LOG basic-campaign-e2e-test 4"); // Upload Contacts cy.get("#contact-upload").attachFile("two-contacts.csv"), { force: true }; cy.wait(400); cy.get("button[data-test=submitContactsCsvUpload]").click(); - + console.log("LOG basic-campaign-e2e-test 5"); + cy.log("cyLOG basic-campaign-e2e-test 5"); // Assignments // Note: Material UI v0 AutoComplete component appears to require a click on the element // later versions should just allow you to hit enter @@ -124,12 +126,14 @@ describe("End-to-end campaign flow", () => { .find("button[data-test=sendFirstTexts]") .click(); cy.get("[name=messageText]").then(els => { - console.log(els[0]); + console.log("name=messageText", els[0]); expect(els[0].value).to.match( /Hi ContactFirst(\d) this is TexterFirst, how are you\?/ ); }); - const x = cy.get("button[data-test=send]"); + const x = cy.get("button[data-test=send]").then(els => { + console.log("button[data-test=send]", els[0]); + }); cy.log("cypress before data-test=send", x, x && x.length); cy.get("button[data-test=send]") From 5a87a8d5b19d6bd4fb939e6fc43bde2446cbe122 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Thu, 26 Aug 2021 10:59:07 -0400 Subject: [PATCH 185/191] debugging cypress tests --- .github/workflows/cypress-tests.yaml | 1 + __test__/cypress/integration/basic-campaign-e2e.test.js | 3 ++- .../default-dynamicassignment/react-component.js | 7 ++++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cypress-tests.yaml b/.github/workflows/cypress-tests.yaml index 869e9475a..31ed72ada 100644 --- a/.github/workflows/cypress-tests.yaml +++ b/.github/workflows/cypress-tests.yaml @@ -28,6 +28,7 @@ jobs: - name: Cypress run uses: cypress-io/github-action@v2 env: + DEBUG: '@cypress/github-action' NODE_ENV: test PORT: 3001 OUTPUT_DIR: ./build diff --git a/__test__/cypress/integration/basic-campaign-e2e.test.js b/__test__/cypress/integration/basic-campaign-e2e.test.js index 93725d75c..9408aaeab 100644 --- a/__test__/cypress/integration/basic-campaign-e2e.test.js +++ b/__test__/cypress/integration/basic-campaign-e2e.test.js @@ -152,7 +152,8 @@ describe("End-to-end campaign flow", () => { // Shows we're done and click back to /todos cy.get("body").contains("You've messaged all your assigned contacts."); - cy.get("button:contains(Back To Todos)").click(); + cy.log("cyLOG step: click back to /todos"); + cy.get("button[name=gotoTodos]").click(); cy.waitUntil(() => cy.url().then(url => url.match(/\/todos$/))); }); }); diff --git a/src/extensions/texter-sideboxes/default-dynamicassignment/react-component.js b/src/extensions/texter-sideboxes/default-dynamicassignment/react-component.js index 2d68db9be..d84d1c98c 100644 --- a/src/extensions/texter-sideboxes/default-dynamicassignment/react-component.js +++ b/src/extensions/texter-sideboxes/default-dynamicassignment/react-component.js @@ -7,6 +7,7 @@ import Button from "@material-ui/core/Button"; import { withRouter } from "react-router"; import gql from "graphql-tag"; import GSTextField from "../../../components/forms/GSTextField"; +import { dataTest } from "../../../lib/attributes"; import loadData from "../../../containers/hoc/load-data"; @@ -144,7 +145,11 @@ export class TexterSideboxClass extends React.Component { ) : null} {contact /*the empty list*/ ? (
-
From 3bb27d0dd8ae3e3c9c2c0c1bedec51d840e7085c Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Thu, 26 Aug 2021 11:22:06 -0400 Subject: [PATCH 186/191] cypress debugging failure --- .../integration/basic-campaign-e2e.test.js | 25 ++++++++----------- __test__/cypress/plugins/tasks.js | 5 ++++ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/__test__/cypress/integration/basic-campaign-e2e.test.js b/__test__/cypress/integration/basic-campaign-e2e.test.js index 9408aaeab..adb69a440 100644 --- a/__test__/cypress/integration/basic-campaign-e2e.test.js +++ b/__test__/cypress/integration/basic-campaign-e2e.test.js @@ -47,29 +47,29 @@ describe("End-to-end campaign flow", () => { cy.get("div.MuiPickersCalendarHeader-switchHeader > button:nth-child(3)") .first() .click(); - console.log("LOG basic-campaign-e2e-test 1"); - cy.log("cyLOG basic-campaign-e2e-test 1"); + cy.task("log", "cyLOG basic-campaign-e2e-test 1"); // Click first of the month cy.get(".MuiPickersCalendar-week button:not(.MuiPickersDay-hidden)") .eq(3) .click(); - console.log("LOG basic-campaign-e2e-test 2"); + cy.task("log", "cyLOG basic-campaign-e2e-test 2"); // Click okay on calendar + /* cy.get(".MuiDialogActions-root button") .eq(1) .click(); - console.log("LOG basic-campaign-e2e-test 3"); + */ + cy.task("log", "cyLOG basic-campaign-e2e-test 3"); // Wait for modal to close then submit // TODO: use cy.waitUntil() instead of wait() cy.wait(400); cy.get("[data-test=campaignBasicsForm]").submit(); - console.log("LOG basic-campaign-e2e-test 4"); + cy.task("log", "cyLOG basic-campaign-e2e-test 4"); // Upload Contacts cy.get("#contact-upload").attachFile("two-contacts.csv"), { force: true }; cy.wait(400); cy.get("button[data-test=submitContactsCsvUpload]").click(); - console.log("LOG basic-campaign-e2e-test 5"); - cy.log("cyLOG basic-campaign-e2e-test 5"); + cy.task("log", "cyLOG basic-campaign-e2e-test 5"); // Assignments // Note: Material UI v0 AutoComplete component appears to require a click on the element // later versions should just allow you to hit enter @@ -131,11 +131,7 @@ describe("End-to-end campaign flow", () => { /Hi ContactFirst(\d) this is TexterFirst, how are you\?/ ); }); - const x = cy.get("button[data-test=send]").then(els => { - console.log("button[data-test=send]", els[0]); - }); - cy.log("cypress before data-test=send", x, x && x.length); - + cy.task("log", "cyLOG basic-campaign-e2e-test 6"); cy.get("button[data-test=send]") .eq(0) .click(); @@ -152,8 +148,9 @@ describe("End-to-end campaign flow", () => { // Shows we're done and click back to /todos cy.get("body").contains("You've messaged all your assigned contacts."); - cy.log("cyLOG step: click back to /todos"); - cy.get("button[name=gotoTodos]").click(); + cy.task("log", "cyLOG basic-campaign-e2e-test 7"); + cy.get("button:contains(Back To Todos)").click(); + cy.task("log", "cyLOG basic-campaign-e2e-test 8"); cy.waitUntil(() => cy.url().then(url => url.match(/\/todos$/))); }); }); diff --git a/__test__/cypress/plugins/tasks.js b/__test__/cypress/plugins/tasks.js index dc9485962..ed32fcb59 100644 --- a/__test__/cypress/plugins/tasks.js +++ b/__test__/cypress/plugins/tasks.js @@ -52,6 +52,11 @@ export function defineTasks(on, config) { }); user.password = userInfo.password; return user; + }, + + log(message, arg1, arg2) { + console.log("LOG", message, arg1, arg2); + return null; } }); From 1c621667676a9ee54343ee2912cfaed708044070 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Thu, 26 Aug 2021 21:32:29 -0400 Subject: [PATCH 187/191] document new Spoke extensions --- docs/HOWTO-extend-spoke.md | 36 +++++--- docs/HOWTO-use-service-managers.md | 143 +++++++++++++++++++++++++++++ docs/HOWTO-use-service-vendors.md | 115 +++++++++++++++++++++++ 3 files changed, 279 insertions(+), 15 deletions(-) create mode 100644 docs/HOWTO-use-service-managers.md create mode 100644 docs/HOWTO-use-service-vendors.md diff --git a/docs/HOWTO-extend-spoke.md b/docs/HOWTO-extend-spoke.md index e0e9971f7..5bb87f98f 100644 --- a/docs/HOWTO-extend-spoke.md +++ b/docs/HOWTO-extend-spoke.md @@ -82,6 +82,23 @@ policies around what the conditions for getting a second batch are. The default before completing another batch. But this extension point allows different rules (per-campaign) to dictate when a texter can request more messages. +## [Service Managers](HOWTO-use-service-managers.md) + +Service Managers have been designed to cross-cut many hooks both across organization and campaign settings. +They can also implement features across different vendors. Besides having standardized ways to add/supplement +organization/campaign settings, they also have hooks for message sending, opt-outs, campaign start and archiving, +post-campaign contact load, and others. + +## [Service Vendors](HOWTO-use-service-vendors.md) + +Spoke has been developed to work best with Twilio at time of writing (Aug 2021). However, +you can set the DEFAULT_SERVICE either in the environment variable or in an organization's `features` column +to change the message service for that organization. Service Vendors need additional settings which can +be configured through their hooks into organization setup or through environment variables if you +have credentials, etc that cross all internal 'organizations' in Spoke. + +(Note: These were previously sometimes called "message services" but that was confusing with +Twilio's internal 'service' called Messaging/Message services -- so we now call these service *vendors*) # Other places to extend Spoke @@ -89,15 +106,6 @@ We are slowly trying to make the pattern that extensible parts of Spoke are in t src/extensions directory and behave in similar ways. However a couple have not been moved yet (If you have time, consider helping with a pull request to do so!) -## Message Services - -Spoke has been developed to work best with Twilio at time of writing (Aug 2020). However, -you can set the DEFAULT_SERVICE either in the environment variable or in an organization -to change the message service for that organization. Message services are currently in files -in the [src/server/api/lib](https://github.com/MoveOnOrg/Spoke/tree/main/src/server/api/lib) directory, -and adding a message service is possible by making - - ## Login We recommend production instances use Auth0 for login by default. Additionally for development, @@ -110,10 +118,8 @@ login through Slack. All of these implementations are *mostly* implemented in t However there are a couple places here or there that have hooks -- at least for more sophisticated integrations. Ideally we will consolidate these hooks into a src/extensions/login/ in the future. +## Secrets managers -# Future hooks - -* On-campaign-start and on-campaign-completion are good places to trigger some actions -* Just like contact-loaders hook in to ways to load contacts, it would be good to allow extensions - to load scripts and canned responses from other sources. There's already a Google Doc script import - option. Abstracting this could provide useful tools and integrations into other campaigning systems/APIs. +Currently, only encrypting locally in src/extensions/secret-manager/default-encrypt is implemented, +but you can implement a different manager and set SECRET_MANAGER= to use it instead. We imagine (and need) +implementations for Amazon Secrets Manager and Google's/Azure's secrets manangers. diff --git a/docs/HOWTO-use-service-managers.md b/docs/HOWTO-use-service-managers.md new file mode 100644 index 000000000..4807590e7 --- /dev/null +++ b/docs/HOWTO-use-service-managers.md @@ -0,0 +1,143 @@ +# Service Managers Extensions + +One of the core tasks in a Spoke campaign is loading in the contacts for texters to target. + +One default and standard way to do so is upload a CSV with their +names, cells, zips, and custom fields. So CSV uploading (`csv-upload`) is +one of many possible (existing or not yet) ways to load contacts for a campaign. + +There's a framework in Spoke that allows the System Administrator to enable additional +ways to load contacts in, as well as make it straightforward to for developers to +integrate into CRMs (Constituent Relationship Management systems) or other software/databases. + +## Enabling Service Managers + +Service managers enabled by an organization in the system are managed by the +environment variable `SERVICE_MANAGERS` (or in the JSON column `organization.features` for a particular organization). +Unset, the default includes nothing. +The value should be the list of enabled contact loaders separated by `,` (commas) +without any spaces before or after the comma. E.g. One to enable testing would be `SERVICE_MANAGERS=test-fake-example,scrub-bad-mobilenums` + +Service managers often have consequences for your service-vendor -- i.e. they can incur costs or +affect how your system works. It's important to understand what a particular service manager does +before enabling it -- that is both the power and the risk of these extensions. + +## Included Service Managers + +### per-campaign-messageservices + +This service manager is intended for Twilio -- twilio restricts the numbers available to a +particular message service in your account, so this creates a new message service per-campaign. +It supports managing moving numbers to/from the general phone numbers bought +(requires PHONE_INVENTORY=1) before a campaign starts, and then after archiving the campaign, +you can (and should) release those numbers back to the general pool. + +Previously this functionality was 'built in' when EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS was set. +That setting is no longer supported and you should enable this service manager instead. + +### scrub-bad-mobilenums + +COSTS: service vendors charge for this -- lookup their "lookup api" pricing. + +This service manager blocks campaign start until one presses a Scrub numbers button in the +Service Managers campaign edit section. Clicking the button triggers lookups of all numbers +uploaded looking for numbers that cannot be texted (e.g. landlines). It then removes those +from the contacts for that campaign. + +This scrubbing data is also remembered for future campaigns, so new uploads for the same numbers +won't incur the same cost and only truly new numbers will be looked up. + +### numpicker-basic + +Must be enabled for Bandwidth.com -- Bandwidth does not support automatic number selection like +Twilio's message service system does. numpicker-basic rotates bought numbers to send to contacts. +It complements well with sticky-sender (below) which remembers which number was used the last time +a contact was sent. This is the most *basic* numpicker -- we expect that future service managers +will have more sophisticated algorithms which take into account past success with phone numbers +maybe depending on the carrier per-contact. + +### sticky-sender + + + +### test-fake-example + +Use this to test and understand all the hooks available to service-managers. If you +are developing a service manager, this is a good directory to copy and then edit/remove +the parts you don't need and implement your service manager. + +### carrier-lookup + +EXPERIMENTAL: carrier-lookup will lookup the carrier of a contact after receiving a delivery report. +At the moment, we recommend only turning this on using Bandwidth.com since carrier info on +delivery reports is free for that vendor. Twilio charges for all carrier lookups and this would be +expensive as it's per-message -- not even per-contact. If you are using Twilio, consider `scrub-bad-mobilenums` which does lookups before starting a campaign. + + +## Developing Service Managers + +The best way to make a new service manager is something like this in the codebase: + +``` +cd src/extensions/service-managers +cp -rp test-fake-example +``` + +Then edit the two files (index.js and react-component.js) in that directory. + +Service managers implement common functions that are called and used in different parts of the application. + +### What to implement + +The first, `index.js` is the backend -- You'll need to change the exported `name` value +and then implement each of the functions included or delete them. All functions besides `metadata()` +are optional and you should delete them if not used -- some, merely by existing can incur a +performance cost on Spoke. + +Here are the hooks available + +- `metadata()` -- REQUIRED: return displayName, description, and several other variables. displayName and description may be presented to the administrators. supportsOrgConfig and supportsCampaignConfig are + also required keys and will correspond with implementations in react-component.js +- `onMessageSend({ message, contact, organization, campaign})` -- called just before a message + is sent from a message vendor. `message` can be changed in-place. Returned values can include + useful values for the service *vendor* to consume, e.g. user_number and/or messageservice_sid + which will be updated on the message. Make sure any value you return is actually used by + the service vendor(s) you need to support -- they do not need to heed this data. +- `onDeliveryReport({ contactNumber, userNumber, messageSid, service, messageServiceSid, newStatus, errorCode, organization, campaignContact, lookup })` -- when a message service receives a delivery + report. Not all these variables are reliably present -- again, it depends on the service vendor. + This function, simply by existing, can add a performance cost since organization and other variables + need to be looked up to call it. +- `onOptOut({ organization, contact, campaign, user, noReply, reason, assignmentId })` -- triggered + on opt-outs. + This function, simply by existing, can add a performance cost since organization and other variables + need to be looked up to call it. +- `getCampaignData({ organization, campaign, user, loaders, fromCampaignStatsPage })` -- from + a campaign admin edit or stats page, this is called. When it's the stats page, fromCampaignStatsPage=true. + When implemented, this should return a dict with JSON data in `data` key. + For the admin edit page , `fullyConfigured` should be false to block campaign start -- null or true otherwise. + For the stats page after the campaign is archived , `unArchiveable` should be false to block un-archiving + (e.g. for per-campaign-messageservice, after releasing phone numbers the campaign cannot be unarchived). +- `onCampaignUpdateSignal({ organization, campaign, user, updateData, fromCampaignStatsPage})` -- + this complements getCampaignData -- when react-component.js implements `CampaignConfig` or `CampaignStats` + then the onSubmit call can send updateData which will be passed back through to this method. You + can process this data and return updates to data, fullyConfigured, and unArchiveable which will re-update + the front-end interface. +- `getOrganizationData({ organization, user, loaders})` -- the same as getCampaignData, but for the + organization Settings config in admin +- `onOrganizationUpdateSignal({ organization, user, updateData })` -- the complement to onCampaignUpdateSignal but for the organization Settings config page and `OrgConfig` react-component.js exported object. +- `onCampaignContactLoad()` -- (see test-fake-example for args) -- triggered after a campaign's + contacts have been successfully uploaded -- this can scrub (or add) contacts further or make other changes. +- `onOrganizationServiceVendorSetup()` -- (see test-fake-example for args) -- triggered after + a service-vendor is configured in Settings. +- `onCampaignStart({ organization, campaign, user })` -- triggered when a campaign has been + successfully started. +- `onCampaignArchive({})` -- triggered when a campaign has been archived +- `onCampaignUnArchive({})` -- triggered when a campaign has been unarchived + + +Then you may want to implement `react-component.js`. In here you can export several react components +that are used in different contexts. See test-fake-example for examples. You can implement and export: +- `OrgConfig` -- added to the Settings page and will receive and send data from/to your index.js getOrganizationData and onOrganizationUpdateSignal respectively. +- `CampaignConfig` -- shown in Campaign admin edit section +- `CampaignStats` -- show at the top of the Campaign admin stats page if implemented (and does not render to null) + diff --git a/docs/HOWTO-use-service-vendors.md b/docs/HOWTO-use-service-vendors.md new file mode 100644 index 000000000..ee892b955 --- /dev/null +++ b/docs/HOWTO-use-service-vendors.md @@ -0,0 +1,115 @@ +# Service Vendors Handlers + +For Spoke to send and receive text messages we need to connect to a vendor that has an API +to send/receive SMS/MMS! Historically, Spoke has worked with Twilio.com. Service Vendors +abstract this to support other vendors. There is an experimental Bandwidth.com implementation, +and we hope/expect others to follow. + +Implementations should set DEFAULT_SERVICE=twilio or the vendor service being used. +This can also be set in a Spoke organization's `features` column as JSON (e.g. `{"DEFAULT_SERVICE": "twilio"}`). + +## Service Vendor configuration options + +Service Vendor implementations should and do accept global environment variable (or organization features settings) values. +However, there is also an implementation in each one where account settings can be configured in the +*Settings* tab in the organization admin panel (organization OWNER permissions are required). + +## Included Service Vendors + +### twilio + +The default production service-vendor. Set DEFAULT_SERVICE=twilio -- configuration can be done in +Settings organization tab if TWILIO_MULTI_ORG=1. + +Alternatively, if you want the same creds for all organizations, then set +the variables TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_MESSAGE_SERVICE_SID. + +See more at [How to integrate Twilio](HOWTO_INTEGRATE_TWILIO.md) + +### fakeservice (for development/testing) + +We recommend using this implementation during development/testing so you don't need to +create an account or pay for 'real messages' to be sent. DEFAULT_SERVICE=fakeservice +supports many aspects like phone number buying, sending messages, replies (any message +sent with the text "autorespond" in it, will automatically receive a reply) -- all fake, +but this helps with dev/testing features supported in Twilio, Bandwidth and other service-vendors. + +### bandwidth (experimental) + +Bandwidth.com is another telephone service API company. Setting DEFAULT_SERVICE=bandwidth +should work, but this implementation has not yet been used in production -- just for testing, +so there may be issues with it. Please create an issue or contact us if you are using this +implementation (successfully or not). Besides setting DEFAULT_SERVICE, bandwidth won't work +without two service managers -- make sure to include SERVICE_MANAGERS=numpicker-basic,sticky-sender + +### nexmo (broken, unknown status) + +Nexmo support existed when MoveOn officially adopted the project in 2017 but +has never been used -- very little work/attention has been done to maintain this implementation. +It likely needs some development to bring support in, which we would encourage and can help +with, but have no plans to do so. + +## Developing Service Vendors + +The best way to make a new message handler is something like this in the codebase: + +``` +cd src/extensions/service-vendors +cp -rp fakeservice +``` + +Then edit index.js in that directory. + + +### API properties for message handlers + +Export methods/constants with these names from your index.js + +#### Required functions + +- `getMetadata()` -- must return dict with `name` and `supportsOrgConfig` boolean +- `sendMessage({ message, contact, trx, organization, campaign, serviceManagerData })` -- sends a message. + At this point, the `message` record has been saved, likely with send_status="SENDING". + Message service onMessageSend() hooks have also been called and any returned info has been compiled into + serviceManagerData. trx value will be non-null/empty if the message is being sent inside a SQL transaction. +- `errorDescriptions` should be an exported object with error number keys and strings as + values to what they mean. +- `errorDescription(errorCode)` - should return an object with code, description, link keys with the latter + being a link to public api documentation on what the error is/means from the vendor service. +- `addServerEndpoints(addPostRoute)` - call addPostRoute(expressRouteString, func1, ....) which is + passed to express app.post() arguments -- this is important for handling receiving messages and delivery reports. +- `handleIncomingMessage(message)` -- not technically required but likely a method you should implement + so that it's +- `fullyConfigured(organization, serviceManagerData)` - should return a boolean on whether the + service is ready to support texting. serviceManagerData is the result of service managers that are + enabled and have implemented onOrganizationServiceVendorSetup() + +#### functions for PHONE_INVENTORY support + +- `buyNumbersInAreaCode(organization, areaCode, limit)` +- `deleteNumbersInAreaCode(organization, areaCode)` +- `addNumbersToMessagingService()` +- `clearMessagingServicePhones()` + +#### methods to support org config/setup + +- `getServiceConfig({ serviceConfig, organization, options = { obscureSensitiveInformation, restrictToOrgFeatures } })` - returns a JSONable object that is passed to your react-component.js in Settings for service vendor section. Be careful to return fake/obscured info when options.obscureSensitiveInformation=true (e.g. `` Where possible, you should use getSecret and convertSecret from secret-manager extensions. +- `updateConfig(oldConfig, config, organization, serviceManagerData)` -- called when onSubmit is called + from your react-component.js org config component -- this should save data into organization. Where possible, + you should use getSecret and convertSecret from extensions/secret-manager so that secrets can be encrypted + or stored in a secrets backend system. + +#### methods for message services + +If your vendor has a concept of a 'messaging service' then implement these: + +- `getMessageServiceSid(organization, contact, messageText)` -- can return a message service + value based on organization and contact + + +#### other optional methods +- `getContactInfo({ organization, contactNumber, lookupName })` - if the vendor service has + a way to reverse lookup phone number data like carrier and name, then this is how to do it. +- `getFreeContactInfo()` - same args as above -- this indicates that info can be looked up for + free and will be preferred when possible and available over the former method. + From 33132036aa02034986e6fba74898e5ff0833a1a9 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Thu, 26 Aug 2021 21:49:57 -0400 Subject: [PATCH 188/191] scrub-bad-mobilenums: twilio should also scrub disconnected and incorrect country-codes --- docs/HOWTO-use-service-vendors.md | 8 +++++++- .../service-managers/scrub-bad-mobilenums/index.js | 10 +++++++--- src/extensions/service-vendors/twilio/index.js | 13 ++++++++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/docs/HOWTO-use-service-vendors.md b/docs/HOWTO-use-service-vendors.md index ee892b955..57a98caf5 100644 --- a/docs/HOWTO-use-service-vendors.md +++ b/docs/HOWTO-use-service-vendors.md @@ -107,7 +107,13 @@ If your vendor has a concept of a 'messaging service' then implement these: value based on organization and contact -#### other optional methods +#### Contact lookup methods + +For both of these they should return values that must include `contact_number`, `organization_id`, and `service` (name, e.g. "twilio"). Other values should only be included in the result if they should override any existing data in organization_contact table. + +The key `status_code` should be 0 or positive if the number is textable. It should be negative if it is not +textable. Specifically, -1 for landlines, -2 for a non-existent/unserviced number, -3 for a bad country code match from PHONE_NUMBER_COUNTRY if that is set. Add other negative (or positive) values if useful to distinguish them. + - `getContactInfo({ organization, contactNumber, lookupName })` - if the vendor service has a way to reverse lookup phone number data like carrier and name, then this is how to do it. - `getFreeContactInfo()` - same args as above -- this indicates that info can be looked up for diff --git a/src/extensions/service-managers/scrub-bad-mobilenums/index.js b/src/extensions/service-managers/scrub-bad-mobilenums/index.js index 250a8497c..9d5ca2627 100644 --- a/src/extensions/service-managers/scrub-bad-mobilenums/index.js +++ b/src/extensions/service-managers/scrub-bad-mobilenums/index.js @@ -123,7 +123,9 @@ export async function getCampaignData({ export async function processJobNumberLookups(job, payload) { // called async from onCampaignUpdateSignal - const organization = cacheableData.organization.load(job.organization_id); + const organization = await cacheableData.organization.load( + job.organization_id + ); console.log( "processJobNumberLookups", job, @@ -146,11 +148,12 @@ export async function processJobNumberLookups(job, payload) { const chunk = chunks[i]; const lookupChunk = await Promise.all( chunk.map(async contact => { - console.log("scrub.lookupChunk", contact); + // console.log("scrub.lookupChunk", contact); const info = await serviceClient.getContactInfo({ organization, contactNumber: contact.cell }); + // console.log('scrub-bad-mobilenums lookup result', info); return { ...contact, ...info @@ -158,11 +161,12 @@ export async function processJobNumberLookups(job, payload) { }) ); const orgContacts = lookupChunk.map( - ({ id, cell, status_code, carrier, lookup_name }) => ({ + ({ id, cell, status_code, carrier, lookup_name, last_error_code }) => ({ id, organization_id: job.organization_id, contact_number: cell, status_code, + last_error_code, lookup_name, carrier, last_lookup: new Date() diff --git a/src/extensions/service-vendors/twilio/index.js b/src/extensions/service-vendors/twilio/index.js index c8697dab1..1cd05e83d 100644 --- a/src/extensions/service-vendors/twilio/index.js +++ b/src/extensions/service-vendors/twilio/index.js @@ -586,6 +586,7 @@ export async function getContactInfo({ const phoneNumber = await twilio.lookups.v1 .phoneNumbers(contactNumber) .fetch({ type: types }); + // console.log('twilio getContactInfo', phoneNumber); const contactInfo = { contact_number: contactNumber, organization_id: organization.id, @@ -594,10 +595,20 @@ export async function getContactInfo({ if (phoneNumber.carrier) { contactInfo.carrier = phoneNumber.carrier.name; } - if (phoneNumber.carrier.type) { + if (phoneNumber.carrier.error_code) { + // e.g. 60600: Unprovisioned or Out of Coverage + contactInfo.status_code = -2; + contactInfo.last_error_code = phoneNumber.carrier.error_code; + } else if ( + getConfig("PHONE_NUMBER_COUNTRY", organization) && + getConfig("PHONE_NUMBER_COUNTRY", organization) !== contactInfo.countryCode + ) { + contactInfo.status_code = -3; // wrong country + } else if (phoneNumber.carrier.type) { // landline, mobile, voip, contactInfo.status_code = phoneNumber.carrier.type === "landline" ? -1 : 1; } + if (phoneNumber.callerName) { contactInfo.lookup_name = phoneNumber.callerName; } From 98b3c7547126bc52821d8b1e080a2bd13278bfff Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Fri, 27 Aug 2021 12:13:15 -0400 Subject: [PATCH 189/191] scrub-bad-mobilenums: delete status_code < 0 more general than just landlines (-2: disconnected nums, -3: different country) --- .../service-managers/scrub-bad-mobilenums/index.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/extensions/service-managers/scrub-bad-mobilenums/index.js b/src/extensions/service-managers/scrub-bad-mobilenums/index.js index 9d5ca2627..767d34b1e 100644 --- a/src/extensions/service-managers/scrub-bad-mobilenums/index.js +++ b/src/extensions/service-managers/scrub-bad-mobilenums/index.js @@ -64,10 +64,8 @@ const deleteLandlineContacts = campaignId => "organization_contact.contact_number", "campaign_contact.cell" ) - .where({ - campaign_id: campaignId, - status_code: -1 // landlines - }) + .where("campaign_id", campaignId) + .where("status_code", "<", 0) // landlines and other non-textables ) .where("campaign_contact.campaign_id", campaignId) .delete(); From 06284b68cd720f826a902a3588d5d309958996b1 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Fri, 27 Aug 2021 12:34:36 -0400 Subject: [PATCH 190/191] release notes for v11.0 --- README.md | 4 ++-- docs/RELEASE_NOTES.md | 55 +++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b3b754f47..56880e196 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Spoke is an open source text-distribution tool for organizations to mobilize sup Spoke was created by Saikat Chakrabarti and Sheena Pakanati, and is now maintained by MoveOn.org. -The latest version is [10.2](https://github.com/MoveOnOrg/Spoke/tree/v10.2) (see [release notes](https://github.com/MoveOnOrg/Spoke/blob/main/docs/RELEASE_NOTES.md#v102)) +The latest version is [11.0](https://github.com/MoveOnOrg/Spoke/tree/v11.0) (see [release notes](https://github.com/MoveOnOrg/Spoke/blob/main/docs/RELEASE_NOTES.md#v110)) ## Setting up Spoke @@ -24,7 +24,7 @@ Want to know more? ### Quick Start with Heroku This version of Spoke suitable for testing and, potentially, for small campaigns. This won't cost any money and will not support production(aka large-scale) usage. It's a great way to practice deploying Spoke or see it in action. - + Deploy diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md index b25d5499a..d55aef936 100644 --- a/docs/RELEASE_NOTES.md +++ b/docs/RELEASE_NOTES.md @@ -1,5 +1,60 @@ # Release Notes +## v11.0 + +_August 2021:_ Version 11.0 + +This major release upgrades several backend libraries and significantly extends and refactors the way vendors (e.g. Twilio) are connected. First, Stefan Hayden has helped us upgrade our Material UI library -- so all components may look *slightly* different in style, but nothing should look unfamiliar. This will make future UI improvements much easier and for developer-contributors to use current documentation and resources (and less buggy!) for continuing UI evolution. + +More details below for a few migration steps depending on your deployment: +* There is a database migration for anyone upgrading -- instances with a very large message table have special instructions +* AWS Lambda deployments have slightly different deployment commands now +* Anyone using EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS or EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE require setup changes (these *were* experimental features) + +### Improvements + +#### Service vendors and Service managers extensions + +Based on work from Larry Person, there is a large refactor of "service-vendors" which makes it easier to contain the code to support a vendor connection like Twilio (and others -- there is an experimental Bandwidth.com implementation now, as well). Service Managers is in-turn an extension-api that allows one to hook into service-vendors and other campaign events easily. Adam Greenspan has created a Sticky Sender feature which allows one to keep the same phone number across conversations, so e.g. Twilio message services aren't necessary. + +#### Additional changes + +* Redis upgrade -- please report any issues -- new Heroku installs support Redis 6 which requires a TLS connection +* keyboard shortcuts for advancing left/right +* service-managers: carrier-lookup, scrub-bad-mobilenums, and per-campaign-messageservices (replacing EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE) +* NGPVAN updates and fixes to use their changed/most recent API + +### Migration Steps + +#### Database upgrades +* This is a major release and includes a schema change. +* For small instances simply leave/set SUPPRESS_MIGRATIONS="" or for [AWS Lambda, see the db migration instructions](https://moveonorg.github.io/Spoke/#/HOWTO_DEPLOYING_AWS_LAMBDA?id=migrating-the-database). +* For instances with a large `message` table, we recommend setting NO_INDEX_CHANGES=1 before running the migration, and then manually running two commands: + * `CREATE INDEX CONCURRENTLY cell_msgsvc_user_number_idx ON message (contact_number, messageservice_sid, user_number);` + * `DROP INDEX cell_messageservice_sid_idx;` + +The schema changes include adding a new table `organization_contact` which will track contacts across campaigns in an organization -- for things like subscription_status (in future), whether the number is a landline or what number has been used to contact them in the past (for 'sticky' sending). We also add user_number at the end of already-indexed cell-messageservice, to make it easier to search for user_numbers (also for sticky sending features). + + +#### AWS Deployment changes + +Instead of running a single 'claudia' command, You will need to tweak two things: +* Add an environment variable `ASSETS_DIR_PREBUILT` and set it to the absolute directory of your deployment checkout directory + "/build/client" (or whatever you have your ASSETS_DIR var set to). For example, on a Mac it might be something like "/Users/Sky/spoke/build/client" +* Instead of a single deployment command, you will first need to run + 1. `ASSETS_DIR=./build/client/assets ASSETS_MAP_FILE=assets.json NODE_ENV=production yarn prod-build-client` + 2. and then for your `claudia update` command you need to include your usual command line parameters and ADD `--no-optional-dependencies` + +These steps remove the client-side libraries from the server-side build, which is necessary now that the client-side libraries are too large to 'fit' into an AWS Lambda server deploy file. This is documented in the [AWS setup/deploy steps](https://github.com/MoveOnOrg/Spoke/compare/main...stage-main-11-0#diff-548e8f704ad84645a42f2efaf1332490f6844d0a0dd50e9ac6b931c198d213f3) + +#### Changes for Experimental Per-campaign phone numbers/message services + +For those that used the experimental feature EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS, it has been moved and refactored into a or Service Manager extension -- for these experimental installs (ONLY!), change to setting SERVICE_MANAGERS=per-campaign-messageservices + +EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE is no longer supported. Please create an issue if you still have a use-case for this -- there is tentative work to move its functionality into per-campaign-messageservices as well, but only if it still has users. + +### Appreciations +* [Adam Greenspan](agreenspan24), [Akash Jairam](https://github.com/Akash-Jairam), [Arique Aguilar](https://github.com/Arique1104) (our new Community Manager -- Welcome!), [Cody Gordon](https://github.com/codygordon), [Fryda Guedes](https://github.com/Frydafly), [Kathy Nguyen](https://github.com/crayolakat) [Schuyler Duveen](https://github.com/schuyler1d), [Stefan Hayden](https://github.com/stefanhayden), and Mark Houghton and [Barbara Atkins](https://github.com/bdatkins) for QA + ## v10.2 diff --git a/package.json b/package.json index caeac02c1..05e2b0c5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "spoke", - "version": "10.2.0", + "version": "11.0.0", "description": "Spoke", "main": "src/server", "engines": { From b6053f0450b98f7070b94d360431fa13dbe29986 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Fri, 27 Aug 2021 12:58:06 -0400 Subject: [PATCH 191/191] missed to credits for v11.0 --- docs/RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md index d55aef936..02879714b 100644 --- a/docs/RELEASE_NOTES.md +++ b/docs/RELEASE_NOTES.md @@ -53,7 +53,7 @@ For those that used the experimental feature EXPERIMENTAL_CAMPAIGN_PHONE_NUMBERS EXPERIMENTAL_TWILIO_PER_CAMPAIGN_MESSAGING_SERVICE is no longer supported. Please create an issue if you still have a use-case for this -- there is tentative work to move its functionality into per-campaign-messageservices as well, but only if it still has users. ### Appreciations -* [Adam Greenspan](agreenspan24), [Akash Jairam](https://github.com/Akash-Jairam), [Arique Aguilar](https://github.com/Arique1104) (our new Community Manager -- Welcome!), [Cody Gordon](https://github.com/codygordon), [Fryda Guedes](https://github.com/Frydafly), [Kathy Nguyen](https://github.com/crayolakat) [Schuyler Duveen](https://github.com/schuyler1d), [Stefan Hayden](https://github.com/stefanhayden), and Mark Houghton and [Barbara Atkins](https://github.com/bdatkins) for QA +* [Adam Greenspan](agreenspan24), [Akash Jairam](https://github.com/Akash-Jairam), [Arique Aguilar](https://github.com/Arique1104) (our new Community Manager -- Welcome!), [Asha Sulaiman](https://github.com/asha15), [Cody Gordon](https://github.com/codygordon), [Fryda Guedes](https://github.com/Frydafly), [Kathy Nguyen](https://github.com/crayolakat), [Neely Kartha](https://github.com/nkartha2), [Schuyler Duveen](https://github.com/schuyler1d), [Stefan Hayden](https://github.com/stefanhayden), and Mark Houghton and [Barbara Atkins](https://github.com/bdatkins) for QA ## v10.2