© Szymon Rozga 2018
Szymon RozgaPractical Bot Developmenthttps://doi.org/10.1007/978-1-4842-3540-9_8

8. Extending Channel Functionality

Szymon Rozga1 
(1)
Port Washington, New York, USA
 

We have spent a substantial amount of time so far discussing NLU systems, conversational experiences, and how we can develop bots in a generic manner using a common format via the Bot Builder SDK. The Bot Builder SDK lets us get up and running quickly. This is part of why it is such a powerful abstraction. But frankly, a lot of the innovation in the space is coming from the various messaging platforms. For example, Slack is leading the pack in terms of collaboration software. Slack’s ability to edit messages, allowing for interactive workflows, is very powerful.

In this chapter, we will explore the ability to invoke native functionality from within a Bot Framework bot. We will learn to invoke Slack’s feature to transform simple text-based workflows into rich button and menu-based experiences. Along the way, we will sign up for a Slack integration, connect our bot to our Slack workspace, and then use native Slack calls to create a compelling and straightforward workflow. Let's dive in.

Deeper Slack Integration

Slack is a rich platform that allows close collaboration among different members of internal and external teams. The interface is simple, yet the messaging framework is quite different from something like Facebook Messenger. For example, although there is a facility called attachments that results in a user interface similar to cards, it is not treated in the same way. There are no carousels, and there are no requirements around aspect ratios for images.

A message in Slack is simply a JSON object with a text property, where the text can have special sequences that reference users, channels, or teams. These references, called @mentions, are text strings like @channel, which notifies all users in a channel to pay attention to a message. Other examples are @here and @everyone. A message can include up to 20 attachments. An attachment is simply an object that provides additional context to the message. The JSON object looks as follows:

{
    "attachments": [
        {
            "fallback": "Required plain-text summary of the attachment.",
            "color": "#36a64f",
            "pretext": "Optional text that appears above the attachment block",
            "author_name": "Bobby Tables",
            "author_link": "http://flickr.com/bobby/",
            "author_icon": "http://flickr.com/icons/bobby.jpg",
            "title": "Slack API Documentation",
            "title_link": "https://api.slack.com/",
            "text": "Optional text that appears within the attachment",
            "fields": [
                {
                    "title": "Priority",
                    "value": "High",
                    "short": false
                }
            ],
            "image_url": "http://my-website.com/path/to/image.jpg",
            "thumb_url": "http://example.com/path/to/thumb.png",
            "footer": "Slack API",
            "footer_icon": "https://platform.slack-edge.com/img/default_application_icon.png",
            "ts": 123456789
        }
    ]
}

Like a HeroCard, we can include title, text, and images. In addition, there are various other parameters we can provide to Slack. We can include references to a message author, data fields or theme colors. 

To aid in the nuances of attachments, Slack includes a Message Builder (Figure 8-1), which can be used to visualize how a JSON object will render in the Slack user interface.
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig1_HTML.jpg
Figure 8-1

Slack Message Builder and preview

Slack also provides best practices documentation for messages.1 Among the advice on the site is to use as few attachments as makes sense for our application (Figure 8-2).
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig2_HTML.jpg
Figure 8-2

Good direction…

Unfortunately, this does not seem to be the way that the Bot Framework works. In fact, the Slack Bot channel connector renders a HeroCard object as multiple attachments (Figure 8-3).
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig3_HTML.jpg
Figure 8-3

Except that the Slack guidelines are not fully respected by the Slack Bot channel connector

It’s a small detail, but it just does not look good. The default styling for an image and buttons is to render the buttons below the image (Figure 8-4). Unfortunately, the rendering is violating the direction provided by Slack.
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig4_HTML.jpg
Figure 8-4

What a well-formed attached could look like

Naturally, this is the kind of detail the Bot Framework team will most likely support in the future. Until then, if there is a mismatch in terms of the type of interface we want to render and what the platform supports, we can drop into the native JSON to achieve our goals.

Slack also includes a few features that we have no way of accessing as first-class citizens in the bot service. Slack supports ephemeral messages, which are messages that are visible to only one user in a group setting. The Bot Builder SDK does not provide an easy way to achieve this. Furthermore, Slack supports the idea of interactive messages, which are messages with buttons and menus that users can act on. Even better, a user’s action can trigger an update to the message rendering! A message can include buttons as a way to gather data from the user (as shown in Figures 8-3 and 8-4), or a message can include menus to select an option (Figure 8-5).
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig5_HTML.jpg
Figure 8-5

A Slack menu

In this section, we will explore how to achieve the interactive message effect by integrating closely via native messages.

First, we will integrate our bot with a Slack workspace. Second, we will create a one-step interactive message. Third, we’ll create a multistep interactive message that provides a rich, Slack-native data-gathering experience.

Before we continue, let’s go over a few ground rules. This chapter is not intended to give you a deep dive into Slack’s Messaging APIs and features. We encourage you to read about these on your own; Slack has very rich documentation on the subject. What we do want to show is how we can leverage the bot service to provide that deeper integration with Slack. You may ask, why not just develop a native Slackbot using Slack’s Node Developer Kit? You certainly can, but there are two big reasons for using the Bot Builder library. One, you get the dialog and conversation engine to help guide a user through a conversation, and two, if you are exposing an experience on multiple messaging channels, one codebase enables code reuse.

