Skip to content

Commit

Permalink
add search based messaging extension from samples (thanks Steven) (#1267
Browse files Browse the repository at this point in the history
)
  • Loading branch information
Eric Dahlvang authored Oct 7, 2019
1 parent b12e442 commit 42a2614
Show file tree
Hide file tree
Showing 10 changed files with 467 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
MicrosoftAppId=
MicrosoftAppPassword=
host=
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Teams Search-based Messaging Extensions Bot

Search-based Messaging Extensions allow users to have bots search for information on the user's behalf in Teams.

___

To create a Teams Bot that contains Search-based Messaging Extensions, users must first create a [Teams Manifest](https://docs.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) which will outline where the extension is available.

> _Note_
>
> _The name of the feature was changed from "compose extension" to "messaging extension" in November, 2017, but the manifest name remains the same so that existing extensions continue to function._
### Example Teams Manifest for Search-based Messaging Extensions bot

```json
{
"$schema": "https://github.com/OfficeDev/microsoft-teams-app-schema/blob/preview/DevPreview/MicrosoftTeams.schema.json",
"manifestVersion": "devPreview",
"version": "1.0",
"id": "<<YOUR_GENERATED_APP_GUID>>",
"packageName": "com.microsoft.teams.samples.v4bot",
"developer": {
"name": "Microsoft Corp",
"websiteUrl": "https://example.azurewebsites.net",
"privacyUrl": "https://example.azurewebsites.net/privacy",
"termsOfUseUrl": "https://example.azurewebsites.net/termsofuse"
},
"name": {
"short": "search-extension-settings",
"full": "Microsoft Teams V4 Search Messaging Extension Bot and settings"
},
"description": {
"short": "Microsoft Teams V4 Search Messaging Extension Bot and settings",
"full": "Sample Search Messaging Extension Bot using V4 Bot Builder SDK and V4 Microsoft Teams Extension SDK"
},
"icons": {
"outline": "icon-outline.png",
"color": "icon-color.png"
},
"accentColor": "#abcdef",
"bots": [
{
"botId": "<<YOUR_BOTS_MSA_APP_ID>>",
"scopes": ["personal", "team"]
}
],
"composeExtensions": [
{
"botId": "<<YOUR_BOTS_MSA_APP_ID>>",
"canUpdateConfiguration": true,
"commands": [
{
"id": "searchQuery",
"context": ["compose", "commandBox"],
"description": "Test command to run query",
"title": "Search",
"type": "query",
"parameters": [
{
"name": "searchQuery",
"title": "Search Query",
"description": "Your search query",
"inputType": "text"
}
]
}
],
"messageHandlers": [
{
"type": "link",
"value": {
"domains": [
"*.ngrok.io"
]
}
}
]
}
],
"validDomains": [
"*.ngrok.io"
]
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "search-extension-bot",
"version": "1.0.0",
"description": "",
"main": "./lib/index.js",
"scripts": {
"start": "tsc --build && node ./lib/index.js",
"build": "tsc --build",
"watch": "nodemon --watch ./src -e ts --exec \"npm run start\""
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"botbuilder": "file:../../../",
"dotenv": "^8.1.0",
"node-fetch": "^2.6.0",
"restify": "^8.4.0",
"uuid": "^3.3.3"
},
"devDependencies": {
"@types/node": "^12.7.1",
"@types/node-fetch": "^2.5.0",
"@types/request": "^2.48.1",
"@types/restify": "^7.2.7",
"nodemon": "^1.19.1",
"ts-node": "^7.0.1",
"typescript": "^3.2.4"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { config } from 'dotenv';
import * as path from 'path';
import * as restify from 'restify';

// Import required bot services.
// See https://aka.ms/bot-services to learn more about the different parts of a bot.
import { BotFrameworkAdapter, MemoryStorage, UserState } from 'botbuilder';

// This bot's main dialog.
import { TeamsSearchExtensionBot } from './teamsSearchExtensionBot';

const ENV_FILE = path.join(__dirname, '..', '.env');
config({ path: ENV_FILE });

// Create HTTP server.
const server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, () => {
console.log(`\n${server.name} listening to ${server.url}`);
console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`);
console.log(`\nTo test your bot, see: https://aka.ms/debug-with-emulator`);
});

