UNPKG

@microsoft/botbuilder-m365

Version:

M365 extensions for Microsoft BotBuilder, Alpha release.

469 lines (435 loc) 20.6 kB
/** * @module botbuilder-m365 */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { TurnContext, TaskModuleTaskInfo, ActivityTypes, InvokeResponse, INVOKE_RESPONSE_KEY, TaskModuleResponse, MessagingExtensionResult, MessagingExtensionActionResponse, MessagingExtensionParameter, MessagingExtensionQuery, Activity } from 'botbuilder'; import { Application, RouteSelector, Query } from './Application'; import { TurnState } from './TurnState'; const ANONYMOUS_QUERY_LINK_INVOKE_NAME = `composeExtension/anonymousQueryLink`; const FETCH_TASK_INVOKE_NAME = `composeExtension/fetchTask`; const QUERY_INVOKE_NAME = `composeExtension/query`; const QUERY_LINK_INVOKE_NAME = `composeExtension/queryLink`; const SELECT_ITEM_INVOKE_NAME = `composeExtension/selectItem`; const SUBMIT_ACTION_INVOKE_NAME = `composeExtension/submitAction`; export class MessageExtensions<TState extends TurnState> { private readonly _app: Application<TState>; public constructor(app: Application<TState>) { this._app = app; } public anonymousQueryLink( commandId: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[], handler: (context: TurnContext, state: TState) => Promise<MessagingExtensionResult> ): Application<TState> { (Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => { const selector = createTaskSelector(cid, ANONYMOUS_QUERY_LINK_INVOKE_NAME); this._app.addRoute( selector, async (context, state) => { // Insure that we're in an invoke as expected if ( context?.activity?.type !== ActivityTypes.Invoke || context?.activity?.name !== ANONYMOUS_QUERY_LINK_INVOKE_NAME ) { throw new Error( `Unexpected MessageExtensions.anonymousQueryLink() triggered for activity type: ${context?.activity?.type}` ); } // Call handler and then check to see if an invoke response has already been added const result = await handler(context, state); if (!context.turnState.get(INVOKE_RESPONSE_KEY)) { // Format invoke response const response: MessagingExtensionActionResponse = { composeExtension: result }; // Queue up invoke response await context.sendActivity({ value: { body: response, status: 200 } as InvokeResponse, type: ActivityTypes.InvokeResponse }); } }, true ); }); return this._app; } public botMessagePreviewEdit( commandId: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[], handler: ( context: TurnContext, state: TState, previewActivity: Partial<Activity> ) => Promise<MessagingExtensionResult | TaskModuleTaskInfo | string | null | undefined> ): Application<TState> { (Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => { const selector = createTaskSelector(cid, SUBMIT_ACTION_INVOKE_NAME, 'edit'); this._app.addRoute( selector, async (context, state) => { // Insure that we're in an invoke as expected if ( context?.activity?.type !== ActivityTypes.Invoke || context?.activity?.name !== SUBMIT_ACTION_INVOKE_NAME || context?.activity?.value?.botMessagePreviewAction !== 'edit' ) { throw new Error( `Unexpected MessageExtensions.botMessagePreviewEdit() triggered for activity type: ${context?.activity?.type}` ); } // Call handler and then check to see if an invoke response has already been added const result = await handler(context, state, context.activity.value?.botActivityPreview[0] ?? {}); await this.returnSubmitActionResponse(context, result); }, true ); }); return this._app; } public botMessagePreviewSend( commandId: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[], handler: (context: TurnContext, state: TState, previewActivity: Partial<Activity>) => Promise<void> ): Application<TState> { (Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => { const selector = createTaskSelector(cid, SUBMIT_ACTION_INVOKE_NAME, 'send'); this._app.addRoute( selector, async (context, state) => { // Insure that we're in an invoke as expected if ( context?.activity?.type !== ActivityTypes.Invoke || context?.activity?.name !== SUBMIT_ACTION_INVOKE_NAME || context?.activity?.value?.botMessagePreviewAction !== 'send' ) { throw new Error( `Unexpected MessageExtensions.botMessagePreviewSend() triggered for activity type: ${context?.activity?.type}` ); } // Call handler and then check to see if an invoke response has already been added await handler(context, state, context.activity.value?.botActivityPreview[0] ?? {}); // Queue up invoke response if (!context.turnState.get(INVOKE_RESPONSE_KEY)) { await context.sendActivity({ value: { body: {}, status: 200 } as InvokeResponse, type: ActivityTypes.InvokeResponse }); } }, true ); }); return this._app; } public fetchTask( commandId: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[], handler: (context: TurnContext, state: TState) => Promise<TaskModuleTaskInfo | string> ): Application<TState> { (Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => { const selector = createTaskSelector(cid, FETCH_TASK_INVOKE_NAME); this._app.addRoute( selector, async (context, state) => { // Insure that we're in an invoke as expected if ( context?.activity?.type !== ActivityTypes.Invoke || context?.activity?.name !== FETCH_TASK_INVOKE_NAME ) { throw new Error( `Unexpected MessageExtensions.fetchTask() triggered for activity type: ${context?.activity?.type}` ); } // Call handler and then check to see if an invoke response has already been added const result = await handler(context, state); if (!context.turnState.get(INVOKE_RESPONSE_KEY)) { // Format invoke response let response: TaskModuleResponse; if (typeof result == 'string') { // Return message response = { task: { type: 'message', value: result } }; } else { // Return card response = { task: { type: 'continue', value: result } }; } // Queue up invoke response await context.sendActivity({ value: { body: response, status: 200 } as InvokeResponse, type: ActivityTypes.InvokeResponse }); } }, true ); }); return this._app; } public query<TParams extends Record<string, any> = Record<string, any>>( commandId: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[], handler: (context: TurnContext, state: TState, query: Query<TParams>) => Promise<MessagingExtensionResult> ): Application<TState> { (Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => { const selector = createTaskSelector(cid, QUERY_INVOKE_NAME); this._app.addRoute( selector, async (context, state) => { // Insure that we're in an invoke as expected if ( context?.activity?.type !== ActivityTypes.Invoke || context?.activity?.name !== QUERY_INVOKE_NAME ) { throw new Error( `Unexpected MessageExtensions.query() triggered for activity type: ${context?.activity?.type}` ); } // Flatten query options const meQuery: MessagingExtensionQuery = context?.activity?.value ?? {}; const query: Query<TParams> = { count: meQuery?.queryOptions?.count ?? 25, skip: meQuery?.queryOptions?.skip ?? 0, parameters: {} as TParams }; // Flatten query parameters (meQuery.parameters ?? []).forEach((param: MessagingExtensionParameter) => { if (param.name) { (query.parameters as any)[param.name] = param.value; } }); // Call handler and then check to see if an invoke response has already been added const result = await handler(context, state, query); if (!context.turnState.get(INVOKE_RESPONSE_KEY)) { // Format invoke response const response: MessagingExtensionActionResponse = { composeExtension: result }; // Queue up invoke response await context.sendActivity({ value: { body: response, status: 200 } as InvokeResponse, type: ActivityTypes.InvokeResponse }); } }, true ); }); return this._app; } public queryLink( commandId: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[], handler: (context: TurnContext, state: TState) => Promise<MessagingExtensionResult> ): Application<TState> { (Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => { const selector = createTaskSelector(cid, QUERY_LINK_INVOKE_NAME); this._app.addRoute( selector, async (context, state) => { // Insure that we're in an invoke as expected if ( context?.activity?.type !== ActivityTypes.Invoke || context?.activity?.name !== QUERY_LINK_INVOKE_NAME ) { throw new Error( `Unexpected MessageExtensions.queryLink() triggered for activity type: ${context?.activity?.type}` ); } // Call handler and then check to see if an invoke response has already been added const result = await handler(context, state); if (!context.turnState.get(INVOKE_RESPONSE_KEY)) { // Format invoke response const response: MessagingExtensionActionResponse = { composeExtension: result }; // Queue up invoke response await context.sendActivity({ value: { body: response, status: 200 } as InvokeResponse, type: ActivityTypes.InvokeResponse }); } }, true ); }); return this._app; } public selectItem( handler: (context: TurnContext, state: TState, item: Record<string, any>) => Promise<MessagingExtensionResult> ): Application<TState> { // Define static route selector const selector = (context: TurnContext) => Promise.resolve( context?.activity?.type == ActivityTypes.Invoke && context?.activity.name === SELECT_ITEM_INVOKE_NAME ); // Add route this._app.addRoute( selector, async (context, state) => { // Call handler and then check to see if an invoke response has already been added const result = await handler(context, state, context?.activity?.value ?? {}); if (!context.turnState.get(INVOKE_RESPONSE_KEY)) { // Format invoke response const response: MessagingExtensionActionResponse = { composeExtension: result }; // Queue up invoke response await context.sendActivity({ value: { body: response, status: 200 } as InvokeResponse, type: ActivityTypes.InvokeResponse }); } }, true ); return this._app; } public submitAction<TData>( commandId: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[], handler: ( context: TurnContext, state: TState, data: TData ) => Promise<MessagingExtensionResult | TaskModuleTaskInfo | string | null | undefined> ): Application<TState> { (Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => { const selector = createTaskSelector(cid, SUBMIT_ACTION_INVOKE_NAME); this._app.addRoute( selector, async (context, state) => { // Insure that we're in an invoke as expected if ( context?.activity?.type !== ActivityTypes.Invoke || context?.activity?.name !== SUBMIT_ACTION_INVOKE_NAME ) { throw new Error( `Unexpected MessageExtensions.submitAction() triggered for activity type: ${context?.activity?.type}` ); } // Call handler and then check to see if an invoke response has already been added const result = await handler(context, state, context.activity.value?.data ?? {}); await this.returnSubmitActionResponse(context, result); }, true ); }); return this._app; } private async returnSubmitActionResponse( context: TurnContext, result: MessagingExtensionResult | TaskModuleTaskInfo | string | null | undefined ): Promise<void> { if (!context.turnState.get(INVOKE_RESPONSE_KEY)) { // Format invoke response let response: MessagingExtensionActionResponse; if (typeof result == 'string') { // Return message response = { task: { type: 'message', value: result } }; } else if (typeof result == 'object') { if ((result as TaskModuleTaskInfo).card) { // Return another task module response = { task: { type: 'continue', value: result as TaskModuleTaskInfo } }; } else { // Return card to user response = { composeExtension: result as MessagingExtensionResult }; } } else { // No action taken response = { composeExtension: undefined }; } // Queue up invoke response await context.sendActivity({ value: { body: response, status: 200 } as InvokeResponse, type: ActivityTypes.InvokeResponse }); } } } /** * * @param {string | RegExp | RouteSelector[]} commandId Name of the commandId * @param {boolean} invokeName Whether or not the commandId a Teams invokable action * @param {string} botMessagePreviewAction Message Extension preview action 'edit' or 'send' * @returns {RouteSelector} Route selector function */ function createTaskSelector( commandId: string | RegExp | RouteSelector, invokeName: string, botMessagePreviewAction?: 'edit' | 'send' ): RouteSelector { if (typeof commandId == 'function') { // Return the passed in selector function return commandId; } else if (commandId instanceof RegExp) { // Return a function that matches the commandId using a RegExp return (context: TurnContext) => { const isInvoke = context?.activity?.type == ActivityTypes.Invoke && context?.activity?.name == invokeName; if ( isInvoke && typeof context?.activity?.value?.commandId == 'string' && matchesPreviewAction(context.activity, botMessagePreviewAction) ) { return Promise.resolve(commandId.test(context.activity.value.commandId)); } else { return Promise.resolve(false); } }; } else { // Return a function that attempts to match commandId return (context: TurnContext) => { const isInvoke = context?.activity?.type == ActivityTypes.Invoke && context?.activity?.name == invokeName; return Promise.resolve( isInvoke && context?.activity?.value?.commandId === commandId && matchesPreviewAction(context.activity, botMessagePreviewAction) ); }; } } /** * Checks if the activity is a bot message preview action. * * @param {Activity} activity The activity / communication type that is being checked. * @param {string} botMessagePreviewAction Name of the preview action * @returns {boolean} True if the activity is a bot message preview action, false if it is not. */ function matchesPreviewAction(activity: Activity, botMessagePreviewAction?: 'edit' | 'send'): boolean { if (typeof activity?.value?.botMessagePreviewAction == 'string') { return activity.value.botMessagePreviewAction == botMessagePreviewAction; } else { return botMessagePreviewAction == undefined; } }