Connecting to Slack

Let’s assume you have never used Slack. We will first need to create a Slack workspace. A workspace is simply a Slack environment for a team to collaborate in. We can create these for free. There are some limitations, but free teams remain very functional and will certainly allow us to develop and demo Slack bots. Go to https://slack.com/create to create a workspace. Slack will ask for an email (Figure 8-6) and send a confirmation code to verify our identity.
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig6_HTML.jpg
Figure 8-6

Creating a new Slack workspace

Once we enter the confirmation code, it will ask us for our name, password, (group) workspace name, target audience, and workspace URL. We can send invitations to the workspace, but we will skip this for now. We will not be redirected to the workspace. For the purposes of this demo, mine is https://srozgaslacksample.slack.com .

At this point, we should integrate the bot service and Slack. In our Bot Service entry on Azure, click the Slack channel. We’ll be greeted with the Slack Configuration screen (Figure 8-7).
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig7_HTML.jpg
Figure 8-7

Configuring our bot’s Slack integration

The interface is like the Facebook Messenger channel configuration interface but asks for differrent data. We will need three pieces of information from Slack: the client ID, the client secret, and the verification token.

Log into Slack and create a new app at https://api.slack.com/apps . Enter an app name and select the development workspace that we just created (Figure 8-8). Lastly click the Create App button.
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig8_HTML.jpg
Figure 8-8

Creating a Slack app

Once the app is created, we will be redirected to the app page. Click Permissions to set up a redirect URL (Figure 8-9). You will be taken to a page called OAuth & Permissions.
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig9_HTML.jpg
Figure 8-9

Setting up the bot service redirect URI

Click Add a new Redirect URL and enter https://slack.botframework.com . Next select the Bot Users item in the left sidebar, and add a user for the bot. This allows us to assign a username to the bot and indicate whether it should always appear online (Figure 8-10).
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig10_HTML.jpg
Figure 8-10

Creating a bot user that represents a bot in a channel

Next, we will subscribe to several events that will be sent to the bot service web hooks. This will ensure that the bot service can properly send the relevant Slack events into our bot. Navigate to Event Subscriptions, enable events via the toggle at the right, and enter https://slack.botframework.com/api/Events/{YourBotHandle} as the request URL. A bot handle was assigned to our bot channel registration in Chapter 5 and can be found in the Settings blade. Once entered, Slack will establish connectivity to the endpoint. Lastly, under Subscribe to Bot Events (not Workspace Events!) add the following events:
  • member_joined_channel

  • member_left_channel

  • message.channels

  • message.groups

  • message.im

  • message.mpim

Figure 8-11 shows the resulting configuration.
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig11_HTML.jpg
Figure 8-11

Subscribing our bot to Slack events

We also need to enable interactive components to support receiving a message with a menu, button, or interactive dialog. Select Interactive Components from the left menu, click Enable Interactive Messages, and enter the following request URL: https://slack.botframework.com/api/Actions (Figure 8-12). Click Enable Interactive Components and save the changes.
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig12_HTML.jpg
Figure 8-12

Enabling interactive components in our bot. That means buttons and menus!

Lastly, we extract the credentials from the App Credentials section (accessible via the Basic Information menu item) and enter the client ID, client secret, and verification token into the Configure Slack screen within the Channels blade of your bot channels registration in the Azure Portal. Once submitted, you will be asked to log into your Slack workspace and verify the app. After authorization, your bot will appear in your Slack workspace interface (under the Apps category), and you will be able to communicate with it (Figure 8-13).
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig13_HTML.jpg
Figure 8-13

We’ve connected to the Azure bot service

Remember to run ngrok! You can tell I forgot to run my ngrok in Figure 8-13.

Exercise 8-1

Basic Slack Integration and Message Rendering

The goal of this exercise is to connect a bot into Slack so you can get familiar with Slack as a messaging and bot platform. Your goal is to take the calendar bot you created in the Chapters 5 and 7 and deploy it to Slack. Once deployed, you may examine how the different elements are rendered in Slack versus the emulator or Facebook Messenger.
  1. 1.

    Create a test Slack workspace.

     
  2. 2.

    Connect your Azure Bot service bot to the workspace by following the steps in the previous section.

     
  3. 3.

    Confirm you can communicate with your bot via Slack.

     
  4. 4.

    Test the bot and answer the following questions: How does the bot render the sign-in button? How does the bot render the primary card selection cards? How does the bot behave in a multi-user conversation (you may need to add a new test user to the workspace)?

     

Great work. You are now able to connect an existing bot to Slack, and you are learning about Slack, its message, and attachments.

Experimenting with the Slack APIs

We just send a message to Slack using the Bot Builder SDK and the Bot Framework, but we can also access the Slack APIs directly. We are interested in several Slack API methods.2
  • Chat.postMessage: Posts a new message into a Slack channel

  • Chat.update: Updates an existing message in Slack

  • Chat.postEphemeral: Posts a new ephemeral message, one visible to only one user, into a Slack channel

  • Chat.delete: Deletes a Slack message

