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/botbuilder-core/src/activityHandler.ts b/libraries/botbuilder-core/src/activityHandler.ts index 4d7a29c4c9..54d9f8397e 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, + 'BadRequest', + `Missing kind property for search.` + ); + + throw new InvokeException(StatusCodes.BAD_REQUEST, response); + } + + if (!value.queryText) { + const response = this.createAdaptiveCardInvokeErrorResponse( + StatusCodes.BAD_REQUEST, + 'BadRequest', + `Missing 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..c3015eee2f 100644 --- a/libraries/botbuilder-core/tests/ActivityHandler.test.js +++ b/libraries/botbuilder-core/tests/ActivityHandler.test.js @@ -338,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; 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: { diff --git a/libraries/botframework-schema/src/index.ts b/libraries/botframework-schema/src/index.ts index 19cb4d28ca..189d603265 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 'application/search'. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SearchInvokeResponse extends AdaptiveCardInvokeResponse {} + /** * Represents a response returned by a bot when it receives an `invoke` activity. *