How to add Twitter Direct Messages into Twilio Flex

August 06, 2021
Written by
Reviewed by

Twilio Flex x Twitter Header

Twitter's own research found 64% of customers on the platform would rather message a support handle than call up a business, and 75% expected to receive a response within 15 minutes. In this post, we’ll take a look at how you can bring Twitter support requests into Twilio Flex, meeting the demands of your customers whilst leveraging interactive features of Twitter's Direct Message API such as 'Quick Replies' and 'Buttons'.

twilio-twitter-flex-view.png

Getting Started

Before we get into the implementation, make sure you've done the following.

Orchestrating Chats

While Flex doesn't provide a Twitter channel out of the box, it does give developers the power to build and add custom channels. To create an open line of conversation between a customer on Twitter and an agent on Flex, we need an application that listens for updates from each side and forwards the messages accordingly so that both the customer and agent are kept in sync. The application should:

  1. Listen for an event where a Direct Message is sent to our Twitter account and forward it on to Flex via a Flex Chat Channel. If the customer is already engaged in a chat with an agent, the message will be sent through an existing Chat Channel, otherwise a new one will be created. We'll refer to this as the [POST] /fromTwitter endpoint.
  2. Listen for an event where a message is sent by a Flex agent and forward it on to Twitter as a Direct Message for the customer. We'll refer to this as the [POST] /fromFlex endpoint.
  3. Respond to Twitter's Challenge-Response Checks so that registered webhooks remain valid. This is a prerequisite for completing the rest of the Twitter webhook configuration you started earlier. We'll refer to this as the [GET]/fromTwitter endpoint.

The sequence diagram below demonstrates the flow of messages back and forth.

Flow between Twitter and Flex

[GET] /fromTwitter Endpoint

Twitter references code examples for building encrypted response tokens that satisfy their Challenge-Response Checks in Python, JavaScript, and Ruby. An example Express-Node.js endpoint is shown below:

app.get('/fromTwitter', (req, res) => {
  const crcToken = req.query.crc_token;
  const hmac = createHmac('sha256', process.env.TWITTER_CONSUMER_SECRET)
    .update(crcToken)
    .digest('base64');
  const resToken = `sha256=${hmac}`;
  res.status(200).json({ response_token: resToken });
});

Once this endpoint is up and running, you can complete steps 3 and 4 of Twitter's Getting Started with Webhooks guide where you'll register the application to receive webhooks and subscribe your Twitter account to updates. Twitter's Developer Relations team also provides a web app that you can use to speed up the process.

[POST] /fromTwitter Endpoint

Now we're ready to start receiving Direct Message events from Twitter. The Chat Channel that we'll use to forward messages on to Flex requires an identity property - a unique identifier for the customer in the chat. In this case, the logical identifier is the customer's Twitter handle, which is provided as part of the message event. First, we'll extract the Twitter handle and the ID from the request, along with the message body, and pass these on to a function called sendMessageToFlex().

app.post('/fromTwitter', (req, res) => {
  if (req.body.direct_message_events) {
    const users = req.body.users;
    const customer = users[Object.keys(users)[0]];
    const twitterHandle = customer.screen_name;
    const twitterId = customer.id;
    // Check to make sure this is a message sent from a customer rather than a Direct
    // Message we sent from our application
    if (twitterHandle !== process.env.TWITTER_CO_HANDLE) {
      const msg =
        req.body.direct_message_events[0].message_create.message_data.text;
      sendMessageToFlex(twilioClient, msg, twitterHandle, twitterId);
    }
  }
  res.sendStatus(200);
});

sendMessageToFlex() is broken out into two further functions:

  • getChannel() - returns the Chat Channel associated with the customer
  • sendMessageToFlex() - sends the message to Flex via the Chat Channel

getChannel() returns a Chat Channel for the customer, either by finding an active, pre-existing Channel, or by creating a new one. A webhook is also configured at this stage so that the [POST] /fromFlex endpoint receives updates when an agent replies.

In order to create a new Flex Chat Channel, a Flex Flow must be provided which tells Twilio things like the Chat Service we want to use as well as how incoming messages should be handled. Visit this post and work through the "Create a new Flex Flow" section, taking note of your Flex Flow SID and Flex Chat Service SID to use in the example code below. The rest of the post provides valuable insight into how messages are orchestrated on Flex as well as guidance for adding custom chat channels, much of which our implementation will be based on.

const getChannel = async (
  twilioClient,
  flexFlowSid,
  flexChatServiceSid,
  twitterHandle,
  twitterId
) => {
  const channelExists = await hasOpenChannel(twilioClient, twitterHandle);
  // If we try and create a new channel that already exists, Twilio will just return the 
  // existing channel SID which we need anyway
  const flexChannel = await twilioClient.flexApi.channel.create({
    // Use newly created Flex Flow SID
    flexFlowSid,
    identity: twitterHandle,
    chatUserFriendlyName: `Twitter with @${twitterHandle}`,
    chatFriendlyName: twitterId,
    target: `@${twitterHandle}`,
  });
  // Duplicating webhooks would result in duplicate flows between Twitter and Flex
  // which we need to avoid so we apply the channelExists check here
  if (!channelExists) {
    await twilioClient.chat
      .services(flexChatServiceSid)
      .channels(flexChannel.sid)
      .webhooks.create({
        type: 'webhook',
        configuration: {
          method: 'POST',
          url: `${process.env.NGROK_URL}/fromFlex`,
          filters: ['onMessageSent'],
        },
      });
  }
  return flexChannel
};

