Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Teams] ComposeMessageExtensionAuthBot teams scenario #1250

Merged
merged 6 commits into from
Oct 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
MicrosoftAppId=
MicrosoftAppPassword=
ConnectionName=
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "messagingextension-auth",
"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,209 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import {
Attachment,
CardFactory,
MessagingExtensionActionResponse,
MessagingExtensionAction,
MessagingExtensionQuery,
TaskModuleContinueResponse,
TaskModuleTaskInfo,
TaskModuleRequest,
TeamsActivityHandler,
TurnContext,
BotFrameworkAdapter,
} from 'botbuilder';

import {
IUserTokenProvider,
} from 'botbuilder-core';

EricDahlvang marked this conversation as resolved.
Show resolved Hide resolved
/*
* This Bot requires an Azure Bot Service OAuth connection name in appsettings.json
* see: https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-authentication
*
* Clicking this bot's Task Menu will retrieve the login dialog, if the user is not already signed in.
*/
export class ComposeMessagingExtensionAuthBot extends TeamsActivityHandler {
connectionName: string;
constructor(authConnectionName: string) {
super();

this.connectionName = authConnectionName;

// See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types.
this.onMessage(async (context, next) => {

// Hack around weird behavior of RemoveRecipientMention (it alters the activity.Text)
const originalText = context.activity.text;
TurnContext.removeRecipientMention(context.activity);
const text = context.activity.text.replace(' ', '').toUpperCase();
context.activity.text = originalText;

if (text === 'LOGOUT' || text === 'SIGNOUT')
{
const adapter: IUserTokenProvider = context.adapter as BotFrameworkAdapter;

await adapter.signOutUser(context, this.connectionName);
await context.sendActivity(`Signed Out: ${context.activity.from.name}`);

return;
}

await context.sendActivity(`You said '${context.activity.text}'`);
EricDahlvang marked this conversation as resolved.
Show resolved Hide resolved
// 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 onTeamsMessagingExtensionFetchTask(context: TurnContext, query: MessagingExtensionQuery): Promise<MessagingExtensionActionResponse> {
const adapter: IUserTokenProvider = context.adapter as BotFrameworkAdapter;
const userToken = await adapter.getUserToken(context, this.connectionName);
if (!userToken)
{
// There is no token, so the user has not signed in yet.

// Retrieve the OAuth Sign in Link to use in the MessagingExtensionResult Suggested Actions
const signInLink = await adapter.getSignInLink(context, this.connectionName);

const response : MessagingExtensionActionResponse = {
composeExtension: {
type: 'auth',
suggestedActions: {
actions: [{
type: 'openUrl',
value: signInLink,
title: 'Bot Service OAuth'
}]
}
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got into the practice of casting (so you get some sense of what each block is) - for example:
public static toComposeExtensionResultResponse(cardAttachment: Attachment) : MessagingExtensionActionResponse {

    return <MessagingExtensionActionResponse> {
        composeExtension: <MessagingExtensionResult> {
            type: 'result',
            attachmentLayout: 'list',
            attachments: [ <MessagingExtensionAttachment> cardAttachment ]

        }
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be better ... i've just been casting the top level return, but should be casting the fields too, you're right!

return response;
};

// User is already signed in.
const continueResponse : TaskModuleContinueResponse = {
type: 'continue',
value: this.CreateSignedInTaskModuleTaskInfo(),
};

const response : MessagingExtensionActionResponse = {
task: continueResponse
};

return response;
}

protected async onTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise<TaskModuleTaskInfo> {
var data = context.activity.value;
if (data && data.state)
{
const adapter: IUserTokenProvider = context.adapter as BotFrameworkAdapter;
const tokenResponse = await adapter.getUserToken(context, this.connectionName, data.state);
return this.CreateSignedInTaskModuleTaskInfo(tokenResponse.token);
}
else
{
await context.sendActivity("OnTeamsTaskModuleFetchAsync called without 'state' in Activity.Value");
return null;
}
}

protected async onTeamsMessagingExtensionSubmitAction(context, action: MessagingExtensionAction): Promise<MessagingExtensionActionResponse> {
if (action.data != null && action.data.key && action.data.key == "signout")
{
// User clicked the Sign Out button from a Task Module
await (context.adapter as IUserTokenProvider).signOutUser(context, this.connectionName);
await context.sendActivity(`Signed Out: ${context.activity.from.name}`);
}

return null;
}

private CreateSignedInTaskModuleTaskInfo(token?: string): TaskModuleTaskInfo {
const attachment = this.GetTaskModuleAdaptiveCard();
let width = 350;
let height = 160;
if(token){

const subCard = CardFactory.adaptiveCard({
version: '1.0.0',
type: 'AdaptiveCard',
body: [
{
type: 'TextBlock',
text: `Your token is ` + token,
wrap: true,
}
]
});

const card = attachment.content;
card.actions.push(
{
type: 'Action.ShowCard',
title: 'Show Token',
card: subCard.content,
}
);
width = 500;
height = 300;
}
return {
card: attachment,
height: height,
width: width,
title: 'Compose Extension Auth Example',
};
}

private GetTaskModuleAdaptiveCard(): Attachment {
return CardFactory.adaptiveCard({
version: '1.0.0',
type: 'AdaptiveCard',
body: [
{
type: 'TextBlock',
text: `You are signed in!`,
},
{
type: 'TextBlock',
text: `Send 'Log out' or 'Sign out' to start over.`,
},
{
type: 'TextBlock',
text: `(Or click the Sign Out button below.)`,
},
],
actions: [
{
type: 'Action.Submit',
title: 'Close',
data: {
key: 'close',
}
},
{
type: 'Action.Submit',
title: 'Sign Out',
data: {
key: 'signout',
}
}
]
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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 } from 'botbuilder';

// This bot's main dialog.
import { ComposeMessagingExtensionAuthBot } from './composeMessagingExtensionAuthBot';

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}`);
};

// Create the main dialog.
const myBot = new ComposeMessagingExtensionAuthBot(process.env.ConnectionName);

// Listen for incoming requests.
server.post('/api/messages', (req, res) => {
adapter.processActivity(req, res, async (context) => {
// Route to main dialog.
await myBot.run(context);
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json",
"manifestVersion": "1.5",
"version": "1.0",
"id": "<<Your Bot Id>>",
"packageName": "com.teams.sample.compose.extension",
"developer": {
"name": "Microsoft",
"websiteUrl": "https://www.microsoft.com",
"privacyUrl": "https://www.teams.com/privacy",
"termsOfUseUrl": "https://www.teams.com/termsofuser"
},
"icons": {
"color": "color.png",
"outline": "outline.png"
},
"name": {
"short": "Compose Extension Auth",
"full": "Compose Messaging Extension Auth Example"
},
"description": {
"short": "Bot Service Auth in Compose Extension",
"full": "Demonstrates Bot Service Auth in a Compose Messaging Extension"
},
"accentColor": "#FFFFFF",
"bots": [
{
"botId": "<<Your Bot Id>>",
"scopes": [
"personal",
"team",
"groupchat"
],
"supportsFiles": false,
"isNotificationOnly": false
}
],
"composeExtensions": [
{
"botId": "<<Your Bot Id>>",
"canUpdateConfiguration": false,
"commands": [
{
"id": "loginCommand",
"type": "action",
"title": "Log In",
"description": "Bot Service Auth flow in a Compose Messaging Extension",
"initialRun": false,
"fetchTask": true,
"context": [
"commandBox",
"compose",
"message"
],
"parameters": [
{
"name": "param",
"title": "param",
"description": ""
}
]
}
]
}
],
"permissions": [
"identity",
"messageTeamMembers"
],
"validDomains": [
"*.botframework.com"
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"composite": true,
"declaration": true,
"sourceMap": true,
"outDir": "./lib",
"rootDir": "./src",
}
}