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] Part of Teams Hackathon Feedback #1308

Merged
merged 9 commits into from
Oct 16, 2019
4 changes: 3 additions & 1 deletion libraries/botbuilder/src/botFrameworkAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ const USER_AGENT: string = `Microsoft-BotFramework/3.1 BotBuilder/${ pjson.versi
`(Node.js,Version=${ NODE_VERSION }; ${ TYPE } ${ RELEASE }; ${ ARCHITECTURE })`;
const OAUTH_ENDPOINT = 'https://api.botframework.com';
const US_GOV_OAUTH_ENDPOINT = 'https://api.botframework.azure.us';
const INVOKE_RESPONSE_KEY: symbol = Symbol('invokeResponse');

// This key is exported internally so that the TeamsActivityHandler will not overwrite any already set InvokeResponses.
export const INVOKE_RESPONSE_KEY: symbol = Symbol('invokeResponse');

/**
* A [BotAdapter](xref:botbuilder-core.BotAdapter) that can connect a bot to a service endpoint.
Expand Down
28 changes: 14 additions & 14 deletions libraries/botbuilder/src/teamsActivityHandler.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
/**
* @module botbuilder
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { InvokeResponse } from './botFrameworkAdapter';
import { InvokeResponse, INVOKE_RESPONSE_KEY } from './botFrameworkAdapter';

import {
ActivityTypes,
ActivityHandler,
ActivityTypes,
AppBasedLinkQuery,
ChannelAccount,
ChannelInfo,
Expand Down Expand Up @@ -37,7 +40,8 @@ export class TeamsActivityHandler extends ActivityHandler {
switch (context.activity.type) {
case ActivityTypes.Invoke:
const invokeResponse = await this.onInvokeActivity(context);
if (invokeResponse) {
// If onInvokeActivity has already sent an InvokeResponse, do not send another one.
if (invokeResponse && !context.turnState.get(INVOKE_RESPONSE_KEY)) {
await context.sendActivity({ value: invokeResponse, type: 'invokeResponse' });
}
break;
Expand All @@ -58,7 +62,8 @@ export class TeamsActivityHandler extends ActivityHandler {
} else {
switch (context.activity.name) {
case 'signin/verifyState':
return await this.onTeamsSigninVerifyState(context, context.activity.value);
await this.onTeamsSigninVerifyState(context, context.activity.value);
return TeamsActivityHandler.createInvokeResponse();

case 'fileConsent/invoke':
return TeamsActivityHandler.createInvokeResponse(await this.onTeamsFileConsent(context, context.activity.value));
Expand Down Expand Up @@ -94,15 +99,10 @@ export class TeamsActivityHandler extends ActivityHandler {
return TeamsActivityHandler.createInvokeResponse();

case 'task/fetch':
const fetchResponse = await this.onTeamsTaskModuleFetch(context, context.activity.value);
const taskModuleContineResponse = { type: 'continue', value: fetchResponse };
const taskModuleResponse = { task: taskModuleContineResponse };
return TeamsActivityHandler.createInvokeResponse(taskModuleResponse);
return TeamsActivityHandler.createInvokeResponse(await this.onTeamsTaskModuleFetch(context, context.activity.value));

case 'task/submit':
const submitResponseBase = await this.onTeamsTaskModuleSubmit(context, context.activity.value);
const taskModuleResponse_submit = { task: submitResponseBase };
return TeamsActivityHandler.createInvokeResponse(taskModuleResponse_submit);
return TeamsActivityHandler.createInvokeResponse(await this.onTeamsTaskModuleSubmit(context, context.activity.value));

default:
throw new Error('NotImplemented');
Expand Down Expand Up @@ -183,7 +183,7 @@ export class TeamsActivityHandler extends ActivityHandler {
* @param context
* @param action
*/
protected async onTeamsSigninVerifyState(context: TurnContext, query: SigninStateVerificationQuery): Promise<InvokeResponse> {
protected async onTeamsSigninVerifyState(context: TurnContext, query: SigninStateVerificationQuery): Promise<void> {
throw new Error('NotImplemented');
}

Expand All @@ -201,7 +201,7 @@ export class TeamsActivityHandler extends ActivityHandler {
* @param context
* @param taskModuleRequest
*/
protected async onTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise<TaskModuleTaskInfo> {
protected async onTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise<TaskModuleResponse> {
throw new Error('NotImplemented');
}

Expand All @@ -210,7 +210,7 @@ export class TeamsActivityHandler extends ActivityHandler {
* @param context
* @param taskModuleRequest
*/
protected async onTeamsTaskModuleSubmit(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise<TaskModuleResponseBase | void> {
protected async onTeamsTaskModuleSubmit(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise<TaskModuleResponse> {
throw new Error('NotImplemented');
}

Expand Down
54 changes: 35 additions & 19 deletions libraries/botbuilder/src/teamsActivityHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,51 @@
/**
* @module botbuilder
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import {
Activity,
ChannelInfo,
NotificationInfo,
TeamInfo,
TeamsChannelData
} from 'botbuilder-core';

/**
* Activity helper methods for Teams.
*/
export function teamsGetChannelId(activity: Activity): string {
if (!activity) {
throw new Error('Missing activity parameter');
}

export function teamsGetChannelId(activity : object) {
const channelData = ('channelData' in activity) ? activity['channelData'] : null;
const channel = (validObject(channelData) && 'channel' in channelData) ? channelData['channel'] : null;
return (validObject(channel) && 'id' in channel) ? channel['id'] : null;
const channelData: TeamsChannelData = activity.channelData as TeamsChannelData;
const channel: ChannelInfo = channelData ? channelData.channel : null;
return channel && channel.id ? channel.id : null;
}

export function teamsGetTeamId(activity : object) {
const channelData = ('channelData' in activity) ? activity['channelData'] : null;
const team = (validObject(channelData) && 'team' in channelData) ? channelData['team'] : null;
return (validObject(team) && 'id' in team) ? team['id'] : null;
export function teamsGetTeamId(activity: Activity): string {
if (!activity) {
throw new Error('Missing activity parameter');
}

const channelData: TeamsChannelData = activity.channelData as TeamsChannelData;
const team: TeamInfo = channelData ? channelData.team : null;
return team && team.id ? team.id : null;
}

export function teamsNotifyUser(activity : object) {
const channelData = (validObject(activity) && 'channelData' in activity) ? activity['channelData'] : { };
channelData['Notification'] = { Alert: true };
activity['channelData'] = channelData;
}
export function teamsNotifyUser(activity: Activity): void {
if (!activity) {
throw new Error('Missing activity parameter');
}

function validObject(activity) {
// Check make sure not a string
if (activity == null || activity == undefined || activity instanceof String || typeof(activity) == 'string' ) {
return false;
if (!activity.channelData || typeof activity.channelData !== 'object') {
activity.channelData = {};
}
return true;
}

const channelData: TeamsChannelData = activity.channelData as TeamsChannelData;
channelData.notification = { alert: true } as NotificationInfo;
}
15 changes: 12 additions & 3 deletions libraries/botbuilder/src/teamsInfo.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* @module botbuilder
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
Expand All @@ -21,6 +24,7 @@ export class TeamsInfo {
if (!teamId) {
throw new Error('This method is only valid within the scope of a MS Teams Team.');
}

return await this.getTeamsConnectorClient(context).teams.fetchTeamDetails(teamId);
}

Expand All @@ -29,6 +33,7 @@ export class TeamsInfo {
if (!teamId) {
throw new Error('This method is only valid within the scope of a MS Teams Team.');
}

const channelList: ConversationList = await this.getTeamsConnectorClient(context).teams.fetchChannelList(teamId);
return channelList.conversations;
}
Expand All @@ -49,20 +54,24 @@ export class TeamsInfo {
if (!conversationId) {
throw new Error('The getMembers operation needs a valid conversationId.');
}

const teamMembers = await connectorClient.conversations.getConversationMembers(conversationId);
teamMembers.forEach((member:any) => {
member.aadObjectId = member.objectId;
});

return teamMembers as TeamsChannelAccount[];
}

private static getTeamId(context: TurnContext): string {
if (!context) {
throw new Error('Missing context parameter');
}

if (!context.activity) {
throw new Error('Missing activity on context');
}

const channelData = context.activity.channelData as TeamsChannelData;
const team = channelData && channelData.team ? channelData.team : undefined;
const teamId = team && typeof(team.id) === 'string' ? team.id : undefined;
Expand All @@ -71,14 +80,14 @@ export class TeamsInfo {

private static getConnectorClient(context: TurnContext): ConnectorClient {
if (!context.adapter || !('createConnectorClient' in context.adapter)) {
throw new Error('This method requires a connector client.')
throw new Error('This method requires a connector client.');
}

return (context.adapter as BotFrameworkAdapter).createConnectorClient(context.activity.serviceUrl);
}

private static getTeamsConnectorClient(context: TurnContext): TeamsConnectorClient {
const connectorClient = this.getConnectorClient(context);
return new TeamsConnectorClient(connectorClient.credentials, { baseUri: context.activity.serviceUrl });
}

}
}
26 changes: 19 additions & 7 deletions libraries/botbuilder/src/teamsTurnContextHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
/**
* @module botbuilder
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import {
Activity,
ChannelInfo,
ConversationParameters,
ConversationReference,
ConversationResourceResponse,
ResourceResponse,
TeamsChannelData,
TurnContext,
TurnContext
} from 'botbuilder-core';

import { teamsGetTeamId } from './teamsActivityHelpers';
import { BotFrameworkAdapter } from './botFrameworkAdapter';

/**
* Turn Context extension methods for Teams.
*/

export async function teamsCreateConversation(turnContext: TurnContext, teamsChannelId: string, message: Partial<Activity>): Promise<[ConversationReference, string]> {
export async function teamsCreateConversation(context: TurnContext, teamsChannelId: string, message: Partial<Activity>): Promise<[ConversationReference, string]> {
if (!teamsChannelId) {
throw new Error('Missing valid teamsChannelId argument');
}
Expand All @@ -33,14 +37,22 @@ export async function teamsCreateConversation(turnContext: TurnContext, teamsCha
id: teamsChannelId
}
},
activity: <Activity>message,
activity: message,
};
const adapter = <BotFrameworkAdapter>turnContext.adapter;
const connectorClient = adapter.createConnectorClient(turnContext.activity.serviceUrl);
const adapter = <BotFrameworkAdapter>context.adapter;
const connectorClient = adapter.createConnectorClient(context.activity.serviceUrl);
// This call does NOT send the outbound Activity is not being sent through the middleware stack.
const conversationResourceResponse: ConversationResourceResponse = await connectorClient.conversations.createConversation(conversationParameters);
const conversationReference = <ConversationReference>TurnContext.getConversationReference(turnContext.activity);
const conversationReference = <ConversationReference>TurnContext.getConversationReference(context.activity);
conversationReference.conversation.id = conversationResourceResponse.id;
return [conversationReference, conversationResourceResponse.activityId];
}

export async function teamsSendToGeneralChannel(context: TurnContext, message: Partial<Activity>): Promise<[ConversationReference, string]> {
const teamId = teamsGetTeamId(context.activity);
if (!teamId) {
throw new Error('The current Activity was not sent from a Teams Team.');
}

return teamsCreateConversation(context, teamId, message);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import {
CardFactory,
InvokeResponse,
MessageFactory,
TaskModuleContinueResponse,
TaskModuleMessageResponse,
TaskModuleRequest,
TaskModuleResponseBase,
TaskModuleResponse,
TaskModuleTaskInfo,
TeamsActivityHandler,
TurnContext
TurnContext,
} from 'botbuilder';

/**
Expand Down Expand Up @@ -66,7 +67,7 @@ export class AdaptiveCardsBot extends TeamsActivityHandler {
});
}

protected async onTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise<TaskModuleTaskInfo> {
protected async onTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise<TaskModuleResponse> {
await context.sendActivity(MessageFactory.text(`OnTeamsTaskModuleFetchAsync TaskModuleRequest: ${JSON.stringify(taskModuleRequest)}`));

/**
Expand Down Expand Up @@ -96,17 +97,28 @@ export class AdaptiveCardsBot extends TeamsActivityHandler {
"version": "1.0"
});
/* tslint:enable:quotemark object-literal-key-quotes */
return {
card,
height: 200,
title: 'Task Module Example',
width: 400
} as TaskModuleTaskInfo;
return {
task: {
type: 'continue',
value: {
card,
height: 200,
title: 'Task Module Example',
width: 400
} as TaskModuleTaskInfo
} as TaskModuleContinueResponse
} as TaskModuleResponse;
}

protected async onTeamsTaskModuleSubmit(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise<TaskModuleResponseBase> {
protected async onTeamsTaskModuleSubmit(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise<TaskModuleResponse> {
await context.sendActivity(MessageFactory.text(`OnTeamsTaskModuleSubmit value: ${JSON.stringify(taskModuleRequest)}`));
return { type: 'message', value: 'Thanks!' } as TaskModuleMessageResponse;

return {
task: {
type: 'message',
value: 'Thanks!'
} as TaskModuleMessageResponse
} as TaskModuleResponse;
}

protected async onTeamsCardActionInvoke(context: TurnContext): Promise<InvokeResponse> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
CardFactory,
MessagingExtensionActionResponse,
MessagingExtensionAction,
MessagingExtensionQuery,
TaskModuleContinueResponse,
TaskModuleResponse,
TaskModuleTaskInfo,
TaskModuleRequest,
TeamsActivityHandler,
Expand Down Expand Up @@ -107,13 +107,23 @@ export class MessagingExtensionAuthBot extends TeamsActivityHandler {
return response;
}

protected async onTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise<TaskModuleTaskInfo> {
protected async onTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise<TaskModuleResponse> {
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);

const continueResponse : TaskModuleContinueResponse = {
type: 'continue',
value: this.CreateSignedInTaskModuleTaskInfo(tokenResponse.token),
};

const response : MessagingExtensionActionResponse = {
task: continueResponse
};

return response;
}
else
{
Expand Down
Loading