To invoke any of these, we need an access token. For example, assuming we have a token, we could use the following Node.js code to create a new message:

function postMessage(token, channel, text, attachments) {
    return new Promise((resolve, reject) => {
        let client = restify.createJsonClient({
            url: 'https://slack.com/api/chat.postMessage',
            headers: {
                Authorization: 'Bearer ' + token
            }
        });
        client.post('',
            {
                channel: channel,
                text: text,
                attachments: attachments
            },
            function (err, req, res, obj) {
                if (err) {
                    console.log('%j', err);
                    reject(err);
                    return;
                }
                console.log('%d -> %j', res.statusCode, res.headers);
                console.log('%j', obj);
                resolve(obj);
            });
    });
}

A natural question is how do we obtain the token? If we examine the message coming in from the bot service channel connector, we notice that we have all that information at our disposal. The full incoming message from Slack looks like this:

{
    "type": "message",
    "timestamp": "2017-11-23T17:27:13.5973326Z",
    "text": "hi",
    "attachments": [],
    "entities": [],
    "sourceEvent": {
        "SlackMessage": {
            "token": "fffffffffffffffffffffff",
            "team_id": "T84FFFFF",
            "api_app_id": "A84SFFFFF",
            "event": {
                "type": "message",
                "user": "U85MFFFFF",
                "text": "hi",
                "ts": "1511458033.000193",
                "channel": "D85TN0231",
                "event_ts": "1511458033.000193"
            },
            "type": "event_callback",
            "event_id": "Ev84PDKPCK",
            "event_time": 1511458033,
            "authed_users": [
                "U84A79YTB"
            ]
        },
        "ApiToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    },
    "address": {
        "id": "ffffffffffffffffffffffffffffffffff",
        "channelId": "slack",
        "user": {
            "id": "U85M9EQJ2:T84V64ML5",
            "name": "szymon.rozga"
        },
        "conversation": {
            "isGroup": false,
            "id": "B84SQJLLU:T84V64ML5:D85TN0231"
        },
        "bot": {
            "id": "B84SQJLLU:T84V64ML5",
            "name": "szymontestbot"
        },
        "serviceUrl": "https://slack.botframework.com"
    },
    "source": "slack",
    "agent": "botbuilder",
    "user": {
        "id": "U85M9EQJ2:T84V64ML5",
        "name": "szymon.rozga"
    }
}

Note that the sourceEvent includes an ApiToken and a SlackMessage with all the details about which channel the bot is in and the user from which the original message originated. In this example, the channel is D85TN0231, and the user is U85M9EQJ2. Further, we can find the IDs for the team, the bot, the bot user, and the app. An incoming message doesn’t really have an ID in Slack; each message has a unique-per-channel timestamp referred to as ts.

So, once we have the first message from a user, we can easily respond either by using the Bot Builder’s session.send method or by using the chat.postMessage endpoint directly (Figure 8-14). Of course, session.send is doing all the token work for us underneath the covers by calling to the Slack channel connector, which then calls chat.postMessage.

const bot = new builder.UniversalBot(connector, [
    session => {
        let token = session.message.sourceEvent.ApiToken;
        let channel = session.message.sourceEvent.SlackMessage.event.channel;
        postMessage(token, channel, 'POST!');
    }
]);
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig14_HTML.jpg
Figure 8-14

Responding using a native Slack call

postMessage does not really get us anything better than session.send, except that chat.postMessage returns the message’s native ts value, whereas session.send does not. Very cool. That means we can now update the message! We define an updateMessage method as follows:

function updateMessage(token, channel, ts, text, attachments) {
    return new Promise((resolve, reject) => {
        let client = restify.createJsonClient({
            url: 'https://slack.com/api/chat.update',
            headers: {
                Authorization: 'Bearer ' + token
            }
        });
        client.post('',
            {
                channel: channel,
                ts: ts,
                text: text,
                attachments: attachments
            },
            function (err, req, res, obj) {
                if (err) {
                    console.log('%j', err);
                    reject(err);
                    return;
                }
                console.log('%d -> %j', res.statusCode, res.headers);
                console.log('%j', obj);
                resolve(obj);
            });
    });
};

Now we can write code to send a message and update it whenever any other response comes in (see Figure 8-15, Figure 8-16, and Figure 8-17).

let msgts = null;
const bot = new builder.UniversalBot(connector, [
    session => {
        let token = session.message.sourceEvent.ApiToken;
        let channel = session.message.sourceEvent.SlackMessage.event.channel;
        let user = session.message.sourceEvent.SlackMessage.event.user;
        if (msgts) {
            updateMessage(token, channel, msgts, '<@' + user + '> said ' + session.message.text);
        } else {
            postMessage(token, channel, 'A placeholder...').then(r => {
                msgts = r.ts;
            });
        }
    }
]);
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig15_HTML.jpg
Figure 8-15

So far so good…

../images/455925_1_En_8_Chapter/455925_1_En_8_Fig16_HTML.jpg
Figure 8-16

Seems to be working

../images/455925_1_En_8_Chapter/455925_1_En_8_Fig17_HTML.jpg
Figure 8-17

Exactly as designed

Now this is a contrived example, but it illustrates our ability to call a postMessage followed by an update to modify the contents of a message. There are some rules around what exactly update can do, but we leave reading that documentation3 as an exercise to the developer.

Another example of what we can accomplish with the APIs is posting and removing ephemeral messages. An ephemeral message is visible only to the recipient of the message. The bot can, for example, give feedback to a user without displaying the result in the channel until all the necessary data has been gathered. Although a slightly different interaction model, the giphy4 Slash command is a great example of this model.

Using / giphy allows us to search for any text and brings up a few GIF options in an ephemeral message. You may have to enable the integration first, before utilizing it. Once we decide which one we want to use and click Send, the GIF is sent to the channel on our behalf (Figure 8-18, Figure 8-19, and Figure 8-20).
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig18_HTML.jpg
Figure 8-18

Invoking the /giphy Slash command

../images/455925_1_En_8_Chapter/455925_1_En_8_Fig19_HTML.jpg
Figure 8-19

A preview of a cool mom mean girls GIF

../images/455925_1_En_8_Chapter/455925_1_En_8_Fig20_HTML.jpg
Figure 8-20

I’ve now immortalized in Slack conversation the 2004 cult classic Mean Girls by using /giphy mean girls

We could use the postEphemeral message to give feedback to only certain users. And, of course, delete gives us the ability to delete old messages from the bot. From a usability perspective, the delete feature is not interesting. It is a better experience to update a message with a correction or to notify the user that a message has been deleted, rather than to simply get rid of it without any explanation.

Simple Interactive Message

Slack allows us to instrument better conversational experiences using what are known as interactive messages.5 An interactive message is a message that includes the usual message data plus buttons and menus. In addition, as users interact with the user interface elements, the message can change to reflect that.

Here is an example: the bot would send a message asking for approval, and when the user clicks the yes or no button, our bot modifies the message to reflect the selection (Figure 8-21, Figure 8-22, and Figure 8-23).
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig21_HTML.jpg
Figure 8-21

A simple interactive message

../images/455925_1_En_8_Chapter/455925_1_En_8_Fig22_HTML.jpg
Figure 8-22

Request approved

../images/455925_1_En_8_Chapter/455925_1_En_8_Fig23_HTML.jpg
Figure 8-23

Request was not approved

Certainly, we can orchestrate this type of behavior using postMessage and updateMessage, but there’s an easier and more integrated way to do it. First, we define a dialog called simpleflow that uses a Choice Prompt to send a message with buttons.

const bot = new builder.UniversalBot(connector, [
    session => {
        session.beginDialog('simpleflow');
    },
    session => {
        session.send('done!!!');
        session.endConversation();
    }
]);
bot.dialog('simpleflow',
[
    (session, arg) =>{
        builder.Prompts.choice(session, 'A request for access to /SYS13/ABD has come in. Do you want to approve?', 'Yes|No');
    },
    ... // next code snippet goes here
]);

Then we handle the response to the button click by making a POST request to a response_url.

(session, arg) =>{
    let r = arg.response.entity;
    let responseUrl = session.message.sourceEvent.Payload.response_url;
    let token = session.message.sourceEvent.Payload.token;
    let client = restify.createJsonClient({
        url: responseUrl
    });
    let userId = session.message.sourceEvent.Payload.user.id;
    let attachment ={
        color: 'danger',
        text: 'Rejected by <@' + userId + '>'
    };
    if (r === 'No'){} else if (r === 'Yes'){
        attachment ={
            color: 'good',
            text: 'Approved by <@' + userId + '>'
        };
    }
    client.post('',
    {
        token: token,
        text: 'Request for access to /SYS13/ABD',
        attachments: [attachment
        ]
    }, function (err, req, res, obj){
        if (err) console.log('Error -> %j', err);
        console.log('%d -> %j', res.statusCode, res.headers);
        console.log('%j', obj);
        session.endDialog();
    });
}

A few things are happening here. First, we grab the response from Slack, which is resolved to the entity value. Second, we grab what’s known as the response_url from the Slack message. A response_url is a URL that allows us to modify the interactive message that a user just responded to or to create a new message in the channel. Next, we grab the token that authorizes us to send POST requests to the response_url. Lastly, we POST to the response_url with the updated message.

We will get into more details around interactive message structure, but let’s discuss user experience. When developing a bot that utilizes this functionality, we have to make a decision: when the bot presents an interactive message, does the user have to answer it immediately, or can the interactive message remain in the history while the user and bot discuss other topics? In the latter case, at any time later in the conversation, the user could scroll back up and click a button to complete the action. The previous sample utilizes the former approach; that is the way Bot Builder prompts work. Figure 8-24 shows what this looks like if the user doesn’t respond to the message.
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig24_HTML.jpg
Figure 8-24

Hmm…seems I have two sets of buttons to answer the same question

OK, we have the two set of buttons. That makes sense. If we click either the Yes or No button, that message will be modified per Figure 8-25. The dialog finished, and the second step of the bot waterfall sends the “done!!!” message. However, the conversation is left in a weird state; it appears as if the original request is still open.
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig25_HTML.jpg
Figure 8-25

Shouldn’t the first message update as well?

Now, the dialog stack no longer contains the choice prompt on top of the stack. This means that if we click the Yes or No button in the upper message, we will run into a problem because our code is not expecting that type of response (Figure 8-26). In fact, we will receive yet another prompt because the bot once again calls beginDialog . Having multiple unresolved interaction messages without the ability to resolve all of them is bad UX.
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig26_HTML.jpg
Figure 8-26

Oh, that makes no sense…

The experience can get complicated quickly. That’s the problem with rendering buttons on any platform: the buttons stay in the chat history and can be clicked any time. Our role as developers is to make sure the bot can handle the buttons and their payloads at any time.

Here is one approach to solve the previous problem. We leave the default behavior as is, but we create a custom recognizer that handles interactive message inputs and redirects the message to a dialog that tells the user that the action has expired, if these inputs are not expected. Let’s start with the dialog. It will read the response_url for the interactive message and simply post a “Sorry, this action has expired.” message to it. The dialog is invoked when the bot resolves the intent practicalbot.expire . A naming convention like that allows us to draw a distinction between LUIS intents and intents internal to the bot.

bot.dialog('remove_action',
[
    (session, arg) =>{
        let responseUrl = session.message.sourceEvent.Payload.response_url;
        let token = session.message.sourceEvent.Payload.token;
        let client = restify.createJsonClient({
            url: responseUrl
        });
        client.post('',
        {
            token: token,
            text: 'Sorry, this action has expired.'
        }, function (err, req, res, obj){
            if (err) console.log('Error -> %j', err);
            console.log('%d -> %j', res.statusCode, res.headers);
            console.log('%j', obj);
            session.endDialog();
        });
    }
]).triggerAction({ matches: 'practicalbot.expire'
});

The custom recognizer would look like this:

bot.recognizer({
    recognize: function (context, done){
        let intent = { score: 0.0 };
        if (context.message.sourceEvent &&
            context.message.sourceEvent.Payload &&
            context.message.sourceEvent.Payload.response_url)
        {
            intent = { score: 1.0, intent: 'practicalbot.expire' };
        }
        done(null, intent);
    }
});
In short, we are saying that if our dialog cannot explicitly handle an action response from the user, the global practicalbot.expire intent will be hit. In that case, we simply tell the user that the action has expired. The net effect can be seen in Figure 8-27 and Figure 8-28. We first get into the scenario where we have two interaction messages asking us for Yes or No input. We approve the second one. In Figure 8-28, we click Yes on the first button set.
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig27_HTML.jpg
Figure 8-27

OK, back to this scenario

../images/455925_1_En_8_Chapter/455925_1_En_8_Fig28_HTML.jpg
Figure 8-28

It works. We can now act of older interactive messages without creating UX chaos.

There are a couple of caveats we should mention. First, if you tried responding to the prompt using text instead of clicking a button, the code provided would fail. Why is this? Slack does not send a Payload object with details about the message interaction. It would just be considered text input, and we would not have a way to properly update the message to be approved or rejected. One way of dealing with this is to simply require button inputs instead of text input. Another way is to accept it but send the confirmation as a new message. Here is the code with that behavior with the resulting conversation after responding with a text message in Figure 8-29:

(session, arg) => {
        let r = arg.response.entity;
        let userId = null;
        const isTextMessage = session.message.sourceEvent.SlackMessage; // this means we receive a slack message
        if (isTextMessage) {
            userId = session.message.sourceEvent.SlackMessage.event.user;
        } else {
            userId = session.message.sourceEvent.Payload.user.id;
        }
        Let attachment = {
            color: 'danger',
            text: 'Rejected by <@' + userId + '>'
        };
        if (r === 'No') {
        } else if (r === 'Yes') {
            attachment = {
                color: 'good',
                text: 'Approved by <@' + userId + '>'
            };
        }
        if (isTextMessage) {
            // if we got a text message, reply using
            // session.send with the confirmation message
            let msg = new builder.Message(session).sourceEvent({
                'slack': {
                    text: 'Request for access to /SYS13/ABD',
                    attachments: [attachment]
                }
            });
            session.send(msg);
        } else {
            let responseUrl = session.message.sourceEvent.Payload.response_url;
            let token = session.message.sourceEvent.Payload.token;
            let client = restify.createJsonClient({
                url: responseUrl
            });
            client.post('', {
                token: token,
                text: 'Request for access to /SYS13/ABD',
                attachments: [attachment]
            }, function (err, req, res, obj) {
                if (err) console.log('Error -> %j', err);
                console.log('%d -> %j', res.statusCode, res.headers);
                console.log('%j', obj);
                session.endDialog();
            });
        }
    }
}
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig29_HTML.jpg
Figure 8-29

