Build a stock update notification bot for Microsoft Teams using Teams Toolkit for Visual Studio Code

Garry Trinder

Microsoft Teams Toolkit for Visual Studio Code enables you to create, debug and deploy Microsoft Teams apps to a Microsoft 365 tenant fast using a no configuration approach.

In the latest release of Teams Toolkit, a new project has been added to the Samples Gallery, Stocks Update Notification Bot, which this tutorial is based on.

If you want to see the finished version, checkout the sample in the gallery or the final source code on the TeamsFX GitHub repository.

By following this tutorial, you will learn how Teams Toolkit helps simplify and accelerate your Microsoft Teams app development by building a bot that obtains data from an external API and sends a rich message using an Adaptive Card into Microsoft Teams on a pre-defined schedule.

A Microsoft Teams chat window is shown with the latest stock price information for MSFT, formatted as an Adaptive Card. A toast notification is shown in the bottom right notifying the user that a card has been sent to them by the Stocks Update Notification Bot

 

Table of Contents

 

🏁 Pre-requisites

To follow this guide successfully, you will need an account that has access to a Microsoft 365 tenant, which has been configured for developing Microsoft Teams apps, have Visual Studio Code and the current LTS version (v16) of nodejs installed.


If you do not have a tenant, I highly recommend that you use a Microsoft 365 Developer Tenant, which you can obtain for free by joining the Microsoft 365 Developer Program.


With the pre-requisites done, let’s begin!

 

🛠 Install Teams Toolkit for Visual Studio Code

Our first step is to install Teams Toolkit into Visual Studio Code, you can do this several ways, however the easiest is way is to search for and install the extension using the Extensions panel from the Side Bar within Visual Studio Code.

  1. Open Visual Studio Code
  2. Open the Extensions panel from the Side Bar
  3. Search for Teams Toolkit in the marketplace
  4. Select Teams Toolkit in the search results
  5. Click the install button to install the extension

Once the extension has been successfully installed, you will see a Microsoft Teams icon in the Side Bar.

Congratulations, you have successfully installed Teams Toolkit for Visual Studio Code and are now ready to create your first Microsoft Teams app! 🎉

 

👷 Scaffold the project

After installing the extension, let’s create a new project.

  1. Open the Teams Toolkit extension by clicking on the Teams icon in the Side Bar
  2. Click the Create a new Teams app button to start the project scaffolding wizard
  3. Select Create a new Teams app in the first step
  4. Select Notification Bot in the Capabilities step
  5. Select Timer Trigger in the Choose triggers step
  6. Select TypeScript in the Programming Language step
  7. Select a folder of your choice in the Workspace folder step, this is the location of where the project will be created.
  8. Enter StocksUpdateNotificationBot in the Application name step. When you complete this step, Teams Toolkit will create a project in a folder with the same name in the Workspace folder location you selected in the previous step

When the project scaffolding is complete, Teams Toolkit will re-open Visual Studio code in the project folder and open a Readme file for the project.

Feel free to look through the Readme file to understand the base project functionality and structure.

Congratulations, you have successfully created your first project with Teams Toolkit that contains a Notification Bot! 🎉

 

🧑‍💻 Sign into your Microsoft 365 tenant

When creating a new project using Teams Toolkit, the next step is to ensure that you have authenticated Teams Toolkit to your Microsoft 365 tenant.

  1. Open the Teams Toolkit extension by clicking on the Teams icon in the Side Bar
  2. In the Accounts section, click Sign in to M365
  3. Click the Sign in button in the prompt, this will open your primary browser and navigate to a Microsoft 365 login screen
  4. Enter your account details to complete the login flow
  5. Return to Visual Studio Code

After successfully authenticating with your Microsoft 365 tenant, you will see that your account name is now showing in the Account section of the Teams Toolkit extension and an indication of the current Sideloading status in your tenant will also be displayed.

Congratulations, you have successfully connected Teams Toolkit with your Microsoft 365 tenant! 🎉

 

ℹ️ Update the Adaptive Card

