m365-msteams-northwind-app-samples

Sample code for Microsoft Teams application with SSO, Bots, Messaging extensions and other capabilities.

Teams App Camp

Add a Messaging Extension

This lab is part of extending app capabilities for your teams app which begins with a Northwind Orders core application using the AAD path. The core app is the boilerplate application with which you will do this lab.

Complete labs A01-A03 for deeper understanding of how the core application works, to set up AAD application registration etc. to update the .env file as per the .env_sample. This configuration is required for the success of the lab.

The completed lab is here

So far you have see how you can bring your application into teams but in this exercise we will explore how you can streamline work using the capabilities in the Microsoft Teams development platform.

Suppose you want to search some data in an external system (in our case the Northwind database) and share the result in a conversation. Or you want to do an action like create, add or update data into this external system and still share all this in a conversation in Teams. All this is possible using Messaging extensions capability in Teams.

We will cover the following concepts in this exercise:

Features

A fully working sample can be found here

Exercise 1: Bot registration


Messaging extensions allow users to bring the application into a conversation in Teams. You can search data in your application, perform actions on them and send back results of your interaction to your application as well as Teams to display all results in a rich card in the conversation.

Since it is a conversation between your application’s web service and teams, you’ll need a secure communication protocol to send and receive messages like the Bot Framework’s messaging schema.

You’ll need to register your web service as a bot in the Bot Framework and update the app manifest file to define your web service so Teams client can know about it.

Step 1: Run ngrok

Ignore this step if you have ngrok already running

Start ngrok to obtain the URL for your application. Run this command in the command line tool of your choice:

ngrok http 3978 -host-header=localhost

The terminal will display a screen like below; Save the URL for Step 2.

ngrok output

Step 2: Register your web service as an bot using Teams Developer Portal

Exercise 2: Code changes


Step 1: Add new files & folders

There are new files and folders that you need to add into the project structure.

}


#### Step 2: Update existing files
There are files that were updated to add the new features.
Let's take files one by one to understand what changes you need to make for this exercise. 

**1. manifest\makePackage.js**
The npm script that builds a manifest file by taking the values from your local development configuration like `.env` file, need an extra token for the Bot we just created. Let's add that token `BOT_REG_AAD_APP_ID` (bot id) into the script.