We can now handle text responses as well

The second caveat is that in the previous example we use the choice prompt that blocks the conversation until a yes or no response is sent by the user. We want to avoid this behavior so that the user can continue working with the bot without necessarily having to answer the prompt immediately. A better approach would be to install a global recognizer that is able to map interactive message responses to intents that, in turn, map to dialogs that fulfill certain actions. We will be looking at this in Exercise 8-2.

Exercise 8-2

Exploring Nonblocking Interactive Messages in Slack

In the previous section we explored how we can utilize the choice prompt to ask the user for input using an interactive message. In this exercise, you will create a custom recognizer to map interactive message responses to dialogs. The dialogs will contain logic to update the interactive messages by using the response_url provided by Slack.
  1. 1.

    Create a universal bot that begins a dialog called sendExpenseApproval.

     
  2. 2.

    Create a dialog called sendExpenseApproval. The dialog should create a random expense object with four fields: ID, user, type, amount. This object would represent the fact that user spent $amount on an item of type type. ID should just be a random unique identifier. For example, create an object representing the fact that Szymon spent $60 on a taxi ride or that Bob spent $20 on a case of flavored sparkling water. After generating the random expense, send a hero card to the user summarizing the expense and two buttons with the labels Approve and Reject. After sending the response using session.send, end the dialog.

     
  3. 3.

    At this point, the bot doesn’t do anything. Modify the Approve and Reject buttons in the hero card so that the value sent to the bot is Approved request with id {ID} and Reject request with id {ID}.

     
  4. 4.

    Create a custom recognizer to match these patterns and extract the ID. Your custom recognizer should return the intent ApproveRequestIntent or RejectRequestIntent based on the input. Make sure to include the ID in the resulting recognizer object.

     
  5. 5.

    Create two dialogs, one called ApproveRequestDialog and one called RejectRequestDialog. Use triggerAction to connect the dialogs to the corresponding intents.

     
  6. 6.

    Ensure the two dialogs send the correct approved or rejected response to the response_url so that the original hero card is updated.

     

