0

fine people of Stack Overflow.

I'm trying to solve a problem I'm having involving twilio functions, messaging services, and databases.

What I'm attempting to do is send a message to all members of a database at once.

My code is a mess, as Javascript isn't my native language and I'm rather new to twilio.

The problem I believe I'm having is with the async/await feature of javascript.

Here is my code so far:

// Boiler Plate Deta Code
const { Deta } = require("deta");

// Function to access database and get object of conta
async function FetchDB(db) {
    let res = await db.fetch();
    allItems = res.items;
    
    // continue fetching until last is not seen
    while (res.last){
      res = await db.fetch({}, {last: res.last});
      allItems = allItems.concat(res.items);
    }
}

// Function to get total number of contacts.
async function ReturnNumberOfContacts(allItems) {
  number_of_contacts = allItems.length;
}

// Function to send message to contact in database.
async function SendMessages(allItems, message) {
       allItems.forEach(contact => {
       let users_name = contact.name
         client.messages
          .create({
            body: `Hey ${users_name}! ${message}`,
            messagingServiceSid: messaging_service,
            to: contact.key
         })
    });
}

// Function to submit response to broadcaster.
async function SuccessResponse(user_name, number_of_contacts) {
   responseObject = {
    "actions": [
      {
        "say": `${user_name}, your broadcast has successfully sent to ${number_of_contacts} contacts.`
      },
      {
        "listen": true
      }
    ]
  }

}

// Main Function
exports.handler = async function(context, event, callback) {
  
  // Placeholder for number of contacts
  let number_of_contacts;
  
  // Place holder for object from database of all contacts
  let allItems;
  
  // Placeholder for users message
  let message;
  
  // Placeholder for response to user
    let responseObject;
  
  //Twilio and Deta, Etc Const
  const client = require('twilio')(context.ACCOUNT_SID, context.AUTH_TOKEN);
  const deta = Deta(context.DETA_PROJECT_KEY);
  const db = deta.Base("users2");
  const messaging_service = context.MESSAGING_SERVICE;
  
  // From Phone Number
  const from = event.UserIdentifier;
  
  // Parse memory
  const memory = JSON.parse(event.Memory);

  // Fetch all items from database and return total number of contacts.
  // Update relavent variables
  await FetchDB(db, allItems).then(ReturnNumberOfContacts(allItems));
  
  // Figure out if message came from short circuit broadcast or normal
  if (memory.triggered) {
    message = memory.message;
  } else {
    message = memory.twilio.collected_data.broadcast_message.answers.message_input.answer;
  }
  
  // Check if verified and set name.
  const current_user = await db.get(from);
  
  // Get the current users name or set a default value
  let user_name = current_user.name || "friend";

  // Determine if user is an authorized broadcaster
  if (from === context.BROADCAST_NUMBER) {
  
  // Decide if the sending of a message should be cancelled.
  if (message.toLowerCase() === "c" || message.toLowerCase() === "cancel") {
    responseObject = {
      "actions": [
        {
          "say": `${user_name}, you have canceled your request and no messages have been sent.`
       },
       {
          "listen": false
       }
      ]
   }
  // Return Callback and end task
  callback(null, responseObject);
  }
  // Move forward with sending a message.
  else {
      // Send message to users in database and send success message to broadcaster.
      await SendMessages(message, client, messaging_service)
      .then(SuccessResponse(user_name, number_of_contacts))
      return callback(null, responseObject);
  }

// The user is not authorized so return this.
}
  return callback(null, {
    "actions": [
      {
        "say": "You are not authorized to broadcast."
      },
      {
        "listen": false
      }
    ]
  })
};

So when the Fetch() function is triggered, I want the database to load a list of everyone and have twilio send them the desired message saved in the message variable. I have the code working so that I can read from the database and get the proper values, and send a single text message with the desired message, but the problem I'm having now is integrating it all together.

Thanks if anyone can point me in the right direction here.

Again, I'm new to javascript and more specifically asynchronous programming.

  • This is the error I'm getting right now >>> Function execution resulted in an error log: UnhandledPromiseRejectionWarning: Unhandled promise rejection: TypeError: Cannot read property 'length' of undefined at ReturnNumberOfContacts (/var/task/handlers/ZN166fcaef71e8b34f546263b2198e8d04.js:18:33) at Object.exports.handler (/var/task/handlers/ZN166fcaef71e8b34f546263b2198e8d04.js:78:36) at Object.exports.handler (/var/task/node_modules/runtime-handler/index.js:310:10) at Runtime.exports.handler (/var/task/runtime-handler.js:17:17) at Runtime.handleOnce (/var/run... – Develop With Data Sep 11 '21 at 01:46
  • Also, I'm building this within twilio functions. – Develop With Data Sep 11 '21 at 01:47

1 Answers1

0

Twilio developer evangelist here.

The issue from the error says that allItems is undefined when you call ReturnNumberOfContacts.

I think the issue comes from trying to use allItems as a sort of global variable, same for number_of_contacts. It would be better for FetchDB to resolve with the list of items and ReturnNumberOfContacts to resolve with the number of the items.

You also have some arguments missing when you call SendMessages in your function. I've updated it to the point that I think it will work:

// Boiler Plate Deta Code
const { Deta } = require("deta");

// Function to access database and get object of conta
async function FetchDB(db) {
  let res = await db.fetch();
  let allItems = res.items;

  // continue fetching until last is not seen
  while (res.last) {
    res = await db.fetch({}, { last: res.last });
    allItems = allItems.concat(res.items);
  }
  return allItems;
}

