Exercise - Bots in Microsoft Teams channels and group chats

Completed

Conversation bots can do many things within the Microsoft Teams client. They can proactively send a message to a channel or group chat, listen for and act on Microsoft Teams specific events and even update their own messages.

In this exercise, you’ll modify the existing Microsoft Teams app to update your bot to respond to message reactions, and update or delete messages capabilities.

Important

This exercise assumes you have created the Microsoft Teams app project with the Yeoman generator from the previous exercise in this module.

Detect conversation in a channel

In this section, you'll modify the existing Microsoft Teams app to properly respond to messages from the Posts tab.

From the command line, navigate to the root folder for the project and execute the following command:

gulp ngrok-serve --debug

Important

Recall from a previous exercise, Ngrok will create a new subdomain. You need to update your bot registration's Messaging endpoint in the Azure portal (shown in a previous exercise) with this new domain before testing it.

Install the app in a channel

Navigate to (or create) a Team for testing the channel conversation. In the Teams list, select More options next to the Team. Then select Manage team.

Screenshot of selecting the Manage team option.

Select the Apps tab and then Upload a custom app.

Screenshot of the Apps tab of the Manage Team page.

In the file dialog that appears, select the Microsoft Teams package in your project. This app package is a ZIP file that can be found in the project's ./package folder.

After the package is uploaded, Microsoft Teams displays a summary of the app. Select the Add button, and select Add to a team to install the app.

In a channel of the Team, @ mention the bot, sending the mention command. Notice that the response doesn't mention the user.

Screenshot of the conversation with incorrect response.

Update the bot code

In the previous exercise, our code was looking for the specific message Mention Me to respond. This works in a 1:1 personal chat because the bot isn't mentioned in the conversation.

However, in a channel conversation, a user must @mention the bot to trigger it. This results in a message containing a reference to the bot, not just the message submitted.

While there are multiple ways to address this, let's check the type of conversation the message is from and handle it correctly.