As our notification bot will use Adaptive Cards to represent our stock price update, let’s update the adaptive card design to fit our needs.

Open the notification-default.json file in the bot/src/adaptiveCards directory and replace the contents with the following JSON.

{
  "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
  "type": "AdaptiveCard",
  "version": "1.4",
  "body": [
    {
      "type": "Container",
      "items": [
        {
          "type": "TextBlock",
          "text": "${name}",
          "size": "Medium",
          "wrap": true
        },
        {
          "type": "TextBlock",
          "text": "${symbol}",
          "isSubtle": true,
          "spacing": "None",
          "wrap": true
        },
        {
          "type": "TextBlock",
          "text": "{{DATE(${timestamp},SHORT)}} {{TIME(${timestamp})}}",
          "wrap": true
        }
      ]
    },
    {
      "type": "Container",
      "spacing": "None",
      "items": [
        {
          "type": "ColumnSet",
          "columns": [
            {
              "type": "Column",
              "width": "stretch",
              "items": [
                {
                  "type": "TextBlock",
                  "text": "${formatNumber(price,2)}",
                  "size": "ExtraLarge",
                  "wrap": true
                },
                {
                  "type": "TextBlock",
                  "text": "${if(change >= 0, '▲', '▼')} ${formatNumber(change,2)} USD (${formatNumber(changePercent, 2)}%)",
                  "spacing": "None",
                  "wrap": true
                }
              ]
            },
            {
              "type": "Column",
              "width": "auto",
              "items": [
                {
                  "type": "FactSet",
                  "facts": [
                    {
                      "title": "Open",
                      "value": "${formatNumber(open,2)}"
                    },
                    {
                      "title": "High",
                      "value": "${formatNumber(high,2)}"
                    },
                    {
                      "title": "Low",
                      "value": "${formatNumber(low,2)}"
                    }
                  ]
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

We are using Adaptive Cards Template Language here to provide separation between our data and layout.

Using binding expressions i.e., ${name}, we can add placeholders to our layout in locations where we want our data to be rendered.

Let’s create an interface to represent the data object to use with the template.

Open the cardModels.ts file in the bot/src directory and replace the contents with the following.

export interface GlobalQuote {
  symbol: string;
  open: number;
  high: number;
  low: number;
  price: number;
  volume: number;
  latestTradingDay: string;
  previousClose: number;
  change: number;
  changePercent: number;
  name?: string;
  timestamp?: string;
}

When we combine our template and data using the Adaptive Cards SDK, the notification will look something like this.

Image successfully creating an Adaptive Card which will represent our stock update notification!

Congratulations, you have successfully created an Adaptive Card which will represent our stock update notification! 🎉

 

📡 Create an API connection

When providing an update, our bot will need to obtain the latest stock price, to do this we will create an API connection to the Alpha Vantage API.

  1. Open the Teams Toolkit extension by clicking on the Teams icon in the Side Bar
  2. In the Development section, select Add features to start the wizard
  3. In the Add features step, select API Connection
  4. In the Enter an API endpoint for local debugging step, enter the Alpha Vantage API endpoint, https://www.alphavantage.co
  5. In the Select component(s) to invoke the API step, select the ./bot checkbox and select OK
  6. In the Enter a friendly name for your API step, enter alphaVantage
  7. In the Select an API authentication type step, select API Key
  8. In the Select the API position in request step, select Query parameter
  9. In the Enter an API Key name step, enter apikey

After completing the wizard, a new folder is created in the bot/src directory called apiConnections, inside it a new file called alphaVantage.ts is generated which contains the logic to create an authenticated API client.

To complete the setup, we must provide a value to pass in the apikey query string parameter.

Open the .env.teamsfx.local file in the bot directory and update the environment variable TEAMSFX_API_ALPHAVANTAGE_API_KEY to TEAMSFX_API_ALPHAVANTAGE_API_KEY=demo

Congratulations, you have successfully created an API connection to an external API using API Key authentication! 🎉

 

🤖 Update the bot logic

Let’s update the logic in the bot.

Open the timerTrigger.ts file, this file contains the core logic of our bot that will be executed on a schedule. For now, remove the contents of the entire file, so we can work from a blank slate.

First, let’s include the imports for the dependant global modules.

import { AzureFunction, Context } from '@azure/functions';
import { AdaptiveCards } from '@microsoft/adaptivecards-tools';
import { AxiosInstance, ConversationBot, TeamsBotInstallation } from '@microsoft/teamsfx';

Let’s cover what these dependencies are.

  • @azure/functions, a module which provides type definitions for working with Azure Functions.
  • @microsoft/adaptivecards-tools, an SDK that makes it easier for developers to work with Adaptive Cards.
  • @microsoft/teamsfx, an SDK that makes it easier for developers to complete common tasks such as implementing single sign on (SSO) and calling external APIs.

Next, lets add our local dependencies.

import { bot } from './internal/initialize';
import { GlobalQuote } from './cardModels';
import template from './adaptiveCards/notification-default.json'
import { alphaVantageClient } from './apiConnections/alphaVantage';

Let’s cover what these dependencies are.

  • ./internal/initialize, is where our bot is initialised using the Bot Framework at run time.
  • ./cardModels, is where we define our data models, in this case we use the GlobalQuote model which represents the quote data that we combine with our Adaptive Card.
  • ./adaptiveCards/notification-default.json, is our representation of our adaptive card.
  • ./apiConnections/alphaVantage, is where we initialise an API client to authenticate and request data from using the Alpha Vantage API.

Now that we have our dependencies in place, lets implement the Azure Function trigger logic.

const timerTrigger: AzureFunction = async function (context: Context, myTimer: any): Promise<void> {
  const timestamp = getTimestamp(new Date());

  getQuoteBySymbol(alphaVantageClient)('MSFT')
    .then(getResponseData)
    .then(extractGlobalQuote)
    .then(transformGlobalQuote)
    .then(quote => addTimestamp(quote)(timestamp))
    .then(quote => addCompanyName(quote)('Microsoft Corporation'))
    .then(quote =>
      getInstallations(bot)
        .then(targets => targets.map(target => sendCard(target)(AdaptiveCards)(template)(quote)))
    )
    .catch(err => handleError(err)(context));

}

export default timerTrigger;

Whilst this is a lot to unpack, the general flow of the function is

  1. Get current timestamp
  2. Get quote data from Alpha Vantage API
  3. Transform quote data, adding timestamp and company name
  4. Get bot installations
  5. Iterate over bot installations and send an Adaptive Card

Next, we will implement the functions that this handler uses.

Add these after the timerTrigger function but before the export statement.

The following functions, getTimestamp and makeValidAdaptiveCardISOString, are used to return a valid time stamp that can be used in an Adaptive Card.

const getTimestamp =
  (date: Date): string =>
    makeValidAdaptiveCardISOString(date.toISOString());

const makeValidAdaptiveCardISOString =
  (iso: string): string =>
    `${iso.split('.')[0]}Z`;

The following functions, getQuoteBySymbol and getResponseData, are used to send a request to the Alpha Vantage API and obtain the response data from the API client.

const getQuoteBySymbol =
  (apiClient: AxiosInstance) =>
    (symbol: string) =>
      apiClient.get(`/query?function=GLOBAL_QUOTE&symbol=${symbol}`)

const getResponseData =
  (res: any): object =>
    res.data;

The following functions, extractGlobalQuote, transformGlobalQuote, removePercent, addTimestamp and addCompanyName, are used to manipulate the API response data and convert it into a GlobalQuote object which we can use with our Adaptive Card.

const extractGlobalQuote =
  (data: object): GlobalQuote =>
    data['Global Quote'];

const transformGlobalQuote =
  (quote: GlobalQuote): GlobalQuote => ({
    symbol: Object.values(quote)[0],
    open: Number.parseFloat(Object.values(quote)[1]),
    high: Number.parseFloat(Object.values(quote)[2]),
    low: Number.parseFloat(Object.values(quote)[3]),
    price: Number.parseFloat(Object.values(quote)[4]),
    volume: Number.parseFloat(Object.values(quote)[5]),
    latestTradingDay: Object.values(quote)[6],
    previousClose: Number.parseFloat(Object.values(quote)[7]),
    change: Number.parseFloat(Object.values(quote)[8]),
    changePercent: Number.parseFloat(removePercent(Object.values(quote)[9]))
  });

const removePercent =
  (string: string): string =>
    string.replace('%', '');

const addTimestamp =
  (quote: GlobalQuote) =>
    (timestamp: string): GlobalQuote => { return { ...quote, timestamp } }

const addCompanyName =
  (quote: GlobalQuote) =>
    (name: string): GlobalQuote => { return { ...quote, name } }

The following function, getInstallations, is used to return an array of locations where the bot is installed.

const getInstallations =
  (bot: ConversationBot): Promise<TeamsBotInstallation[]> =>
    bot.notification.installations();

The following function, sendCard, is used to send an Adaptive Card to a bot installation location, this is also where the card template and quote data is merged.

const sendCard =
  <T extends object>(target: TeamsBotInstallation) =>
    (ac: typeof AdaptiveCards) =>
      (template: object) =>
        (quote: GlobalQuote): Promise<any> =>
          target.sendAdaptiveCard(ac.declare<T>(template).render(quote as T));

Finally, the following function, handleError, is used as a basic error handler, logging out any errors using the Azure Function context log.

const handleError =
  (err: Error) =>
    (context: Context) =>
      context.log({ err });

Congratulations, you have successfully updated the bot to request data from an external API and render an Adaptive Card in the locations where the bot is installed! 🎉

 

🧪 Test your bot!

Now it is time to see our bot in action!

Open the Debug panel from the Side Bar, select your browser of choice from the drop-down menu and start the bot by pressing the F5 key.

Now, sit back, relax and watch the magic of Teams Toolkit in action 🪄

When you start the bot Teams Toolkit will perform several checks of your environment, ensuring that all the required dependencies are installed.

  • Checks that a supported version of nodejs is installed.
  • Checks Teams Toolkit has been authenticated with a Microsoft 365 tenant, that has Sideloading enabled.
  • Checks that required npm packages are installed, if not these are installed automatically.
  • Checks that Azure Functions Core Tools are installed, if not, this is installed automatically.
  • Checks that ngrok is installed, if not, this is installed automatically.
  • Checks that the required Ports are open.

In addition to the above checks, Teams Toolkit is also registering a bot with Bot Framework and updating the app manifest for you.

Once all the checks have been passed, Teams Toolkit will launch a browser session, you may be prompted to sign in with your account the first time you launch your bot, if so, complete the sign in flow.

After signing into Microsoft 365, the web version of Microsoft Teams is opened and a prompt to sideload your app will be shown. Click the Add button to install the bot as a personal app.

Wait a few seconds for your bot to be executed, sending a message into Microsoft Teams with the latest stock price.

Congratulations, you have just created your first notification bot using Microsoft Teams Toolkit 🎉

 

⏭ What’s next?

Want to make more updates? Here are a few things you can try.

  • Use a different symbol, currently the stock price for MSFT is returned, pass a different symbol into the getQuoteBySymbol function and update the company name.
  • Change the timer frequency, currently the timer trigger is executed every 30 seconds, change this by updating the schedule property in the json file located in the bot/src/notifyTimerTrigger directory.
  • Update the app manifest, currently the app manifest contains mostly default values, use the Edit manifest file feature in Teams Toolkit to open the app manifest template and makes changes to it.
  • Update the app icons, currently the app uses stock icons, update the icons in the templates/appPackage/resources directory with new more relevant icons.
  • Disable the bot chat compose box, by default the bot chat compose box is enabled to disable this edit the app manifest and update the isNotificationOnly property to
  • Restrict supported Teams scopes, by default the bot can be installed into a personal, chat or channel context, to restrict where the bot can be used, update the scopes array in the app manifest.

 

📚 Learn more!

Happy coding!