From bd25f7e331a471be4efbb0616c91fd020cd453a4 Mon Sep 17 00:00:00 2001 From: Corina <14900841+corinagum@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:08:58 -0800 Subject: [PATCH] [JS] feat: #243, #245 Task Module config fetch/submit, file consent, and actionable message invokes (#821) ## Linked issues closes: #243, #245 ## Details - Add handling for 'config/fetch' and 'config/submit' - Add handling for 'actionableMessage/executeAction` - Add handling for 'fileConsent/invoke' --- - Rename & refactor usage of `MessagingExtensionInvokeNames` to `MessageExtensionInvokeNames` - Have unit test coverage ignore export files ## Attestation Checklist # Unit Tests TBD in later PR - [x] My code follows the style guidelines of this project - I have checked for/fixed spelling, linting, and other errors - I have commented my code for clarity - I have made corresponding changes to the documentation (we use [TypeDoc](https://typedoc.org/) to document our code) - My changes generate no new warnings - I have added tests that validates my changes, and provides sufficient test coverage. I have tested with: - Local testing - E2E testing in Teams - New and existing unit tests pass locally with my changes --------- Co-authored-by: Corina Gum <> --- js/.nycrc | 9 +- js/packages/teams-ai/src/Application.spec.ts | 17 +++ js/packages/teams-ai/src/Application.ts | 77 +++++++++- .../teams-ai/src/MessageExtensions.spec.ts | 74 ++++++---- js/packages/teams-ai/src/MessageExtensions.ts | 47 ++++--- js/packages/teams-ai/src/TaskModules.ts | 133 ++++++++++++++++-- 6 files changed, 290 insertions(+), 67 deletions(-) diff --git a/js/.nycrc b/js/.nycrc index d8dfacc00..fb53ca077 100644 --- a/js/.nycrc +++ b/js/.nycrc @@ -1,7 +1,14 @@ { "extension": [".ts"], "include": ["packages/**/src/**/*.ts", "lib/**/*.js"], - "exclude": ["**/node_modules/**", "**/tests/**", "**/coverage/**", "**/*.d.ts", "**/*.spec.ts"], + "exclude": [ + "**/node_modules/**", + "**/tests/**", + "**/coverage/**", + "**/*.d.ts", + "**/*.spec.ts", + "packages/**/src/index.ts" + ], "reporter": ["html", "text"], "all": true, "cache": true diff --git a/js/packages/teams-ai/src/Application.spec.ts b/js/packages/teams-ai/src/Application.spec.ts index 1e13ddce6..e6a53d9d1 100644 --- a/js/packages/teams-ai/src/Application.spec.ts +++ b/js/packages/teams-ai/src/Application.spec.ts @@ -371,6 +371,16 @@ describe('Application', () => { }); }); } + + it(`should throw an error when event is not known case`, () => { + assert.throws( + () => + mockApp.messageEventUpdate('test' as any, async (_context, _state) => { + assert.fail('should not be called'); + }), + new Error(`Invalid TeamsMessageEvent type: test`) + ); + }); }); describe('messageUpdate', () => { @@ -429,5 +439,12 @@ describe('Application', () => { }); }); } + + it('should throw an error when handler is not a function', () => { + assert.throws( + () => mockApp.messageEventUpdate('editMessage', 1 as any), + new Error(`MessageUpdate 'handler' for editMessage is number. Type of 'handler' must be a function.`) + ); + }); }); }); diff --git a/js/packages/teams-ai/src/Application.ts b/js/packages/teams-ai/src/Application.ts index 375dd494a..5f6495fa0 100644 --- a/js/packages/teams-ai/src/Application.ts +++ b/js/packages/teams-ai/src/Application.ts @@ -12,6 +12,8 @@ import { ActivityTypes, BotAdapter, ConversationReference, + FileConsentCardResponse, + O365ConnectorCardActionQuery, ResourceResponse, Storage, TurnContext @@ -516,6 +518,71 @@ export class Application { return this; } + /** + * Registers a handler to process when a file consent card is accepted by the user. + * @param {(context: TurnContext, state: TState, fileConsentResponse: FileConsentCardResponse) => Promise} handler Function to call when the route is triggered. + * @returns {this} The application instance for chaining purposes. + */ + public fileConsentAccept( + handler: (context: TurnContext, state: TState, fileConsentResponse: FileConsentCardResponse) => Promise + ): this { + const selector = (context: TurnContext): Promise => { + return Promise.resolve( + context.activity.type === ActivityTypes.Invoke && + context.activity.name === 'fileConsent/invoke' && + context.activity.value?.action === 'accept' + ); + }; + const handlerWrapper = (context: TurnContext, state: TState) => { + return handler(context, state, context.activity.value as FileConsentCardResponse); + }; + this.addRoute(selector, handlerWrapper); + return this; + } + + /** + * Registers a handler to process when a file consent card is declined by the user. + * @param {(context: TurnContext, state: TState, fileConsentResponse: FileConsentCardResponse) => Promise} handler Function to call when the route is triggered. + * @returns {this} The application instance for chaining purposes. + */ + public fileConsentDecline( + handler: (context: TurnContext, state: TState, fileConsentResponse: FileConsentCardResponse) => Promise + ): this { + const selector = (context: TurnContext): Promise => { + return Promise.resolve( + context.activity.type === ActivityTypes.Invoke && + context.activity.name === 'fileConsent/invoke' && + context.activity.value?.action === 'decline' + ); + }; + const handlerWrapper = (context: TurnContext, state: TState) => { + return handler(context, state, context.activity.value as FileConsentCardResponse); + }; + this.addRoute(selector, handlerWrapper); + return this; + } + + /** + * Registers a handler to process when a O365 Connector Card Action activity is received from the user. + * @param {(context: TurnContext, state: TState, query: O365ConnectorCardActionQuery) => Promise} handler Function to call when the route is triggered. + * @returns {this} The application instance for chaining purposes. + */ + public O365ConnectorCardAction( + handler: (context: TurnContext, state: TState, query: O365ConnectorCardActionQuery) => Promise + ): this { + const selector = (context: TurnContext): Promise => { + return Promise.resolve( + context.activity.type === ActivityTypes.Invoke && + context.activity.name === 'actionableMessage/executeAction' + ); + }; + const handlerWrapper = (context: TurnContext, state: TState) => { + return handler(context, state, context.activity.value as O365ConnectorCardActionQuery); + }; + this.addRoute(selector, handlerWrapper); + return this; + } + /** * Dispatches an incoming activity to a handler registered with the application. * @remarks @@ -1045,9 +1112,9 @@ function createConversationUpdateSelector(event: ConversationUpdateEvents): Rout return (context: TurnContext) => { return Promise.resolve( context?.activity?.type == ActivityTypes.ConversationUpdate && - context?.activity?.channelData?.eventType == event && - context?.activity?.channelData?.channel && - context.activity.channelData?.team + context?.activity?.channelData?.eventType == event && + context?.activity?.channelData?.channel && + context.activity.channelData?.team ); }; case 'membersAdded': @@ -1198,8 +1265,8 @@ function createMessageReactionSelector(event: MessageReactionEvents): RouteSelec return (context: TurnContext) => { return Promise.resolve( context?.activity?.type == ActivityTypes.MessageReaction && - Array.isArray(context?.activity?.reactionsRemoved) && - context.activity.reactionsRemoved.length > 0 + Array.isArray(context?.activity?.reactionsRemoved) && + context.activity.reactionsRemoved.length > 0 ); }; } diff --git a/js/packages/teams-ai/src/MessageExtensions.spec.ts b/js/packages/teams-ai/src/MessageExtensions.spec.ts index 78f29f941..40988396a 100644 --- a/js/packages/teams-ai/src/MessageExtensions.spec.ts +++ b/js/packages/teams-ai/src/MessageExtensions.spec.ts @@ -11,6 +11,18 @@ import { TurnContext } from 'botbuilder'; +const { + ANONYMOUS_QUERY_LINK_INVOKE, + FETCH_TASK_INVOKE, + QUERY_INVOKE, + QUERY_LINK_INVOKE, + SELECT_ITEM_INVOKE, + SUBMIT_ACTION_INVOKE, + QUERY_SETTING_URL, + CONFIGURE_SETTINGS, + QUERY_CARD_BUTTON_CLICKED +} = MessageExtensionsInvokeNames; + describe('MessageExtensions', () => { const adapter = new TestAdapter(); let mockApp: Application; @@ -22,9 +34,9 @@ describe('MessageExtensions', () => { assert.equal(mockApp.messageExtensions instanceof MessageExtensions, true); }); - describe(`${MessageExtensionsInvokeNames.ANONYMOUS_QUERY_LINK_INVOKE}`, () => { + describe(`${ANONYMOUS_QUERY_LINK_INVOKE}`, () => { it('should return InvokeResponse with status code 200 with an unfurled link in the response', async () => { - const activity = createTestInvoke(MessageExtensionsInvokeNames.ANONYMOUS_QUERY_LINK_INVOKE, { + const activity = createTestInvoke(ANONYMOUS_QUERY_LINK_INVOKE, { url: 'https://www.youtube.com/watch?v=971YIvosuUk&ab_channel=MicrosoftDeveloper' }); activity.channelId = Channels.Msteams; @@ -82,9 +94,9 @@ describe('MessageExtensions', () => { }); }); - describe(`${MessageExtensionsInvokeNames.CONFIGURE_SETTINGS}`, () => { + describe(`${CONFIGURE_SETTINGS}`, () => { it('should return InvokeResponse with status code 200 with the configure setting invoke name', async () => { - const activity = createTestInvoke(MessageExtensionsInvokeNames.CONFIGURE_SETTINGS, { theme: 'dark' }); + const activity = createTestInvoke(CONFIGURE_SETTINGS, { theme: 'dark' }); activity.channelId = Channels.Msteams; mockApp.messageExtensions.configureSettings(async (context: TurnContext, _state, value) => { @@ -100,9 +112,9 @@ describe('MessageExtensions', () => { }); }); - describe(`${MessageExtensionsInvokeNames.FETCH_TASK_INVOKE}`, () => { + describe(`${FETCH_TASK_INVOKE}`, () => { it('should return InvokeResponse with status code 200 with the task invoke card', async () => { - const activity = createTestInvoke(MessageExtensionsInvokeNames.FETCH_TASK_INVOKE, { + const activity = createTestInvoke(FETCH_TASK_INVOKE, { commandId: 'showTaskModule' }); activity.channelId = Channels.Msteams; @@ -157,7 +169,7 @@ describe('MessageExtensions', () => { }); it('should return InvokeResponse with status code 200 with a string message', async () => { - const activity = createTestInvoke(MessageExtensionsInvokeNames.FETCH_TASK_INVOKE, { + const activity = createTestInvoke(FETCH_TASK_INVOKE, { commandId: 'showMessage' }); activity.channelId = Channels.Msteams; @@ -183,22 +195,22 @@ describe('MessageExtensions', () => { it('should call the same handler among an array of commandIds', async () => { // commandId: ['showTaskModule', 'show', 'show task module'] - const activity = createTestInvoke(MessageExtensionsInvokeNames.FETCH_TASK_INVOKE, { + const activity = createTestInvoke(FETCH_TASK_INVOKE, { commandId: 'showTaskModule' }); activity.channelId = Channels.Msteams; const regexp = new RegExp(/show$/, 'i'); - const activity2 = createTestInvoke(MessageExtensionsInvokeNames.FETCH_TASK_INVOKE, { + const activity2 = createTestInvoke(FETCH_TASK_INVOKE, { commandId: 'Show' }); activity2.channelId = Channels.Msteams; - const activity3 = createTestInvoke(MessageExtensionsInvokeNames.FETCH_TASK_INVOKE, { + const activity3 = createTestInvoke(FETCH_TASK_INVOKE, { commandId: 'show task module' }); activity3.channelId = Channels.Msteams; - const activity4 = createTestInvoke(MessageExtensionsInvokeNames.FETCH_TASK_INVOKE, { + const activity4 = createTestInvoke(FETCH_TASK_INVOKE, { commandId: 'show task' }); activity4.channelId = Channels.Msteams; @@ -245,7 +257,7 @@ describe('MessageExtensions', () => { it('should throw an error when the routeSelector routes incorrectly', async () => { // Incorrect invoke - const activity = createTestInvoke(MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE, { + const activity = createTestInvoke(SUBMIT_ACTION_INVOKE, { commandId: 'Create', botActivityPreview: [1], botMessagePreviewAction: 'edit' @@ -253,7 +265,7 @@ describe('MessageExtensions', () => { mockApp.messageExtensions.fetchTask( async (context) => { - return context.activity.name === MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE; + return context.activity.name === SUBMIT_ACTION_INVOKE; }, async (context: TurnContext, _state) => { assert.fail('should not have reached this point'); @@ -268,9 +280,9 @@ describe('MessageExtensions', () => { }); }); - describe(`${MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE}`, () => { + describe(`${SUBMIT_ACTION_INVOKE}`, () => { it('should return InvokeResponse with status code 200 with the submit action invoke name for submitAction', async () => { - const activity = createTestInvoke(MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE, { + const activity = createTestInvoke(SUBMIT_ACTION_INVOKE, { commandId: 'giveKudos', commandContext: 'compose', context: { @@ -342,7 +354,7 @@ describe('MessageExtensions', () => { }); it('should return InvokeResponse with status code 200 with the submit action invoke name for botMessagePreviewSend', async () => { - const activity = createTestInvoke(MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE, { + const activity = createTestInvoke(SUBMIT_ACTION_INVOKE, { commandId: 'Create Preview', botActivityPreview: [1], botMessagePreviewAction: 'send' @@ -365,7 +377,7 @@ describe('MessageExtensions', () => { }); it('should return InvokeResponse with status code 200 with the submit action invoke name for botMessagePreviewEdit', async () => { - const activity = createTestInvoke(MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE, { + const activity = createTestInvoke(SUBMIT_ACTION_INVOKE, { commandId: 'Create Preview', botActivityPreview: [1], botMessagePreviewAction: 'edit' @@ -390,14 +402,14 @@ describe('MessageExtensions', () => { it('should call the same handler among an array of commandIds for botMessagePreviewSend', async () => { // commandId: ['create preview', 'preview'] - const activity = createTestInvoke(MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE, { + const activity = createTestInvoke(SUBMIT_ACTION_INVOKE, { commandId: 'create preview', botActivityPreview: ['create preview'], botMessagePreviewAction: 'send' }); activity.channelId = Channels.Msteams; - const activity2 = createTestInvoke(MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE, { + const activity2 = createTestInvoke(SUBMIT_ACTION_INVOKE, { commandId: 'preview', botActivityPreview: ['preview'], botMessagePreviewAction: 'send' @@ -426,7 +438,7 @@ describe('MessageExtensions', () => { it('should throw an error when the routeSelector routes incorrectly for botMessagePreviewSend', async () => { // Incorrect invoke - const activity = createTestInvoke(MessageExtensionsInvokeNames.FETCH_TASK_INVOKE, { + const activity = createTestInvoke(FETCH_TASK_INVOKE, { commandId: 'Create', botActivityPreview: [1], botMessagePreviewAction: 'edit' @@ -435,7 +447,7 @@ describe('MessageExtensions', () => { mockApp.messageExtensions.botMessagePreviewSend( async (context) => { - return context.activity.name === MessageExtensionsInvokeNames.FETCH_TASK_INVOKE; + return context.activity.name === FETCH_TASK_INVOKE; }, async (context: TurnContext, _state, previewActivity) => { assert.fail('should not have reached this point'); @@ -452,9 +464,9 @@ describe('MessageExtensions', () => { }); }); - describe(`${MessageExtensionsInvokeNames.QUERY_INVOKE}`, () => { + describe(`${QUERY_INVOKE}`, () => { it('should return InvokeResponse with status code 200 with the query invoke name', async () => { - const activity = createTestInvoke(MessageExtensionsInvokeNames.QUERY_INVOKE, { commandId: 'showQuery' }); + const activity = createTestInvoke(QUERY_INVOKE, { commandId: 'showQuery' }); activity.channelId = Channels.Msteams; interface MyParams {} @@ -485,9 +497,9 @@ describe('MessageExtensions', () => { }); }); - describe(`${MessageExtensionsInvokeNames.QUERY_CARD_BUTTON_CLICKED}`, () => { + describe(`${QUERY_CARD_BUTTON_CLICKED}`, () => { it('should return InvokeResponse with status code 200 with the query card button clicked invoke name', async () => { - const activity = createTestInvoke(MessageExtensionsInvokeNames.QUERY_CARD_BUTTON_CLICKED, { + const activity = createTestInvoke(QUERY_CARD_BUTTON_CLICKED, { title: 'Query button', displayText: 'Yes', value: 'Yes' @@ -509,9 +521,9 @@ describe('MessageExtensions', () => { }); }); - describe(`${MessageExtensionsInvokeNames.QUERY_LINK_INVOKE}`, () => { + describe(`${QUERY_LINK_INVOKE}`, () => { it('should return InvokeResponse with status code 200 with an unfurled link in the response', async () => { - const activity = createTestInvoke(MessageExtensionsInvokeNames.QUERY_LINK_INVOKE, { + const activity = createTestInvoke(QUERY_LINK_INVOKE, { url: 'https://www.youtube.com/watch?v=971YIvosuUk&ab_channel=MicrosoftDeveloper' }); activity.channelId = Channels.Msteams; @@ -570,9 +582,9 @@ describe('MessageExtensions', () => { }); }); - describe(`${MessageExtensionsInvokeNames.QUERY_SETTING_URL}`, async () => { + describe(`${QUERY_SETTING_URL}`, async () => { it('should return InvokeResponse with status code 200 when querySettingUrl is invoked', async () => { - const activity = createTestInvoke(MessageExtensionsInvokeNames.QUERY_SETTING_URL, {}); + const activity = createTestInvoke(QUERY_SETTING_URL, {}); activity.channelId = Channels.Msteams; mockApp.messageExtensions.queryUrlSetting(async (context: TurnContext, _state) => { @@ -590,9 +602,9 @@ describe('MessageExtensions', () => { }); }); - describe(`${MessageExtensionsInvokeNames.SELECT_ITEM_INVOKE}`, () => { + describe(`${SELECT_ITEM_INVOKE}`, () => { it('should return InvokeResponse with status code 200 with selected item in the response', async () => { - const activity = createTestInvoke(MessageExtensionsInvokeNames.SELECT_ITEM_INVOKE, { + const activity = createTestInvoke(SELECT_ITEM_INVOKE, { attachmentLayout: 'list', attachments: [ { diff --git a/js/packages/teams-ai/src/MessageExtensions.ts b/js/packages/teams-ai/src/MessageExtensions.ts index c6a150261..35a98fa39 100644 --- a/js/packages/teams-ai/src/MessageExtensions.ts +++ b/js/packages/teams-ai/src/MessageExtensions.ts @@ -102,10 +102,11 @@ export class MessageExtensions { public anonymousQueryLink( handler: (context: TurnContext, state: TState, url: string) => Promise ): Application { + const { ANONYMOUS_QUERY_LINK_INVOKE } = MessageExtensionsInvokeNames; const selector = (context: TurnContext) => Promise.resolve( context?.activity?.type == ActivityTypes.Invoke && - context?.activity.name === MessageExtensionsInvokeNames.ANONYMOUS_QUERY_LINK_INVOKE + context?.activity.name === ANONYMOUS_QUERY_LINK_INVOKE ); this._app.addRoute( selector, @@ -152,15 +153,16 @@ export class MessageExtensions { previewActivity: Partial ) => Promise ): Application { + const { SUBMIT_ACTION_INVOKE } = MessageExtensionsInvokeNames; (Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => { - const selector = createTaskSelector(cid, MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE, 'edit'); + const selector = createTaskSelector(cid, SUBMIT_ACTION_INVOKE, '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 !== MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE || + context?.activity?.name !== SUBMIT_ACTION_INVOKE || context?.activity?.value?.botMessagePreviewAction !== 'edit' ) { throw new Error( @@ -196,15 +198,15 @@ export class MessageExtensions { commandId: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[], handler: (context: TurnContext, state: TState, previewActivity: Partial) => Promise ): Application { + const { SUBMIT_ACTION_INVOKE } = MessageExtensionsInvokeNames; (Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => { - const selector = createTaskSelector(cid, MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE, 'send'); + const selector = createTaskSelector(cid, SUBMIT_ACTION_INVOKE, '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 !== MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE || + context?.activity?.type !== ActivityTypes.Invoke || context?.activity?.name !== SUBMIT_ACTION_INVOKE || context?.activity?.value?.botMessagePreviewAction !== 'send' ) { throw new Error( @@ -244,15 +246,16 @@ export class MessageExtensions { commandId: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[], handler: (context: TurnContext, state: TState) => Promise ): Application { + const { FETCH_TASK_INVOKE } = MessageExtensionsInvokeNames; (Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => { - const selector = createTaskSelector(cid, MessageExtensionsInvokeNames.FETCH_TASK_INVOKE); + const selector = createTaskSelector(cid, FETCH_TASK_INVOKE); 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 !== MessageExtensionsInvokeNames.FETCH_TASK_INVOKE + context?.activity?.name !== FETCH_TASK_INVOKE ) { throw new Error( `Unexpected MessageExtensions.fetchTask() triggered for activity type: ${context?.activity?.type}` @@ -312,16 +315,14 @@ export class MessageExtensions { commandId: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[], handler: (context: TurnContext, state: TState, query: Query) => Promise ): Application { + const { QUERY_INVOKE } = MessageExtensionsInvokeNames; (Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => { - const selector = createTaskSelector(cid, MessageExtensionsInvokeNames.QUERY_INVOKE); + const selector = createTaskSelector(cid, QUERY_INVOKE); 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 !== MessageExtensionsInvokeNames.QUERY_INVOKE - ) { + if (context?.activity?.type !== ActivityTypes.Invoke || context?.activity?.name !== QUERY_INVOKE) { throw new Error( `Unexpected MessageExtensions.query() triggered for activity type: ${context?.activity?.type}` ); @@ -374,9 +375,10 @@ export class MessageExtensions { public queryLink( handler: (context: TurnContext, state: TState, url: string) => Promise ): Application { + const { QUERY_LINK_INVOKE } = MessageExtensionsInvokeNames; const selector = (context: TurnContext) => Promise.resolve( - context?.activity?.type == ActivityTypes.Invoke && context?.activity.name === MessageExtensionsInvokeNames.QUERY_LINK_INVOKE + context?.activity?.type == ActivityTypes.Invoke && context?.activity.name === QUERY_LINK_INVOKE ); this._app.addRoute( @@ -421,10 +423,11 @@ export class MessageExtensions { public selectItem = Record>( handler: (context: TurnContext, state: TState, item: TItem) => Promise ): Application { + const { SELECT_ITEM_INVOKE } = MessageExtensionsInvokeNames; // Define static route selector const selector = (context: TurnContext) => Promise.resolve( - context?.activity?.type == ActivityTypes.Invoke && context?.activity.name === MessageExtensionsInvokeNames.SELECT_ITEM_INVOKE + context?.activity?.type == ActivityTypes.Invoke && context?.activity.name === SELECT_ITEM_INVOKE ); // Add route @@ -470,15 +473,16 @@ export class MessageExtensions { data: TData ) => Promise ): Application { + const { SUBMIT_ACTION_INVOKE } = MessageExtensionsInvokeNames; (Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => { - const selector = createTaskSelector(cid, MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE); + const selector = createTaskSelector(cid, SUBMIT_ACTION_INVOKE); 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 !== MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE + context?.activity?.name !== SUBMIT_ACTION_INVOKE ) { throw new Error( `Unexpected MessageExtensions.submitAction() triggered for activity type: ${context?.activity?.type}` @@ -558,10 +562,11 @@ export class MessageExtensions { public queryUrlSetting( handler: (context: TurnContext, state: TState) => Promise ): Application { + const { QUERY_SETTING_URL } = MessageExtensionsInvokeNames; // Define static route selector const selector = (context: TurnContext) => Promise.resolve( - context?.activity?.type == ActivityTypes.Invoke && context?.activity.name === MessageExtensionsInvokeNames.QUERY_SETTING_URL + context?.activity?.type == ActivityTypes.Invoke && context?.activity.name === QUERY_SETTING_URL ); // Add route @@ -600,10 +605,11 @@ export class MessageExtensions { public configureSettings>( handler: (context: TurnContext, state: TState, settings: TData) => Promise ): Application { + const { CONFIGURE_SETTINGS } = MessageExtensionsInvokeNames; // Define static route selector const selector = (context: TurnContext) => Promise.resolve( - context?.activity?.type == ActivityTypes.Invoke && context?.activity.name === MessageExtensionsInvokeNames.CONFIGURE_SETTINGS + context?.activity?.type == ActivityTypes.Invoke && context?.activity.name === CONFIGURE_SETTINGS ); // Add route @@ -642,10 +648,11 @@ export class MessageExtensions { public handleOnButtonClicked>( handler: (context: TurnContext, state: TState, data: TData) => Promise ): Application { + const { QUERY_CARD_BUTTON_CLICKED } = MessageExtensionsInvokeNames; // Define static route selector const selector = (context: TurnContext) => Promise.resolve( - context?.activity?.type == ActivityTypes.Invoke && context?.activity.name === MessageExtensionsInvokeNames.QUERY_CARD_BUTTON_CLICKED + context?.activity?.type == ActivityTypes.Invoke && context?.activity.name === QUERY_CARD_BUTTON_CLICKED ); // Add route diff --git a/js/packages/teams-ai/src/TaskModules.ts b/js/packages/teams-ai/src/TaskModules.ts index ccc3e305a..591227854 100644 --- a/js/packages/teams-ai/src/TaskModules.ts +++ b/js/packages/teams-ai/src/TaskModules.ts @@ -8,9 +8,11 @@ import { ActivityTypes, + CacheInfo, Channels, INVOKE_RESPONSE_KEY, InvokeResponse, + SuggestedActions, TaskModuleResponse, TaskModuleTaskInfo, TurnContext @@ -18,20 +20,31 @@ import { import { Application, RouteSelector } from './Application'; import { TurnState } from './TurnState'; -/** - * @private - */ -const FETCH_INVOKE_NAME = `task/fetch`; +export enum TaskModuleInvokeNames { + CONFIG_FETCH_INVOKE_NAME = `config/fetch`, + CONFIG_SUBMIT_INVOKE_NAME = `config/submit`, + FETCH_INVOKE_NAME = `task/fetch`, + SUBMIT_INVOKE_NAME = `task/submit`, + DEFAULT_TASK_DATA_FILTER = 'verb' +} /** - * @private + * temporary types + * Due to an error found in botbuilder-js, we need to temporarily define these types here in order to avoid being blocked by botbuilder-js bugfix release. corinagum is tracking; message her for updates. Merged PR: https://github.com/microsoft/botbuilder-js/pull/4570 + * release tbd */ -const SUBMIT_INVOKE_NAME = `task/submit`; +export interface ConfigResponse { + cacheInfo?: CacheInfo; + config: ConfigResponseConfig; + responseType: 'config'; +} +export interface BotConfigAuth { + suggestedActions?: SuggestedActions; + type: 'auth'; +} -/** - * @private - */ -const DEFAULT_TASK_DATA_FILTER = 'verb'; +export type ConfigResponseConfig = BotConfigAuth | TaskModuleResponse; +// end temporary types section /** * Options for TaskModules class. @@ -81,6 +94,7 @@ export class TaskModules { handler: (context: TurnContext, state: TState, data: TData) => Promise ): Application { (Array.isArray(verb) ? verb : [verb]).forEach((v) => { + const { DEFAULT_TASK_DATA_FILTER, FETCH_INVOKE_NAME } = TaskModuleInvokeNames; const filterField = this._app.options.taskModules?.taskDataFilter ?? DEFAULT_TASK_DATA_FILTER; const selector = createTaskSelector(v, filterField, FETCH_INVOKE_NAME); this._app.addRoute( @@ -156,6 +170,7 @@ export class TaskModules { ) => Promise ): Application { (Array.isArray(verb) ? verb : [verb]).forEach((v) => { + const { DEFAULT_TASK_DATA_FILTER, SUBMIT_INVOKE_NAME } = TaskModuleInvokeNames; const filterField = this._app.options.taskModules?.taskDataFilter ?? DEFAULT_TASK_DATA_FILTER; const selector = createTaskSelector(v, filterField, SUBMIT_INVOKE_NAME); this._app.addRoute( @@ -215,6 +230,104 @@ export class TaskModules { }); return this._app; } + + /** + * Registers a handler for fetching Teams config data for Auth or Task Modules + * @template TData Optional. Type of the data object being passed to the handler. + * @param {(context: TurnContext, state: TState, data: TData) => Promise} handler - Function to call when the handler is triggered. + * @param {TurnContext} handler.context - Context for the current turn of conversation with the user. + * @param {TState} handler.state - Current state of the turn. + * @param {TData} handler.data - Data object passed to the handler. + * @returns {Application} The application for chaining purposes. + */ + public configFetch>( + handler: (context: TurnContext, state: TState, data: TData) => Promise + ): Application { + const selector = (context: TurnContext) => { + const { CONFIG_SUBMIT_INVOKE_NAME } = TaskModuleInvokeNames; + return Promise.resolve( + context?.activity?.type === ActivityTypes.Invoke && + context?.activity?.name === CONFIG_SUBMIT_INVOKE_NAME + ); + }; + this._app.addRoute( + selector, + async (context, state) => { + if (context?.activity?.channelId === Channels.Msteams) { + // 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 ?? {}); + let response: ConfigResponse; + if (!context.turnState.get(INVOKE_RESPONSE_KEY)) { + // Format invoke response) + response = { + responseType: 'config', + config: result + }; + + if ('cacheInfo' in result) { + response.cacheInfo = result.cacheInfo; + } + + // Queue up invoke response + await context.sendActivity({ + value: { body: response, status: 200 } as InvokeResponse, + type: ActivityTypes.InvokeResponse + }); + } + } + }, + true + ); + return this._app; + } + + /** + * Registers a handler for submitting Teams config data for Auth or Task Modules + * @template TData Optional. Type of the data object being passed to the handler. + * @param {(context: TurnContext, state: TState, data: TData) => Promise} handler - Function to call when the handler is triggered. + * @param {TurnContext} handler.context - Context for the current turn of conversation with the user. + * @param {TState} handler.state - Current state of the turn. + * @param {TData} handler.data - Data object passed to the handler. + * @returns {Application} The application for chaining purposes. + */ + public configSubmit>( + handler: (context: TurnContext, state: TState, data: TData) => Promise + ): Application { + const selector = (context: TurnContext) => { + const { CONFIG_FETCH_INVOKE_NAME } = TaskModuleInvokeNames; + return Promise.resolve( + context?.activity?.type === ActivityTypes.Invoke && context?.activity?.name === CONFIG_FETCH_INVOKE_NAME + ); + }; + this._app.addRoute( + selector, + async (context, state) => { + if (context?.activity?.channelId === Channels.Msteams) { + // 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 ?? {}); + let response: ConfigResponse; + if (!context.turnState.get(INVOKE_RESPONSE_KEY)) { + // Format invoke response) + response = { + responseType: 'config', + config: result + }; + if ('cacheInfo' in result) { + response.cacheInfo = result.cacheInfo; + } + + // Queue up invoke response + await context.sendActivity({ + value: { body: response, status: 200 } as InvokeResponse, + type: ActivityTypes.InvokeResponse + }); + } + } + }, + true + ); + return this._app; + } } /**