The technique used in this exercise to handle all the interactive messages globally is powerful and extensible. You can easily add more message types, intents, and dialogs for any future behavior. In practice, you may end up with a mix of blocking and nonblocking messages. You are now equipped to handle both styles.

Multistep Experience

In the previous section, we created a single-step interactive message. We will continue our exploration of interactive messages on Slack with a more complex, multistep interaction. Let’s say we want to guide the user through a multistep process of selecting a type of pizza, some ingredients, and a size. We will build the experience using a multistep interactive message. The code for this section is included in the book’s git repos; we will share the most relevant bits in the following pages.

Our experience will look as follows. The bot will first ask the user for a sauce type for their pizza (Figure 8-30).
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig30_HTML.jpg
Figure 8-30

What pizza sauce would you like?

If the user responds tomato sauce, our limited bot will ask the user to select one of two types of pies: regular or pepperoni (Figure 8-31).
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig31_HTML.jpg
Figure 8-31

Pizza type options with tomato sauce

If the user had selected the Oil & Garlic sauce, they would get a different set of options (Figure 8-32).
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig32_HTML.jpg
Figure 8-32

Extra ingredient options for an Oil & Garlic base pizza

The last step requires the user to select a size. We render a menu for this step (Figure 8-33).
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig33_HTML.jpg
Figure 8-33

