From 11a5bafba78e9564040e21d5e34934f27b31ef75 Mon Sep 17 00:00:00 2001 From: Eric Dahlvang Date: Mon, 21 Mar 2022 17:25:37 -0500 Subject: [PATCH 1/9] Add search invoke --- .../botbuilder-core/src/activityHandler.ts | 56 +++++++++++++++++++ .../tests/ActivityHandler.test.js | 23 ++++++++ libraries/botframework-schema/src/index.ts | 48 ++++++++++++++++ 3 files changed, 127 insertions(+) diff --git a/libraries/botbuilder-core/src/activityHandler.ts b/libraries/botbuilder-core/src/activityHandler.ts index 4d7a29c4c9..e325c37b6a 100644 --- a/libraries/botbuilder-core/src/activityHandler.ts +++ b/libraries/botbuilder-core/src/activityHandler.ts @@ -13,6 +13,8 @@ import { Activity, AdaptiveCardInvokeResponse, AdaptiveCardInvokeValue, + SearchInvokeResponse, + SearchInvokeValue, MessageReaction, StatusCodes, } from 'botframework-schema'; @@ -496,6 +498,12 @@ export class ActivityHandler extends ActivityHandlerBase { protected async onInvokeActivity(context: TurnContext): Promise { try { switch (context.activity.name) { + case 'application/search': { + const invokeValue = this.getSearchInvokeValue(context.activity); + const response = await this.onSearchInvoke(context, invokeValue); + return { status: response.statusCode, body: response }; + } + case 'adaptiveCard/action': { const invokeValue = this.getAdaptiveCardInvokeValue(context.activity); const response = await this.onAdaptiveCardInvoke(context, invokeValue); @@ -550,6 +558,19 @@ export class ActivityHandler extends ActivityHandlerBase { return Promise.reject(new InvokeException(StatusCodes.NOT_IMPLEMENTED)); } + /** + * Invoked when the bot is sent an invoke activity with name of 'application/search'. + * + * @param context the context object for the current turn + * @param invokeValue incoming activity value + */ + protected onSearchInvoke( + context: TurnContext, + invokeValue: SearchInvokeValue + ): Promise { + return Promise.reject(new InvokeException(StatusCodes.NOT_IMPLEMENTED)); + } + /** * Runs all registered _endOfConversation_ handlers and then continues the event emission process. * @@ -693,6 +714,41 @@ export class ActivityHandler extends ActivityHandlerBase { await this.handle(context, 'UnrecognizedActivityType', this.defaultNextEvent(context)); } + private getSearchInvokeValue(activity: Activity): SearchInvokeValue { + const { value }: { value?: SearchInvokeValue } = activity; + if (!value) { + const response = this.createAdaptiveCardInvokeErrorResponse( + StatusCodes.BAD_REQUEST, + 'BadRequest', + 'Missing value property for search' + ); + + throw new InvokeException(StatusCodes.BAD_REQUEST, response); + } + + if (!value.kind) { + const response = this.createAdaptiveCardInvokeErrorResponse( + StatusCodes.BAD_REQUEST, + 'NotSupported', + `Missing kind property for search.` + ); + + throw new InvokeException(StatusCodes.BAD_REQUEST, response); + } + + if (!value.queryText) { + const response = this.createAdaptiveCardInvokeErrorResponse( + StatusCodes.BAD_REQUEST, + 'NotSupported', + `Missing kind queryText for search.` + ); + + throw new InvokeException(StatusCodes.BAD_REQUEST, response); + } + + return value; + } + private getAdaptiveCardInvokeValue(activity: Activity): AdaptiveCardInvokeValue { const { value }: { value?: AdaptiveCardInvokeValue } = activity; if (!value) { diff --git a/libraries/botbuilder-core/tests/ActivityHandler.test.js b/libraries/botbuilder-core/tests/ActivityHandler.test.js index 9ec1eca2b9..2df30fd6bb 100644 --- a/libraries/botbuilder-core/tests/ActivityHandler.test.js +++ b/libraries/botbuilder-core/tests/ActivityHandler.test.js @@ -312,6 +312,29 @@ describe('ActivityHandler', function () { assert(onAdpativeCardInvokeCalled); }); + it(`should fire onSearchInvoke`, async function () { + const bot = new ActivityHandler(); + + let onSearchInvokeCalled = false; + bot.onSearchInvoke = async () => { + onSearchInvokeCalled = true; + return { statusCode: 200, value: 'called' }; + }; + + await processActivity( + { + type: ActivityTypes.Invoke, + name: 'application/search', + value: { + kind: 'search', + queryText: 'test bot' + }, + }, + bot + ); + assert(onSearchInvokeCalled); + }); + it(`should fire onUnrecognizedActivityType`, async function () { const bot = new ActivityHandler(); diff --git a/libraries/botframework-schema/src/index.ts b/libraries/botframework-schema/src/index.ts index 19cb4d28ca..747f3cdcba 100644 --- a/libraries/botframework-schema/src/index.ts +++ b/libraries/botframework-schema/src/index.ts @@ -2368,6 +2368,54 @@ export interface AdaptiveCardInvokeValue { state: string; } +/** + * Defines the structure that arrives in the Activity.Value for Invoke activity with + * Name of 'application/search'. + */ + export interface SearchInvokeValue { + /** + * The kind of the search invoke value. + * Must be either search, searchAnswer or typeAhead. + */ + kind: string; + /** + * The query text for the search invoke value. + */ + queryText: string; + /** + * The [SearchInvokeOptions](xref:botframework-schema.SearchInvokeOptions) + * for this search invoke value. + */ + queryOptions: SearchInvokeOptions; + /** + * The the context information about the query. Such as the UI control that issued the query. + * The type of the context field is object and is dependent on the kind field. + * For search and searchAnswers, there is no defined context value. + */ + context: any; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +/** + * Provides information about the options desired for a [SearchInvokeValue](xref:botframework-schema.SearchInvokeValue) + */ + export interface SearchInvokeOptions { + /** + * Indicates starting reference number from which ordered search results should be returned. + */ + skip: number; + /** + * Indicates the number of search results that should be returned. + */ + top: number; +} + +/** + * Defines the structure that is returned as the result of an Invoke activity with + * Name of 'pplication/search'. + */ + export interface SearchInvokeResponse extends AdaptiveCardInvokeResponse { +} + /** * Represents a response returned by a bot when it receives an `invoke` activity. * From 614fbb21a97b3e3b542b25481747a0b2e15ef6b6 Mon Sep 17 00:00:00 2001 From: Eric Dahlvang Date: Mon, 21 Mar 2022 17:33:56 -0500 Subject: [PATCH 2/9] allow empty SearchInvokeResponse interface --- libraries/botframework-schema/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/botframework-schema/src/index.ts b/libraries/botframework-schema/src/index.ts index 747f3cdcba..7a9465d710 100644 --- a/libraries/botframework-schema/src/index.ts +++ b/libraries/botframework-schema/src/index.ts @@ -2413,6 +2413,7 @@ export interface AdaptiveCardInvokeValue { * Defines the structure that is returned as the result of an Invoke activity with * Name of 'pplication/search'. */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SearchInvokeResponse extends AdaptiveCardInvokeResponse { } From e29a5c560979fd5c92d96335aab46c52fe6345b2 Mon Sep 17 00:00:00 2001 From: Eric Dahlvang Date: Mon, 21 Mar 2022 17:39:14 -0500 Subject: [PATCH 3/9] lint --- libraries/botframework-schema/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/botframework-schema/src/index.ts b/libraries/botframework-schema/src/index.ts index 7a9465d710..fef8fd0d6f 100644 --- a/libraries/botframework-schema/src/index.ts +++ b/libraries/botframework-schema/src/index.ts @@ -2415,6 +2415,7 @@ export interface AdaptiveCardInvokeValue { */ // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SearchInvokeResponse extends AdaptiveCardInvokeResponse { + } /** From dc17aec241652187e50c429d4d7279f198d53ac1 Mon Sep 17 00:00:00 2001 From: Eric Dahlvang Date: Mon, 21 Mar 2022 17:55:21 -0500 Subject: [PATCH 4/9] lint2 --- libraries/botframework-schema/src/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libraries/botframework-schema/src/index.ts b/libraries/botframework-schema/src/index.ts index fef8fd0d6f..02cdbb1dce 100644 --- a/libraries/botframework-schema/src/index.ts +++ b/libraries/botframework-schema/src/index.ts @@ -2414,9 +2414,7 @@ export interface AdaptiveCardInvokeValue { * Name of 'pplication/search'. */ // eslint-disable-next-line @typescript-eslint/no-empty-interface - export interface SearchInvokeResponse extends AdaptiveCardInvokeResponse { - -} + export interface SearchInvokeResponse extends AdaptiveCardInvokeResponse {} /** * Represents a response returned by a bot when it receives an `invoke` activity. From 96e57d84f9bd256bb940cc6ff086c39e6c6e9049 Mon Sep 17 00:00:00 2001 From: Eric Dahlvang Date: Mon, 21 Mar 2022 18:07:29 -0500 Subject: [PATCH 5/9] lint3 --- libraries/botframework-schema/src/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/botframework-schema/src/index.ts b/libraries/botframework-schema/src/index.ts index 02cdbb1dce..d47f32b7d3 100644 --- a/libraries/botframework-schema/src/index.ts +++ b/libraries/botframework-schema/src/index.ts @@ -2372,7 +2372,7 @@ export interface AdaptiveCardInvokeValue { * Defines the structure that arrives in the Activity.Value for Invoke activity with * Name of 'application/search'. */ - export interface SearchInvokeValue { +export interface SearchInvokeValue { /** * The kind of the search invoke value. * Must be either search, searchAnswer or typeAhead. @@ -2386,10 +2386,10 @@ export interface AdaptiveCardInvokeValue { * The [SearchInvokeOptions](xref:botframework-schema.SearchInvokeOptions) * for this search invoke value. */ - queryOptions: SearchInvokeOptions; + queryOptions: SearchInvokeOptions; /** - * The the context information about the query. Such as the UI control that issued the query. - * The type of the context field is object and is dependent on the kind field. + * The the context information about the query. Such as the UI control that issued the query. + * The type of the context field is object and is dependent on the kind field. * For search and searchAnswers, there is no defined context value. */ context: any; // eslint-disable-line @typescript-eslint/no-explicit-any @@ -2398,7 +2398,7 @@ export interface AdaptiveCardInvokeValue { /** * Provides information about the options desired for a [SearchInvokeValue](xref:botframework-schema.SearchInvokeValue) */ - export interface SearchInvokeOptions { +export interface SearchInvokeOptions { /** * Indicates starting reference number from which ordered search results should be returned. */ @@ -2414,7 +2414,7 @@ export interface AdaptiveCardInvokeValue { * Name of 'pplication/search'. */ // eslint-disable-next-line @typescript-eslint/no-empty-interface - export interface SearchInvokeResponse extends AdaptiveCardInvokeResponse {} +export interface SearchInvokeResponse extends AdaptiveCardInvokeResponse { } /** * Represents a response returned by a bot when it receives an `invoke` activity. From 39222d080acd6d54668d63ae3e0c189cae26e5e1 Mon Sep 17 00:00:00 2001 From: Eric Dahlvang Date: Mon, 21 Mar 2022 18:20:58 -0500 Subject: [PATCH 6/9] remove space --- libraries/botframework-schema/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/botframework-schema/src/index.ts b/libraries/botframework-schema/src/index.ts index d47f32b7d3..1d240025c7 100644 --- a/libraries/botframework-schema/src/index.ts +++ b/libraries/botframework-schema/src/index.ts @@ -2414,7 +2414,7 @@ export interface SearchInvokeOptions { * Name of 'pplication/search'. */ // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface SearchInvokeResponse extends AdaptiveCardInvokeResponse { } +export interface SearchInvokeResponse extends AdaptiveCardInvokeResponse {} /** * Represents a response returned by a bot when it receives an `invoke` activity. From 9873c94611f1e315cb955ad91f0fc1bad1c11495 Mon Sep 17 00:00:00 2001 From: Eric Dahlvang Date: Tue, 22 Mar 2022 16:59:29 -0500 Subject: [PATCH 7/9] update botframework-schema.api.md --- .../etc/botframework-schema.api.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/libraries/botframework-schema/etc/botframework-schema.api.md b/libraries/botframework-schema/etc/botframework-schema.api.md index 2df641951a..1898944e44 100644 --- a/libraries/botframework-schema/etc/botframework-schema.api.md +++ b/libraries/botframework-schema/etc/botframework-schema.api.md @@ -1454,6 +1454,24 @@ export enum RoleTypes { User = "user" } +// @public +export interface SearchInvokeOptions { + skip: number; + top: number; +} + +// @public +export interface SearchInvokeResponse extends AdaptiveCardInvokeResponse { +} + +// @public +export interface SearchInvokeValue { + context: any; + kind: string; + queryOptions: SearchInvokeOptions; + queryText: string; +} + // @public export interface SemanticAction { entities: { From 50e8b4f46427a6b1507e2c6a8880c54388f85a6f Mon Sep 17 00:00:00 2001 From: Eric Dahlvang Date: Tue, 22 Mar 2022 17:11:29 -0500 Subject: [PATCH 8/9] update botbuilder-core.api.md --- libraries/botbuilder-core/etc/botbuilder-core.api.md | 3 +++ libraries/botframework-schema/src/index.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/botbuilder-core/etc/botbuilder-core.api.md b/libraries/botbuilder-core/etc/botbuilder-core.api.md index eef206aa17..ef14a50002 100644 --- a/libraries/botbuilder-core/etc/botbuilder-core.api.md +++ b/libraries/botbuilder-core/etc/botbuilder-core.api.md @@ -33,6 +33,8 @@ import { O365ConnectorCard } from 'botframework-schema'; import { PasswordServiceClientCredentialFactory } from 'botframework-connector'; import { ReceiptCard } from 'botframework-schema'; import { ResourceResponse } from 'botframework-schema'; +import { SearchInvokeResponse } from 'botframework-schema'; +import { SearchInvokeValue } from 'botframework-schema'; import { ServiceClientCredentials } from 'botframework-connector'; import { ServiceClientCredentialsFactory } from 'botframework-connector'; import { ServiceCollection } from 'botbuilder-dialogs-adaptive-runtime-core'; @@ -94,6 +96,7 @@ export class ActivityHandler extends ActivityHandlerBase { protected onReactionsAddedActivity(reactionsAdded: MessageReaction[], context: TurnContext): Promise; onReactionsRemoved(handler: BotHandler): this; protected onReactionsRemovedActivity(reactionsRemoved: MessageReaction[], context: TurnContext): Promise; + protected onSearchInvoke(context: TurnContext, invokeValue: SearchInvokeValue): Promise; protected onSignInInvoke(context: TurnContext): Promise; onTokenResponseEvent(handler: BotHandler): this; onTurn(handler: BotHandler): this; diff --git a/libraries/botframework-schema/src/index.ts b/libraries/botframework-schema/src/index.ts index 1d240025c7..189d603265 100644 --- a/libraries/botframework-schema/src/index.ts +++ b/libraries/botframework-schema/src/index.ts @@ -2411,7 +2411,7 @@ export interface SearchInvokeOptions { /** * Defines the structure that is returned as the result of an Invoke activity with - * Name of 'pplication/search'. + * Name of 'application/search'. */ // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SearchInvokeResponse extends AdaptiveCardInvokeResponse {} From d4b45fabbcb1c62c60bf08391d78bfb449140210 Mon Sep 17 00:00:00 2001 From: Eric Dahlvang Date: Wed, 23 Mar 2022 17:29:21 -0500 Subject: [PATCH 9/9] add tests for search value validation --- .../botbuilder-core/src/activityHandler.ts | 6 +- .../tests/ActivityHandler.test.js | 117 ++++++++++++++---- 2 files changed, 97 insertions(+), 26 deletions(-) diff --git a/libraries/botbuilder-core/src/activityHandler.ts b/libraries/botbuilder-core/src/activityHandler.ts index e325c37b6a..54d9f8397e 100644 --- a/libraries/botbuilder-core/src/activityHandler.ts +++ b/libraries/botbuilder-core/src/activityHandler.ts @@ -729,7 +729,7 @@ export class ActivityHandler extends ActivityHandlerBase { if (!value.kind) { const response = this.createAdaptiveCardInvokeErrorResponse( StatusCodes.BAD_REQUEST, - 'NotSupported', + 'BadRequest', `Missing kind property for search.` ); @@ -739,8 +739,8 @@ export class ActivityHandler extends ActivityHandlerBase { if (!value.queryText) { const response = this.createAdaptiveCardInvokeErrorResponse( StatusCodes.BAD_REQUEST, - 'NotSupported', - `Missing kind queryText for search.` + 'BadRequest', + `Missing queryText for search.` ); throw new InvokeException(StatusCodes.BAD_REQUEST, response); diff --git a/libraries/botbuilder-core/tests/ActivityHandler.test.js b/libraries/botbuilder-core/tests/ActivityHandler.test.js index 2df30fd6bb..c3015eee2f 100644 --- a/libraries/botbuilder-core/tests/ActivityHandler.test.js +++ b/libraries/botbuilder-core/tests/ActivityHandler.test.js @@ -312,29 +312,6 @@ describe('ActivityHandler', function () { assert(onAdpativeCardInvokeCalled); }); - it(`should fire onSearchInvoke`, async function () { - const bot = new ActivityHandler(); - - let onSearchInvokeCalled = false; - bot.onSearchInvoke = async () => { - onSearchInvokeCalled = true; - return { statusCode: 200, value: 'called' }; - }; - - await processActivity( - { - type: ActivityTypes.Invoke, - name: 'application/search', - value: { - kind: 'search', - queryText: 'test bot' - }, - }, - bot - ); - assert(onSearchInvokeCalled); - }); - it(`should fire onUnrecognizedActivityType`, async function () { const bot = new ActivityHandler(); @@ -361,6 +338,100 @@ describe('ActivityHandler', function () { assert(onDialogCalled); }); + describe('ActivityHandler.onSearchInvoke', function () { + it(`should fire onSearchInvoke`, async function () { + const bot = new ActivityHandler(); + + let onSearchInvokeCalled = false; + let value = null; + bot.onSearchInvoke = async (context, invokeValue) => { + onSearchInvokeCalled = true; + value = invokeValue; + return { statusCode: 200, value: 'called' }; + }; + + const activity = { + type: ActivityTypes.Invoke, + name: 'application/search', + value: { + kind: 'search', + queryText: 'test bot', + queryOptions: { + skip: 5, + top: 10 + }, + context: "bot framework" + }, + }; + + await processActivity( + activity, + bot + ); + + assert(onSearchInvokeCalled); + assert.equal(activity.value.queryText, value.queryText, 'missing query text'); + assert.equal(activity.value.kind, value.kind, 'missing kind'); + assert.equal(activity.value.queryOptions.skip, value.queryOptions.skip, 'missing skip'); + assert.equal(activity.value.queryOptions.top, value.queryOptions.top, 'missing top'); + assert.equal(activity.value.context, value.context, 'missing context'); + }); + + it(`should throw on onSearchInvoke activity missing value`, async function () { + const activity = { + type: ActivityTypes.Invoke, + name: 'application/search' + }; + + await assertSearchResultError(activity, 'Missing value property for search'); + }); + + it(`should throw on onSearchInvoke activity missing kind`, async function () { + const activity = { + type: ActivityTypes.Invoke, + name: 'application/search', + value: { + queryText: 'test bot' + }, + }; + + await assertSearchResultError(activity, 'Missing kind property for search.'); + }); + + it(`should throw on onSearchInvoke activity missing queryText`, async function () { + const activity = { + type: ActivityTypes.Invoke, + name: 'application/search', + value: { + kind: 'search' + }, + }; + + await assertSearchResultError(activity, 'Missing queryText for search.'); + }); + + async function assertSearchResultError(activity, errorMessage) { + const bot = new ActivityHandler(); + const testAdapter = new TestAdapter(); + + let onSearchInvokeCalled = false; + bot.onSearchInvoke = async (context, invokeValue) => { + onSearchInvokeCalled = true; + return { statusCode: 200, value: 'called' }; + }; + + const context = new TurnContext(testAdapter, activity); + await bot.run(context); + + assert(!onSearchInvokeCalled, 'Search should fail to be called due to invalid activity.value'); + const responseValue = testAdapter.activeQueue[0].value; + assert.equal(400, responseValue.status); + assert.equal('application/vnd.microsoft.error', responseValue.body.type); + assert.equal('BadRequest', responseValue.body.value.code); + assert.equal(errorMessage, responseValue.body.value.message); + } + }); + describe('should by default', function () { let onTurnCalled = false; let onMessageCalled = false;