Replace code block:
<pre>
if (key.indexOf('TEAMS_APP_ID') === 0 ||
            key.indexOf('HOSTNAME') === 0 ||
            key.indexOf('CLIENT_ID') === 0) {
</pre>
With: 
<pre>
 if (key.indexOf('TEAMS_APP_ID') === 0 ||
            key.indexOf('HOSTNAME') === 0 ||
            key.indexOf('CLIENT_ID') === 0||
           <b> key.indexOf('BOT_REG_AAD_APP_ID') === 0) {</b>
</pre>

**2.manifest\manifest.template.json**

Add the messaging extension command information (bolded) in the manifest after `showLoadingIndicator` property:
<pre>
{
  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.8/MicrosoftTeams.schema.json",
  "manifestVersion": "1.8",
  "version": "1.6.0",
  "id": "&lt;TEAMS_APP_ID&gt;",
  "packageName": "io.github.officedev.teamsappcamp1.northwind",
  "developer": {
    "name": "Northwind Traders",
    "websiteUrl": "https://&lt;HOSTNAME&gt;/",
    "privacyUrl": "https://&lt;HOSTNAME&gt;/privacy.html",
    "termsOfUseUrl": "https://&lt;HOSTNAME&gt;/termsofuse.html"
  },
  "icons": {
      "color": "northwind192.png",
      "outline": "northwind32.png"
  },
  "name": {
    "short": "Northwind Orders",
    "full": "Northwind Traders Order System"
  },
  "description": {
    "short": "Sample enterprise app using the Northwind Traders sample database",
    "full": "Simple app to demonstrate porting a SaaS app to Microsoft Teams"
  },
  "accentColor": "#FFFFFF",
  "configurableTabs": [
    {
        "configurationUrl": "https://&lt;HOSTNAME&gt;/pages/tabConfig.html",
        "canUpdateConfiguration": true,
        "scopes": [
            "team",
            "groupchat"
        ]
    }
],
"staticTabs": [
    {
      "entityId": "Orders",
      "name": "My Orders",
      "contentUrl": "https://&lt;HOSTNAME&gt;/pages/myOrders.html",
      "websiteUrl": "https://&lt;HOSTNAME&gt;/pages/myOrders.html",
      "scopes": [
        "personal"
      ]
    },
    {
      "entityId": "Products",
      "name": "Products",
      "contentUrl": "https://&lt;HOSTNAME&gt;/pages/categories.html",
      "websiteUrl": "https://&lt;HOSTNAME&gt;/pages/categories.html",
      "scopes": [
        "personal"
      ]
    }
  ],
  "showLoadingIndicator": false,
  <b>"composeExtensions": [
    {
      "botId": "&lt;BOT_REG_AAD_APP_ID&gt;",
      "canUpdateConfiguration": true,
      "commands": [
        {
          "id": "productSearch",
          "type": "query",
          "title": "Find product",
          "description": "",
          "initialRun": false,
          "fetchTask": false,
          "context": [
            "commandBox",
            "compose"
          ],
          "parameters": [
            {
              "name": "productName",
              "title": "product name",
              "description": "Enter the product name",
              "inputType": "text"
            }
          ]
        }
      ]
    }
  ], 
  "bots": [
    {
      "botId": "&lt;BOT_REG_AAD_APP_ID&gt;",
      "scopes": [ "personal", "team", "groupchat" ],
      "isNotificationOnly": false,
      "supportsFiles": false
    }
  ],

</b>
  
  "permissions": [
      "identity",
      "messageTeamMembers"
  ],
  "validDomains": [
      "&lt;HOSTNAME&gt;"
  ],
  "webApplicationInfo": {
      "id": "&lt;CLIENT_ID&gt;",
      "resource": "api://&lt;HOSTNAME>/&lt;CLIENT_ID&gt;"
  }
}
</pre>

Update the version number so it's greater than it was; for example if your manifest was version 1.4, make it 1.4.1 or 1.5.0. This is required in order for you to update the app in Teams.

~~~json
"version": "1.5.0"
~~~

**3.server\identityService.js**

Add a condition to let validation  be performed by Bot Framework Adapter.
In the function `validateApiRequest()`, add an `if` condition and check if request is from `bot` then move to next step.

<pre>
  if (req.path==="/messages") {
        console.log('Request for bot, validation will be performed by Bot Framework Adapter');
        next();
    } else {
       //do the rest
    }
</pre>

The final form of the function definition will look as below:
<pre>
async function validateApiRequest(req, res, next) {
    const audience = `api://${process.env.HOSTNAME}/${process.env.CLIENT_ID}`;
    const token = req.headers['authorization'].split(' ')[1];

    <b>if (req.path==="/messages") {
        console.log('Request for bot, validation will be performed by Bot Framework Adapter');
        next();
    } else {
       </b> aad.verify(token, { audience: audience }, async (err, result) => {
            if (result) {
                console.log(`Validated authentication on /api${req.path}`);
                next();
            } else {
            console.error(`Invalid authentication on /api${req.path}: ${err.message}`);
                res.status(401).json({ status: 401, statusText: "Access denied" });
            }
        });
   <b> }</b>
}
</pre>
**4.server\northwindDataService.js**

Add two new functions as below
- <b>getProductByName()</b> - This will search products by name and bring the top 5 results back to the messaging extension's search results.
- <b>updateProductUnitStock()</b> - This will update the value of unit stock based on the input action of a user on the product result card.

Add the two new function definitions by appending below code block into the file:

```javascript
export async function getProductByName(productNameStartsWith) {
    let result = {};

    const products = await db.getTable("Products", "ProductID");
    const match = productNameStartsWith.toLowerCase();
    const matchingProducts =
        products.data.filter((item) => item.ProductName.toLowerCase().startsWith(match));

    result = matchingProducts.map(product => ({
        productId: product.ProductID,
        productName: product.ProductName,
        unitsInStock: product.UnitsInStock,
        categoryId: product.CategoryID
    }));

    return result;
}

export async function updateProductUnitStock(productId, unitsInStock) {

    const products = await db.getTable("Products", "ProductID");
    const product = products.item(productId);
    product.UnitsInStock = unitsInStock;
    productCache[productId] = null;         // Clear the product cache
    categoryCache[product.CategoryID]=null;// Clear the category cache for this product  
    await products.save();                  // Write the products "table"

}

5.server\server.js

Import the needed modules for bot related code. Import required bot service from botbuilder package and the bot StockManagerBot from the newly added file bot.js

import {StockManagerBot} from './bot.js';
import { BotFrameworkAdapter } from 'botbuilder';

As a standard , app.listen() should always be at the end of the file, so any code update should happen before this request.

A bot adapter authenticates and connects a bot to a service endpoint to send and receive message. So to authenticate, we’ll need to pass the bot registration’s AAD app id and app secret. Add below code to initialize the bot adapter.

const adapter = new BotFrameworkAdapter({
  appId: process.env.BOT_REG_AAD_APP_ID,
  appPassword:process.env.BOT_REG_AAD_APP_PASSWORD
});

Create the bot that will handle incoming messages.

const stockManagerBot = new StockManagerBot();

For the main dialog add error handling.

// Catch-all for errors.
const onTurnErrorHandler = async (context, error) => {
  // This check writes out errors to console log .vs. app insights.
  // NOTE: In production environment, you should consider logging this to Azure
  //       application insights.
  console.error(`\n [onTurnError] unhandled error: ${ error }`);

  // Send a trace activity, which will be displayed in Bot Framework Emulator
  await context.sendTraceActivity(
      'OnTurnError Trace',
      `${ error }`,
      'https://www.botframework.com/schemas/error',
      'TurnError'
  );

  // Send a message to the user
  await context.sendActivity('The bot encountered an error or bug.');
  await context.sendActivity('To continue to run this bot, please fix the bot source code.');
};
// Set the onTurnError for the singleton BotFrameworkAdapter.
adapter.onTurnError = onTurnErrorHandler;

Listen for incoming requests.


app.post('/api/messages', (req, res) => {
  adapter.processActivity(req, res, async (context) => {
    await stockManagerBot.run(context);
  }).catch(error=>{
    console.log(error)
  });
});

6. package.json

You’ll need to install additional packages for adaptive cards and botbuilder. Add below packages into the package.json file by run below script to install new packages:

npm i adaptive-expressions adaptivecards adaptivecards-templating botbuilder

Check if packages are added into dependencies in the package.json file:

    "adaptive-expressions": "^4.15.0",
    "adaptivecards": "^2.10.0",
    "adaptivecards-templating": "^2.2.0",   
    "botbuilder": "^4.15.0"

7. .env

Open the .env file in your working directory and add two new tokens BOT_REG_AAD_APP_ID(Bot id) and BOT_REG_AAD_APP_PASSWORD(client secret) with values copied in Step 2.

The .env file contents will now look like below:

COMPANY_NAME=Northwind Traders
PORT=3978

TEAMS_APP_ID=c42d89e3-19b2-40a3-b20c-44cc05e6ee26
HOSTNAME=yourhostname.ngrok.io

TENANT_ID=c8888ec7-a322-45cf-a170-7ce0bdb538c5
CLIENT_ID=b323630b-b67c-4954-a6e2-7cfa7572bbc6
CLIENT_SECRET=111111.ABCD
BOT_REG_AAD_APP_ID=88888888-0d02-43af-85d7-72ba1d66ae1d
BOT_REG_AAD_APP_PASSWORD=111111vk

Exercise 3: Test the changes


Now that you have applied all code changes, let’s test the features.

Step 1 : Create new teams app package

Make sure the env file is configured as per the sample file .env_Sample. Make sure all npm packages are installed, run below script in the command line tool

npm i

Create updated teams app package by running below script:

npm run package

Step 2: Start your local project

Now it’s time to run your updated application and run it in Microsoft Teams. Start the application by running below command:

npm start

Step 3: Upload the app package

In the Teams web or desktop UI, click “Apps” in the sidebar 1️⃣, then “Manage your apps” 2️⃣. At this point you have three choices:

In this case, choose the first option 3️⃣.

Upload the app

Navigate to the Northwind.zip file in your manifest directory and upload it. The Teams client will display the application information, add the application to a team or a group chat. Add the app

Step 4 : Run the application in Teams client

We have added the app into a Group chat for demonstration. Go to the chat where the app is installed.

Open the messaging extension app from the compose area. Open the app

Search for the product from the messaging extension (This should be easy if you have used GIPHY before 😉) Search product

Select the product you want to add in the conversation. Select product

A little preview will be shown in the message compose area. Note at the time this lab was created, there is an outstanding platform issue related to the preview. If you are in a Teams team, this will be blank. Hence showing this capability in a group chat.

This is the product card, with a form to fill in and submit, incase the unit stock value has to be changed.

Product card

Fill in a new value in the form, and select Update stock. Product update form

Once it’s success fully updated, the card refreshes to show the new stock value. Product updated

The new stock values are not saved back into the northwind database as it is only read only for this lab. You can call your CRUD operations suitably in your application. Here we are manipulating the cache.

Next steps

After completing this lab, you may continue with any of the following labs.