Which size would you like?

Once done, the message will turn into a summary of the order (Figure 8-34).
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig34_HTML.jpg
Figure 8-34

User order summary

As an exercise, we will utilize the native Slack APIs. The Bot Builder SDK needs a dialog step to explicitly use prompts to proceed from one step to the next. Since we will be using the Slack API directly, we will have a one-step waterfall dialog. This means the same function will be called over and over until a different global action is recognized or our dialog calls endDialog .

You’ll recall that in the previous example, we took advantage of Bot Builder’s prompts to send buttons back and collect the results back to logic in our bot. One of the things that the Bot Framework abstracts for us is that sending a prompt to a user actually sends a Slack message with an attachment that includes a set of actions where each button is a different action. When the user taps or clicks a button, a callback is made into our bot with a callback ID to identify the action.

For example, if we send this message to Slack, it will render a message that looks like Figure 8-31 .

pizzatype: {
    text: 'Sauce',
    attachments: [
        {
            callback_id: 'pizzatype',
            title: 'Choose a Pizza Sauce',
            actions: [
                {
                    name: 'regular',
                    value: 'regular',
                    text: 'Tomato Sauce',
                    type: 'button'
                },
                {
                    name: 'step2b',
                    value: 'oilandgarlic',
                    text: 'Oil & Garlic',
                    type: 'button'
                }
            ]
        }
    ]
}

When either button is clicked, our bot will receive a message with a callback ID of pizzatype and the selected value. Here is the relevant JSON fragment of the message we receive when we click Tomato Sauce:

"sourceEvent": {
    "Payload": {
        "type": "interactive_message",
        "actions": [
            {
                "name": "regular",
                "type": "button",
                "value": "regular"
            }
        ],
        "callback_id": "pizzatype",
        ...
    },
    "ApiToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

So, the logic to figure out whether we are getting a callback of a type is easy. In fact, the code is similar to our recognizer code shown earlier. We create an isCallbackResponse function that can tell us whether a message is a callback and, optionally, whether it is a callback of a certain type.

const isCallbackResponse = function (context, callbackId){
    const msg = context.message;
    let result = msg.sourceEvent &&
        msg.sourceEvent.Payload &&
        msg.sourceEvent.Payload.response_url;
    if (callbackId){
        result = result && msg.sourceEvent.Payload.callback_id === callbackId;
    }
    return result;
};

We can then configure our recognizer to use this function instead.

bot.recognizer({
    recognize: function (context, done) {
        let intent = { score: 0.0 };
        if (isCallbackResponse(context)) {
            intent = { score: 1.0, intent: 'practicalbot.expire' };
        }
        done(null, intent);
    }
});
Now we can build a dialog that is able to walk users through a process. We first declare the messages that we will send for each step. We will send one of five messages:
  • The first message to select a pizza type

  • Based on the pizza type selected, one of two ingredient selections

  • A selection for the pizza size

  • A final confirmation message

Here is the JSON we use:

exports.multiStepData = {
    pizzatype: {
        text: 'Sauce',
        attachments: [
            {
                callback_id: 'pizzatype',
                title: 'Choose a Pizza Sauce',
                actions: [
                    {
                        name: 'regular',
                        value: 'regular',
                        text: 'Tomato Sauce',
                        type: 'button'
                    },
                    {
                        name: 'step2b',
                        value: 'oilandgarlic',
                        text: 'Oil & Garlic',
                        type: 'button'
                    }
                ]
            }
        ]
    },
    regular: {
        text: 'Pizza Type',
        attachments: [
            {
                callback_id: 'ingredient',
                title: 'Do you want a regular or pepperoni pie?',
                actions: [
                    {
                        name: 'regular',
                        value: 'regular',
                        text: 'Regular',
                        type: 'button'
                    },
                    {
                        name: 'pepperoni',
                        value: 'pepperoni',
                        text: 'Pepperoni',
                        type: 'button'
                    }
                ]
            }
        ]
    },
    oilandgarlic: {
        text: 'Extra Ingredients',
        attachments: [
            {
                callback_id: 'ingredient',
                title: 'Do you want ricotta or caramelized onions?',
                actions: [
                    {
                        name: 'ricotta',
                        value: 'ricotta',
                        text: 'Ricotta',
                        type: 'button'
                    },
                    {
                        name: 'carmelizedonions',
                        value: 'carmelizedonions',
                        text: 'Caramelized Onions',
                        type: 'button'
                    }
                ]
            }
        ]
    },
    collectsize: {
        text: 'Size',
        attachments: [
            {
                text: 'Which size would you like?',
                callback_id: 'finish',
                actions: [
                    {
                        name: 'size_list',
                        text: 'Pick a pizza size...',
                        type: 'select',
                        options: [
                            {
                                text: 'Small',
                                value: 'small'
                            },
                            {
                                text: 'Medium',
                                value: 'medium'
                            },
                            {
                                text: 'Large',
                                value: 'large'
                            }
                        ]
                    }
                ]
            }
        ]
    },
    finish: {
        attachments: [{
            color: 'good',
            text: 'Well done'
        }]
    }
};

We then create a waterflow dialog with one step. If the message we receive from the user is not a callback, we send the first step using postMessage .

let apiToken = session.message.sourceEvent.ApiToken;
let channel = session.message.sourceEvent.SlackMessage.event.channel;
let user = session.message.sourceEvent.SlackMessage.event.user;
let typemsg = multiFlowSteps.pizzatype;
session.privateConversationData.workflowData ={};
postMessage(apiToken, channel, typemsg.text, typemsg.attachments).then(function (){
    console.log('created message');
});

Otherwise, if the message is a callback, we determine the callback type, get the data passed in the message (which is slightly different depending on whether it is coming from a button press or a menu), save the response data appropriately, and respond with the next relevant message. We track that state using privateConversationData . One caveat is that we need to explicitly save the state.

session.save();

Typically, the state would be saved as part of the session.send call. Since we don’t use this mechanism anymore because we are using the Slack API directly, we’ll call it explicitly at the end of our method. We detect if the user says “quit” to exit the flow. Here’s what the entire method looks like:

(session, arg, next) => {
    if (session.message.text === 'quit') {
        session.endDialog();
        return;
    }
    if (isCallbackResponse(session)) {
        let responseUrl = session.message.sourceEvent.Payload.response_url;
        let token = session.message.sourceEvent.Payload.token;
        console.log(JSON.stringify(session.message));
        let client = restify.createJsonClient({
            url: responseUrl
        });
        let text = '';
        let attachments = [];
        let val = null;
        const payload = session.message.sourceEvent.Payload;
        const callbackChannel = payload.channel.id;
        if (payload.actions && payload.actions.length > 0) {
            val = payload.actions[0].value;
            if (!val) {
                val = payload.actions[0].selected_options[0].value;
            }
        }
        if (isCallbackResponse(session, 'pizzatype')) {
            session.privateConversationData.workflowData.pizzatype = val;
            let ingredientStep = multiFlowSteps[val
            ];
            text = ingredientStep.text;
            attachments = ingredientStep.attachments;
        }
        else if (isCallbackResponse(session, 'ingredient')) {
            session.privateConversationData.workflowData.ingredient = val;
            var ingredientstep = multiFlowSteps.collectsize;
            text = ingredientstep.text;
            attachments = ingredientstep.attachments;
        }
        else if (isCallbackResponse(session, 'finish')) {
            session.privateConversationData.workflowData.size = val;
            text = 'Flow completed with data: ' + JSON.stringify(session.privateConversationData.workflowData);
            attachments = multiFlowSteps.finish.attachments;
        }
        client.post('',
            {
                token: token,
                text: text,
                attachments: attachments
            }, function (err, req, res, obj) {
                if (err) console.log('Error -> %j', err);
                console.log('%d -> %j', res.statusCode, res.headers);
                console.log('%j', obj);
                if (isCallbackResponse(session, 'finish')) {
                    session.send('The flow is completed!');
                    session.endDialog();
                    return;
                }
            });
    } else {
        let apiToken = session.message.sourceEvent.ApiToken;
        let channel = session.message.sourceEvent.SlackMessage.event.channel;
        let user = session.message.sourceEvent.SlackMessage.event.user;
        // we are beginning the flow... so we send an ephemeral message
        let typemsg = multiFlowSteps.pizzatype;
        session.privateConversationData.workflowData = {};
        postMessage(apiToken, channel, typemsg.text, typemsg.attachments).then(function () {
            console.log('created message');
        });
    }
    session.save();
}
After writing all that code, let us see what happens (Figure 8-35 and 8-36).
../images/455925_1_En_8_Chapter/455925_1_En_8_Fig35_HTML.jpg
Figure 8-35

So far so good

../images/455925_1_En_8_Chapter/455925_1_En_8_Fig36_HTML.jpg
Figure 8-36

Yikes!

So, what happened? As it turns out, the recognizer we previously created to reject interactive message responses when they were not expected kicked in and told us the action is expired. It seems that the prompt code pre-empted the global recognizer, whereas if we use a waterfall dialog, there is no way for us to control the recognition process.

In Chapter 6, when we discussed custom dialogs, we briefly touched on a method called recognize. This method allows us to indicate to the Bot Builder SDK that we want our current dialog to be first in line in interpreting a user message. In this case, we have specific callbacks coming in from Slack. This is a great use case for the recognize feature. But how do we access it? Turns out, we can create a custom subclass of WaterfallDialog and define a custom recognize implementation.

class WaterfallWithRecognizeDialog extends builder.WaterfallDialog {
    constructor(callbackId, steps) {
        super(steps);
        this.callbackId = callbackId;
    }
    recognize(context, done) {
        var cb = this.callbackId;
        if (_.isFunction(this.callbackId)) {
            cb = this.callbackId();
            // callback can be a function that returns an ID
        }
        if (!_.isArray(cb)) cb = [cb]; // or a list of IDs
        let intent = { score: 0.0 };
        // lastly we evaluate each ID to see if it matches the message.
        // if yes, handle within this dialog
        for (var i = 0; i < cb.length; i++) {
            if (isCallbackResponse(context, cb[i])) {
                intent = { score: 1.0 };
                break;
            }
        }
        done(null, intent);
    }
}

In short, recognize is called any time a message comes in. We resolve the supported callbacks in the dialog from the this.callbackId object. We support a single callback value, an array of callback values, or a function that returns callback values. If the callback is of any of the supported callback IDs, we return a score of 1.0, which means that our dialog will handle the message. Otherwise, we pass a score of 0.0. This means these callbacks will go up to the global recognizers, as discussed in Chapter 6. Any other callback ID will be considered expired.

We can easily use this class as follows:

bot.dialog('multi-step-flow', new WaterfallWithRecognizeDialog(['pizzatype', 'ingredient', 'finish'], [
    ...
]));

If we run the code now, we get the same resulting flow as in Figures 8-30 through 8-33.

Exercise 8-3

Interactive Messages

In this exercise, you will create a multistep interactive flow to support a bot that could filter clothing products. The goal will be to utilize a similar approach to the previous section to guide the user through a multistep data input process.
  1. 1.

    Create a universal bot with two steps. The first step calls a dialog called filterClothing, and the second step prints the dialog’s result to the console and ends the conversation.

     
  2. 2.

    Follow the structure of the latest section to create a multistep interactive message dialog called filterClothing. Collect three pieces of data to filter a hypothetical clothing collection: garment type, size, and color. Exclusively use menus.

     
  3. 3.

    Make sure to utilize HTTP requests against response_url to update the interactive message.

     

You are now well-versed in exercising the Slack API for multistep interactive messages, one of the cooler Slack features.

Conclusion

The code demonstrated in this chapter is just scratching the surface of the integration possibilities between our Bot Builder bots and different channels. Although we have deliberately focused on Slack use cases, we hope it is clear there are plenty opportunities to reuse our bot code across a spectrum of different experiences both generic and platform-specific in nature.

The powerful abstractions of dialogs, state, and recognizers can be applied across all channels, even when using native mechanisms to invoke the dialogs. We have not yet explored creating a connector for a custom channel. We will examine this in the next chapter.