// Create adapter.
// See https://aka.ms/about-bot-adapter to learn more about adapters.
const adapter = new BotFrameworkAdapter({
appId: process.env.MicrosoftAppId,
appPassword: process.env.MicrosoftAppPassword
});

// Catch-all for errors.
adapter.onTurnError = async (context, error) => {
// This check writes out errors to console log .vs. app insights.
console.error('[onTurnError]:');
console.error(error);
// Send a message to the user
await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`);
};

// Define a state store for your bot. See https://aka.ms/about-bot-state to learn more about using MemoryStorage.
// A bot requires a state store to persist the dialog and user state between messages.

// For local development, in-memory storage is used.
// CAUTION: The Memory Storage used here is for local bot debugging only. When the bot
// is restarted, anything stored in memory will be gone.
const memoryStorage = new MemoryStorage();
const userState = new UserState(memoryStorage);

// Create the main dialog.
const myBot = new TeamsSearchExtensionBot(userState);

server.get('/*', restify.plugins.serveStatic({
directory: path.join(__dirname, '../static'),
appendRequestPath: false
}));

// Listen for incoming requests.
server.post('/api/messages', (req, res) => {
adapter.processActivity(req, res, async (context) => {
// Route to main dialog.
await myBot.run(context);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import {
ActionTypes,
CardFactory,
BotState,
MessagingExtensionResponse,
MessagingExtensionResult,
MessagingExtensionQuery,
TeamsActivityHandler,
TurnContext,
MessagingExtensionSuggestedAction,
MessagingExtensionActionResponse,
AppBasedLinkQuery
} from 'botbuilder';

const RICH_CARD_PROPERTY = 'richCardConfig';

export class TeamsSearchExtensionBot extends TeamsActivityHandler {

// For this example, we're using UserState to store the user's preferences for the type of Rich Card they receive.
// Users can specify the type of card they receive by configuring the Messaging Extension.
// To store their configuration, we will use the userState passed in via the constructor.

/**
* We need to change the key for the user state because the bot might not be in the conversation, which means they get a 403 error.
* @param userState
*/
constructor(public userState: BotState) {
super();

// See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types.
this.onMessage(async (context, next) => {
await context.sendActivity(`You said '${context.activity.text}'`);
// By calling next() you ensure that the next BotHandler is run.
await next();
});

this.onMembersAdded(async (context, next) => {
const membersAdded = context.activity.membersAdded;
for (const member of membersAdded) {
if (member.id !== context.activity.recipient.id) {
await context.sendActivity('Hello and welcome!');
}
}

// By calling next() you ensure that the next BotHandler is run.
await next();
});
}

protected async onTeamsMessagingExtensionQuery(context: TurnContext, query: MessagingExtensionQuery): Promise<MessagingExtensionResponse>{
const accessor = this.userState.createProperty<{ useHeroCard: boolean }>(RICH_CARD_PROPERTY);
const config = await accessor.get(context, { useHeroCard: true });

const searchQuery = query.parameters[0].value;
const cardText = `You said "${searchQuery}"`;
let composeExtensionResponse: MessagingExtensionResponse;

const bfLogo = 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU';
const button = { type: 'openUrl', title: 'Click for more Information', value: "https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/bots/bots-overview" };

if (config.useHeroCard) {
const heroCard = CardFactory.heroCard('You searched for:', cardText, [bfLogo], [button]);
const preview = CardFactory.heroCard('You searched for:', cardText, [bfLogo]);

const secondPreview = CardFactory.heroCard('Learn more about Teams:', "https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/bots/bots-overview", [bfLogo]);
const secondHeroCard = CardFactory.heroCard('Learn more about Teams:', "https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/bots/bots-overview", [bfLogo], [button]);
composeExtensionResponse = {
composeExtension: {
type: 'result',
attachmentLayout: 'list',
attachments: [
{ ...heroCard, preview },
{ ...secondHeroCard, preview: secondPreview }
]
}
}
} else {
const thumbnailCard = CardFactory.thumbnailCard('You searched for:', cardText, [bfLogo], [button]);
const preview = CardFactory.thumbnailCard('You searched for:', cardText, [bfLogo]);

const secondPreview = CardFactory.thumbnailCard('Learn more about Teams:', "https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/bots/bots-overview", [bfLogo]);
const secondThumbnailCard = CardFactory.thumbnailCard('Learn more about Teams:', "https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/bots/bots-overview", [bfLogo], [button]);
composeExtensionResponse = {
composeExtension: {
type: 'result',
attachmentLayout: 'list',
attachments: [
{ ...thumbnailCard, preview },
{ ...secondThumbnailCard, preview: secondPreview }
]
}
}
}

return composeExtensionResponse;
}

protected async onTeamsAppBasedLinkQuery(context: TurnContext, query: AppBasedLinkQuery): Promise<MessagingExtensionResponse>{
const accessor = this.userState.createProperty<{ useHeroCard: boolean }>(RICH_CARD_PROPERTY);
const config = await accessor.get(context, { useHeroCard: true });

const url = query.url;
const cardText = `You entered "${url}"`;
let composeExtensionResponse: MessagingExtensionResponse;

const bfLogo = 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU';
const button = { type: 'openUrl', title: 'Click for more Information', value: "https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/bots/bots-overview" };

if (config.useHeroCard) {
const heroCard = CardFactory.heroCard('HeroCard for Link Unfurling:', cardText, [bfLogo], [button]);
const preview = CardFactory.heroCard('HeroCard for Link Unfurling:', cardText, [bfLogo]);

composeExtensionResponse = {
composeExtension: {
type: 'result',
attachmentLayout: 'list',
attachments: [
{ ...heroCard, preview }
]
}
}
} else {
const thumbnailCard = CardFactory.thumbnailCard('ThumbnailCard for Link Unfurling:', cardText, [bfLogo], [button]);
const preview = CardFactory.thumbnailCard('ThumbnailCard for Link Unfurling:', cardText, [bfLogo]);

composeExtensionResponse = {
composeExtension: {
type: 'result',
attachmentLayout: 'list',
attachments: [
{ ...thumbnailCard, preview }
]
}
}
}

return composeExtensionResponse;
}

protected async onTeamsMessagingExtensionConfigurationQuerySettingUrl(context: TurnContext, query: MessagingExtensionQuery){
return <MessagingExtensionActionResponse>
{
composeExtension: <MessagingExtensionResult> {
type: 'config',
suggestedActions: <MessagingExtensionSuggestedAction> {
actions: [
{
type: ActionTypes.OpenUrl,
title: 'Config',
value: process.env.host + '/composeExtensionSettings.html',
},
]
}
}
}
}

protected async onTeamsMessagingExtensionConfigurationSetting(context: TurnContext, settings: MessagingExtensionQuery){
// This event is fired when the settings page is submitted
const accessor = this.userState.createProperty<{ useHeroCard: boolean }>(RICH_CARD_PROPERTY);
const config = await accessor.get(context, { useHeroCard: true });

if (settings.state === 'hero') {
config.useHeroCard = true;
} else if (settings.state === 'thumbnail') {
config.useHeroCard = false;
}

// We should save it after we send the message back to Teams.
await this.userState.saveChanges(context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<title>Bot Info</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src='https://statics.teams.microsoft.com/sdk/v1.0/js/MicrosoftTeams.min.js'></script>
<script src='https://code.jquery.com/jquery-1.11.3.min.js'></script>
</head>

<body>
<p>I prefer:</p>
<button onclick="selectThumbnailCards()">Thumbnail Cards</button>
<button onclick="selectHeroCards()">Hero Cards</button>
<script>
var microsoftTeams;

$(document).ready(function () {
microsoftTeams.initialize();
});

function selectThumbnailCards() {
microsoftTeams.authentication.notifySuccess('thumbnail');
}

function selectHeroCards() {
microsoftTeams.authentication.notifySuccess('hero');
}
</script>
</body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 42a2614

Please sign in to comment.