const hasOpenChannel = async (twilioClient, twitterHandle) => {
  const channels = await twilioClient.chat
    .services(process.env.FLEX_CHAT_SERVICE)
    .channels.list();
  const openChannelExists =
    channels.filter((c) => {
      const { from, status } = JSON.parse(c.attributes);
      // Channels are automatically set to INACTIVE when they are closed by a Flex Agent
      return from.includes(twitterHandle) && status !== 'INACTIVE';
    }).length > 0;
  return openChannelExists;
};

sendChatMessage() handles the final sending of the message to Flex.

const sendChatMessage = async (flexChatServiceSid, flexChannelSid, twitterHandle, msg) => {
  // Source: https://www.twilio.com/blog/add-custom-chat-channel-twilio-flex
  const params = new URLSearchParams();
  params.append('Body', msg);
  params.append('From', twitterHandle);
  const res = await fetch(
    `https://chat.twilio.com/v2/Services/${flexChatServiceSid}/Channels/${flexChannelSid}/Messages`,
    {
      method: 'post',
      body: params,
      headers: {
        'X-Twilio-Webhook-Enabled': 'true',
        Authorization: `Basic ${base64.encode(
          `${process.env.TWILIO_ACCOUNT_SID}:${process.env.TWILIO_AUTH_TOKEN}`
        )}`,
      },
    }
  );
  return res;
};

By this point, you should be able to send a Direct Message to your Twitter account and see it appear on Flex. Give it a test!

[POST] /fromFlex Endpoint

In the [POST] /fromTwitter endpoint, we subscribed to Flex message events through a webhook that was configured for the Chat Channel. As well as the message body included in these events, we need to extract the customer's Twitter ID from the associated Chat Channel Resource. This ID is the key user identifier that Twitter requires for sending a message through their Direct Message API and its retrieval is handled in the getUserFromChannel() function below.

app.post('/fromFlex', async (req, res) => {
  // Source will be 'API' for Twitter customer side, 'SDK' for Flex agent side
  if (req.body.Source === 'SDK') {
    const channelId = req.body.ChannelSid;
    const twitterId = await getUserFromChannel(
      twilioClient,
      channelId
    );
    const msg = req.body.Body;
    await sendMessageToTwitter(twitterClient, msg, twitterId);
  }
  res.sendStatus(200);
});

const getUserFromChannel = async (twilioClient, channelId) => {
  const chat = await twilioClient.chat
    .services(process.env.FLEX_CHAT_SERVICE)
    .channels(channelId)
    .fetch();
  const twitterId = chat.friendlyName;
  return twitterId;
};

To finish the flow, a reply is sent to the customer within the sendMessageToTwitter() function by creating a new Direct Message event. Note that in this example, we are using twit as a Twitter Client for handling the API request but the event object below is the standard format expected by Twitter.

const sendMessageToTwitter = async (twitterClient, msg, twitterId) => {
  twitterClient.post(
    'direct_messages/events/new',
    {
      event: {
        type: 'message_create',
        message_create: {
          target: {
            recipient_id: twitterId,
          },
          message_data: {
            text: msg,
          },
        },
      },
    },
  );
};

Now you can test the process end-to-end, sending messages back and forth between Twitter and Flex. You can try opening and closing conversations as well as processing messages from multiple Twitter accounts to make sure things are working as expected.

Interactive Chat Features

Rich chat experiences can be developed with the aid of Twitter's Quick Replies and Buttons. These features are realised through some minor alterations to the event object that we used to send a Direct Message in the [POST] /fromFlex endpoint.

twilio-twitter-flex-quick-replies.png

In order to let Twitter show Quick Replies in a message, the message_data property should contain a quick_reply object. Below you can see an example of what the event object should look like:

  "event": {
    "type": "message_create",
    "message_create": {
      "target": {
        "recipient_id": "844385345234",
      },
      "message_data": {
        "text": "Ok, this weeks 2 boxes are:",
        "quick_reply": {
          "type": "options",
          "options": [
            {
              "label": "Missed a delivery 📦",
              "description": "Reschedule a date and time",
            },
            {
              "label": "General information 🍓",
              "description": "Available fruit boxes and what's in season"
            },
          ],
        },
      },
    },
  }

For this implementation, the agent triggers Quick Replies using an "Options" keyword followed by an option list. A parsing function, getTwitterMessageData(), splits the agent's message on the keyword and packages the remainder of the message into an array of options with labels and descriptions. The message_data object returned by the function is then sent as part of the new Direct Message event. This is just one of many possible approaches that could be taken for empowering agents with Quick Replies within the Flex UI.

const getTwitterMessageData = (msg) => {
  let formattedMsg = msg;
  let optionsObj = {};
  if (msg.includes('Options')) {
    const msgSplit = msg.split('Options');
    const optionsSplit = msgSplit[1].split(',');
    formattedMsg = msgSplit[0];
    const options = optionsSplit.map((op) => {
      const optionDescSplit = op.split('-');
      const option = {
        label: optionDescSplit[0],
      };
      if (optionDescSplit.length > 1) {
        option.description = optionDescSplit[1];
      }
      return option;
    });
    // Package the Quick Reply object
    optionsObj = {
      quick_reply: {
        type: 'options',
        options,
      },
    };
  }
  const messageData = { text: formattedMsg, ...optionsObj, };
  return messageData;
};

Next Steps for your Twitter Integration

The code for this proof of concept, along with an implementation of interactive Buttons, is available in this repo. You might want to explore some further additions for your integration such as collecting support satisfaction scores using Twitter's feedback cards or developing a Flex Plugin that displays the Twitter logo and colours so that agents readily know they're communicating with a customer on Twitter.

Mark Marshall is a Senior Solutions Engineer at Twilio. He can be reached at mmarshall [at] twilio.com.