Locate and open the bot in the file ./src/server/conversationalBot/dialogs/mainDialog.ts. Locate the existing actStep() method in the class and find the switch statement that checks the activity text (it's in the else if (this.onboarding) block). Add the following if statement above the switch statement:

if (stepContext.context.activity.conversation.conversationType === "channel") {
  TurnContext.removeRecipientMention(stepContext.context.activity);
}

In the switch statement, the text from the command isn't included. Verify the switch statement to include the text mention me should be listed immediately before the case "mention" statement.

switch (stepContext.context.activity.text) {
  // .. existing code omitted for brevity

  case "mention me":
  case "mention": {
    return await stepContext.beginDialog("mentionUserDialog");
  }

  // .. existing code omitted for brevity
}

In the context of a channel, the repeated prompt "What else can I do for you?" may be too noisy. The prompt can be suppressed when in a channel by updating the finalStep method. Replace the finalStep method with the following:

private async finalStep(stepContext: WaterfallStepContext): Promise<DialogTurnResult> {
  if (stepContext.context.activity.conversation.conversationType !== "channel") {
    return await stepContext.replaceDialog(this.initialDialogId, { restartMsg: "What else can I do for you?" });
  } else {
    return await stepContext.endDialog();
  }
}

Save your changes, update, and test the installed app.

The gulp task will detect the change and run the webpack:server task.

Screenshot of the gulp task running the webpack task.

Once that task completes, @mention the bot again.

Screenshot of the correct bot response.

Reply to messages with Adaptive cards

In this section, you'll update the bot to respond to unknown messages using an Adaptive Card. The card's single action will trigger the bot to update the existing message with a new Adaptive Card. The updated message will include an extra action that will trigger the bot to delete the message.

Run the following command to install the Adaptive Cards SDK and supporting packages:

npm install adaptivecards-templating adaptive-expressions -SE

Create a new file named responseCard.json in the ./src/server/conversationalBot/cards folder. Add the following json that defines an adaptive card:

{
  "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
  "type": "AdaptiveCard",
  "version": "1.4",
  "body": [
    {
      "type": "Container",
      "items": [
        {
          "type": "TextBlock",
          "text": "Adaptive card response",
          "weight": "bolder",
          "size": "large"
        }
      ]
    },
    {
      "type": "Container",
      "items": [
        {
          "type": "TextBlock",
          "text": "${message}",
          "wrap": true
        }
      ]
    },
    {
      "type": "ActionSet",
      "actions": [
        {
          "type": "Action.Execute",
          "title": "Update card",
          "verb": "update",
          "data": {
            "count":"${count}"
          }
        },
        {
          "type": "Action.Execute",
          "title": "Delete card",
          "verb": "delete",
          "$when": "${showDelete}"
        }
      ]
    }
  ]
}

Notice that the card contains binding expressions (${...}) used with Adaptive Card templating. The card also contains actions, which contain verb and data attributes that are sent to the bot when the action is invoked (selected).

Create a new file named responseCard.ts in the ./src/server/conversationalBot/cards folder. Add the following statements to load the card json:

const ResponseCard = require("./responseCard.json");
export default ResponseCard;

Locate and open the dialog in the file ./src/server/conversationalBot/dialogs/mainDialog.ts.

Add the following statements to the top of the file:

import ResponseCard from "../cards/responseCard";
import * as ACData from "adaptivecards-templating";

Update the existing import from botbuilder to include the CardFactory class:

import {
  MessageFactory,
  StatePropertyAccessor,
  InputHints,
  TurnContext,
  CardFactory
} from "botbuilder";

Locate the existing actStep() method in the MainDialog class and find the switch statement that checks the activity text: switch (stepContext.context.activity.text). Replace the default case statements with a call to a method that will load and send the response card:

switch (stepContext.context.activity.text) {
  // existing code omitted for brevity
  default: {
    await this.sendResponseCard(stepContext.context);
    return await stepContext.next();
  }
}

Add the following method to the MainDialog class to send an adaptive card if the bot receives an unknown command:

private async sendResponseCard(turnContext: TurnContext): Promise<void> {
  const cardData = {
    message: "Demonstrates how to respond with a card, update the card and ultimately delete the response.",
    count: 0
  };
  const template = new ACData.Template(ResponseCard);
  const context: ACData.IEvaluationContext = {
    $root: cardData
  };
  const acCard = template.expand(context);
  const attachment = CardFactory.adaptiveCard(acCard);
  await turnContext.sendActivity({ attachments: [attachment] });
}

When the use selects the button that represents an action, the bot will receive an onAdaptiveCardInvoke event. This event is part of the TeamsActivityHandler that is the base class for the ConversationalBot class. The event can be handled by implementing the handler in our class.

Open the file ./src/server/conversationalBot/ConversationalBot.ts.

Add the following import statements to the top of the file:

import ResponseCard from "./cards/responseCard";
import * as ACData from "adaptivecards-templating";

Update the existing import from botbuilder to include the following classes:

import {
  AdaptiveCardInvokeValue,
  AdaptiveCardInvokeResponse,
  StatusCodes,
  // existing imports omitted for brevity
} from "botbuilder";

Add the following method to the ConversationalBot class:

protected async onAdaptiveCardInvoke(context: TurnContext, invokeValue: AdaptiveCardInvokeValue): Promise<any> {
  let cardResponse: AdaptiveCardInvokeResponse;

  try {
    const verb = invokeValue.action.verb;
    switch (verb) {
      case "update":
        {
          let clickCount: number = invokeValue.action.data.count as number;
          const cardData = {
            message: `Updated count: ${++clickCount}`,
            count: clickCount,
            showDelete: true
          };
          const template = new ACData.Template(ResponseCard);
          const context: ACData.IEvaluationContext = {
            $root: cardData
          };
          const acCard = template.expand(context);

          cardResponse = {
            statusCode: StatusCodes.OK,
            type: "application/vnd.microsoft.card.adaptive",
            value: acCard
          } as unknown as AdaptiveCardInvokeResponse;

        }
        break;

      case "delete":
        await context.deleteActivity(context!.activity!.replyToId!);
        return Promise.resolve({
          statusCode: 200,
          type: "application/vnd.microsoft.activity.message",
          value: "Deleting activity..."
        });

      default:
        return Promise.resolve({
          statusCode: 200,
          type: "application/vnd.microsoft.activity.message",
          value: "I don't know how to process that verb"
        });
    }
    return Promise.resolve(cardResponse);
  } catch (error) {
    return Promise.reject(error);
  }
}

In the code you've added, notice the update code block retrieves and increments the count property it received. It then creates a new cardData object with an updated message and boolean property that will render an extra action to delete the card. The method returns an AdaptiveCardInvokeResponse object containing an updated card to render in place of the original.

The delete code block deletes the card using the deleteActivity() method.

Test the bot updating existing messages

Save your changes. The gulp task will detect the change and run the webpack:server task. Wait for that task to complete.

Screenshot of the gulp task running the webpack task.

In the Microsoft Teams client, go to the channel you installed the bot in the previous section. From the Conversations tab, @mention the bot with a random string to trigger the else condition.

The bot will reply to the message with a card:

Screenshot of a message from the bot using cards.

Select the button Update card. After a few seconds, the card should be updated with a new card containing an incremented counter value and a new button:

Screenshot of an updated message from the bot using cards.

Select the Update card button a few more times to see the counter get updated.

Finally, select the Delete card button. After a few seconds, the card will be removed by the bot.

Reply to message reactions

In this section, you'll update the bot to respond when someone likes a message from the bot.

Locate and open the bot in the file ./src/server/conversationalBot/ConversationalBot.ts.

Add the following this.onMessageReaction() handler to the class constructor method:

this.onMessageReaction(async (context, next) => {
  try {
    if (context.activity.reactionsAdded) {
      context.activity.reactionsAdded.forEach(async (reaction) => {
        if (reaction.type === "like") {
          await context.sendActivity("Thank you!");
        }
      });
    }
    await next();
  } catch (error) {
    log("onMessageReaction: error\n", error);
  }
});

This code will execute when a user adds a reaction to a message from the bot. If the reaction is a like, the bot will reply with a "Thank you!" message

Test the bot reacting to message reactions

From the command line, navigate to the root folder for the project and execute the following command:

gulp ngrok-serve --debug

Important

Recall from a previous exercise, Ngrok will create a new subdomain. You need to update your bot registration's Messaging endpoint in the Azure portal (shown in a previous exercise) with this new domain before testing it.

In the Microsoft Teams client, go to the channel you installed the bot in the previous section. From the Conversations tab, find a message from the bot and apply a like reaction to it. After a few seconds, the bot will reply with a message, thanking them for liking the reaction:

Screenshot of a bot replying to a reaction.

Summary

In this exercise, you modified the existing Microsoft Teams app to update your bot to respond to message reactions, and update or delete messages capabilities.

Test your knowledge

1.

Bots added to a channel can do all except which of the following things?

2.

What do you need to do so a bot can respond to a channel or group conversation?