// Function to send message to contact in database.
async function SendMessages(allItems, message, client, messagingService) {
  return Promise.all(
    allItems.map((contact) => {
      let usersName = contact.name;
      return client.messages.create({
        body: `Hey ${usersName}! ${message}`,
        messagingServiceSid: messagingService,
        to: contact.key,
      });
    })
  );
}

// Main Function
exports.handler = async function (context, event, callback) {
  // Placeholder for users message
  let message;

  //Twilio and Deta, Etc Const
  const client = require("twilio")(context.ACCOUNT_SID, context.AUTH_TOKEN);
  const deta = Deta(context.DETA_PROJECT_KEY);
  const db = deta.Base("users2");
  const messagingService = context.MESSAGING_SERVICE;

  // From Phone Number
  const from = event.UserIdentifier;

  // Parse memory
  const memory = JSON.parse(event.Memory);

  // Fetch all items from database and return total number of contacts.
  // Update relavent variables
  const allItems = await FetchDB(db);
  const numberOfContacts = allItems.length;

  // Figure out if message came from short circuit broadcast or normal
  if (memory.triggered) {
    message = memory.message;
  } else {
    message =
      memory.twilio.collected_data.broadcast_message.answers.message_input
        .answer;
  }

  // Check if verified and set name.
  const currentUser = await db.get(from);

  // Get the current users name or set a default value
  let userName = currentUser.name || "friend";

  // Determine if user is an authorized broadcaster
  if (from === context.BROADCAST_NUMBER) {
    // Decide if the sending of a message should be cancelled.
    if (message.toLowerCase() === "c" || message.toLowerCase() === "cancel") {
      // Return Callback and end task
      callback(null, {
        actions: [
          {
            say: `${userName}, you have canceled your request and no messages have been sent.`,
          },
          {
            listen: false,
          },
        ],
      });
    }
    // Move forward with sending a message.
    else {
      // Send message to users in database and send success message to broadcaster.
      await SendMessages(allItems, message, client, messagingService);
      return callback(null, {
        actions: [
          {
            say: `${userName}, your broadcast has successfully sent to ${numberOfContacts} contacts.`,
          },
          {
            listen: true,
          },
        ],
      });
    }

    // The user is not authorized so return this.
  }
  return callback(null, {
    actions: [
      {
        say: "You are not authorized to broadcast.",
      },
      {
        listen: false,
      },
    ],
  });
};

What I did here was change FetchDB to only take the db as an argument, then to create a local allItems variable that collects all the contacts and then returns them.

async function FetchDB(db) {
  let res = await db.fetch();
  let allItems = res.items;

  // continue fetching until last is not seen
  while (res.last) {
    res = await db.fetch({}, { last: res.last });
    allItems = allItems.concat(res.items);
  }
  return allItems;
}

This is then called in the main body of the function to assign a local variable. I also replaced the ReturnNumberOfContacts function with a simple assignment.

  const allItems = await FetchDB(db);
  const numberOfContacts = allItems.length;

One thing you may want to consider is how many contacts you are trying to send messages to during this function. There are a few limits you need to be aware of.

Firstly, Function execution time is limited to 10 seconds so you need to make sure you can load and send all your messages within that amount of time if you want to use a Twilio Function for this.

Also, there are limits for the number of concurrent connections you can make to the Twilio API. That limit used to be 100 connections per account, but it may vary these days. When sending asynchronous API requests as you do in JavaScript, the platform will attempt to create as many connections to the API that it can in order to trigger all the requests asynchronously. If you have more than 100 contacts you are trying to send messages to here, that will quickly exhaust your available concurrent connections and you will receive 429 errors. You may choose to use a queue, like p-queue, to ensure your concurrent connections never get too high. The issue in this case is that it will then take longer to process the queue which brings me back to the original limit of 10 seconds of function execution.

So, I think the above code may work in theory now, but using it in practice may have other issues that you will need to consider.

philnash
  • 70,667
  • 10
  • 60
  • 88
  • philnash, thank you so much for your help. This code worked and helped me get proof of concept. The system works as expected with 3 contacts, but clearly I'm concerned about the issues you pointed out. I need this system to perform as expected for a minimum of 1500 contacts, and preferably 5000+. My thoughts are to host the actual sending portion of the code on something like aws lambda which gives up to a 15 minute time out. But my immediate question if you had a chance, what would you expect the breaking point of this code to be as is? Thanks for your assistance, has been a huge help. – Develop With Data Sep 14 '21 at 15:35
  • When you say "Also, there are limits for the number of concurrent connections you can make to the Twilio API. That limit used to be 100 connections per account, but it may vary these days." -- Is this per account or per sub-account? – Develop With Data Sep 14 '21 at 15:37
  • I'm not sure on concurrency any more as the limit is no longer published. The best thing to do is [observe the response headers](https://www.twilio.com/docs/usage/rest-api-best-practices#monitor-your-usage) and implement retries and exponential back off when you receive a 429 response. – philnash Sep 15 '21 at 01:59
  • As for sending thousands of messages, it is likely not best to do within a single Function/Lambda/whatever platform. If they are to be different messages to each user, then I would recommend trying to set up a job queue that can process the API requests one at a time and control the concurrency by the number of workers you have servicing the queue. – philnash Sep 15 '21 at 02:00