From 861ae1de5ba7d95cd0f63a7ae594d9ff132a6658 Mon Sep 17 00:00:00 2001 From: Steven Ickman Date: Sat, 13 Oct 2018 18:48:51 -0700 Subject: [PATCH 1/4] Cleaned up adapter - Updated code to use async/await - Fixed status codes returned by processActivity(). - Added ability to pass ConversationParamters to createConversation(). --- .../botbuilder/src/botFrameworkAdapter.ts | 450 ++++++++---------- .../tests/botFrameworkAdapter.test.js | 10 +- 2 files changed, 204 insertions(+), 256 deletions(-) diff --git a/libraries/botbuilder/src/botFrameworkAdapter.ts b/libraries/botbuilder/src/botFrameworkAdapter.ts index 25c9f46ed6..7454836212 100644 --- a/libraries/botbuilder/src/botFrameworkAdapter.ts +++ b/libraries/botbuilder/src/botFrameworkAdapter.ts @@ -24,7 +24,6 @@ import { ConversationAccount, ConversationParameters, ConversationReference, - ConversationResourceResponse, ConversationsResult, ResourceResponse, TokenResponse, @@ -197,7 +196,7 @@ export class BotFrameworkAdapter extends BotAdapter { * @param reference A `ConversationReference` saved during a previous incoming activity. * @param logic A function handler that will be called to perform the bots logic after the the adapters middleware has been run. */ - public continueConversation(reference: Partial, logic: (context: TurnContext) => Promise): Promise { + public async continueConversation(reference: Partial, logic: (context: TurnContext) => Promise): Promise { const request: Partial = TurnContext.applyConversationReference( {type: 'event', name: 'continueConversation' }, reference, @@ -205,7 +204,7 @@ export class BotFrameworkAdapter extends BotAdapter { ); const context: TurnContext = this.createContext(request); - return this.runMiddleware(context, logic as any); + await this.runMiddleware(context, logic as any); } /** @@ -238,34 +237,38 @@ export class BotFrameworkAdapter extends BotAdapter { * }); * ``` * @param reference A `ConversationReference` of the user to start a new conversation with. + * @param params (Optional) parameters to start the conversation with. * @param logic A function handler that will be called to perform the bot's logic after the the adapters middleware has been run. */ - public createConversation(reference: Partial, logic: (context: TurnContext) => Promise): Promise { - try { - if (!reference.serviceUrl) { throw new Error(`BotFrameworkAdapter.createConversation(): missing serviceUrl.`); } - - // Create conversation - const parameters: ConversationParameters = { bot: reference.bot, members: [reference.user] } as ConversationParameters; - const client: ConnectorClient = this.createConnectorClient(reference.serviceUrl); - - return client.conversations.createConversation(parameters).then((response: ConversationResourceResponse) => { - // Initialize request and copy over new conversation ID and updated serviceUrl. - const request: Partial = TurnContext.applyConversationReference( - {type: 'event', name: 'createConversation' }, - reference, - true - ); - request.conversation = { id: response.id } as ConversationAccount; - if (response.serviceUrl) { request.serviceUrl = response.serviceUrl; } - - // Create context and run middleware - const context: TurnContext = this.createContext(request); - - return this.runMiddleware(context, logic as any); - }); - } catch (err) { - return Promise.reject(err); + public createConversation(reference: Partial, logic: (context: TurnContext) => Promise): Promise; + public createConversation(reference: Partial, params: ConversationParameters, logic: (context: TurnContext) => Promise): Promise; + public async createConversation(reference: Partial, params: ((context: TurnContext) => Promise)|ConversationParameters, logic?: (context: TurnContext) => Promise): Promise { + if (!reference.serviceUrl) { throw new Error(`BotFrameworkAdapter.createConversation(): missing serviceUrl.`); } + if (typeof params === 'function') { + logic = params; + params = {} as ConversationParameters; } + + // Initialize params + params.bot = reference.bot; + if (!params.members) { params.members = [reference.user] } + + // Create conversation + const client: ConnectorClient = this.createConnectorClient(reference.serviceUrl); + const response = await client.conversations.createConversation(params); + + // Initialize request and copy over new conversation ID and updated serviceUrl. + const request: Partial = TurnContext.applyConversationReference( + {type: 'event', name: 'createConversation' }, + reference, + true + ); + request.conversation = { id: response.id } as ConversationAccount; + if (response.serviceUrl) { request.serviceUrl = response.serviceUrl; } + + // Create context and run middleware + const context: TurnContext = this.createContext(request); + await this.runMiddleware(context, logic as any); } /** @@ -280,19 +283,14 @@ export class BotFrameworkAdapter extends BotAdapter { * @param context Context for the current turn of conversation with the user. * @param reference Conversation reference information for the activity being deleted. */ - public deleteActivity(context: TurnContext, reference: Partial): Promise { - try { - if (!reference.serviceUrl) { throw new Error(`BotFrameworkAdapter.deleteActivity(): missing serviceUrl`); } - if (!reference.conversation || !reference.conversation.id) { - throw new Error(`BotFrameworkAdapter.deleteActivity(): missing conversation or conversation.id`); - } - if (!reference.activityId) { throw new Error(`BotFrameworkAdapter.deleteActivity(): missing activityId`); } - const client: ConnectorClient = this.createConnectorClient(reference.serviceUrl); - - return client.conversations.deleteActivity(reference.conversation.id, reference.activityId); - } catch (err) { - return Promise.reject(err); + public async deleteActivity(context: TurnContext, reference: Partial): Promise { + if (!reference.serviceUrl) { throw new Error(`BotFrameworkAdapter.deleteActivity(): missing serviceUrl`); } + if (!reference.conversation || !reference.conversation.id) { + throw new Error(`BotFrameworkAdapter.deleteActivity(): missing conversation or conversation.id`); } + if (!reference.activityId) { throw new Error(`BotFrameworkAdapter.deleteActivity(): missing activityId`); } + const client: ConnectorClient = this.createConnectorClient(reference.serviceUrl); + await client.conversations.deleteActivity(reference.conversation.id, reference.activityId); } /** @@ -305,20 +303,15 @@ export class BotFrameworkAdapter extends BotAdapter { * @param context Context for the current turn of conversation with the user. * @param memberId ID of the member to delete from the conversation. */ - public deleteConversationMember(context: TurnContext, memberId: string): Promise { - try { - if (!context.activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.deleteConversationMember(): missing serviceUrl`); } - if (!context.activity.conversation || !context.activity.conversation.id) { - throw new Error(`BotFrameworkAdapter.deleteConversationMember(): missing conversation or conversation.id`); - } - const serviceUrl: string = context.activity.serviceUrl; - const conversationId: string = context.activity.conversation.id; - const client: ConnectorClient = this.createConnectorClient(serviceUrl); - - return client.conversations.deleteConversationMember(conversationId, memberId); - } catch (err) { - return Promise.reject(err); + public async deleteConversationMember(context: TurnContext, memberId: string): Promise { + if (!context.activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.deleteConversationMember(): missing serviceUrl`); } + if (!context.activity.conversation || !context.activity.conversation.id) { + throw new Error(`BotFrameworkAdapter.deleteConversationMember(): missing conversation or conversation.id`); } + const serviceUrl: string = context.activity.serviceUrl; + const conversationId: string = context.activity.conversation.id; + const client: ConnectorClient = this.createConnectorClient(serviceUrl); + await client.conversations.deleteConversationMember(conversationId, memberId); } /** @@ -332,24 +325,20 @@ export class BotFrameworkAdapter extends BotAdapter { * @param context Context for the current turn of conversation with the user. * @param activityId (Optional) activity ID to enumerate. If not specified the current activities ID will be used. */ - public getActivityMembers(context: TurnContext, activityId?: string): Promise { - try { - if (!activityId) { activityId = context.activity.id; } - if (!context.activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.getActivityMembers(): missing serviceUrl`); } - if (!context.activity.conversation || !context.activity.conversation.id) { - throw new Error(`BotFrameworkAdapter.getActivityMembers(): missing conversation or conversation.id`); - } - if (!activityId) { - throw new Error(`BotFrameworkAdapter.getActivityMembers(): missing both activityId and context.activity.id`); - } - const serviceUrl: string = context.activity.serviceUrl; - const conversationId: string = context.activity.conversation.id; - const client: ConnectorClient = this.createConnectorClient(serviceUrl); - - return client.conversations.getActivityMembers(conversationId, activityId); - } catch (err) { - return Promise.reject(err); + public async getActivityMembers(context: TurnContext, activityId?: string): Promise { + if (!activityId) { activityId = context.activity.id; } + if (!context.activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.getActivityMembers(): missing serviceUrl`); } + if (!context.activity.conversation || !context.activity.conversation.id) { + throw new Error(`BotFrameworkAdapter.getActivityMembers(): missing conversation or conversation.id`); + } + if (!activityId) { + throw new Error(`BotFrameworkAdapter.getActivityMembers(): missing both activityId and context.activity.id`); } + const serviceUrl: string = context.activity.serviceUrl; + const conversationId: string = context.activity.conversation.id; + const client: ConnectorClient = this.createConnectorClient(serviceUrl); + + return await client.conversations.getActivityMembers(conversationId, activityId); } /** @@ -363,20 +352,16 @@ export class BotFrameworkAdapter extends BotAdapter { * members of the conversation, not just those directly involved in the activity. * @param context Context for the current turn of conversation with the user. */ - public getConversationMembers(context: TurnContext): Promise { - try { - if (!context.activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.getConversationMembers(): missing serviceUrl`); } - if (!context.activity.conversation || !context.activity.conversation.id) { - throw new Error(`BotFrameworkAdapter.getConversationMembers(): missing conversation or conversation.id`); - } - const serviceUrl: string = context.activity.serviceUrl; - const conversationId: string = context.activity.conversation.id; - const client: ConnectorClient = this.createConnectorClient(serviceUrl); - - return client.conversations.getConversationMembers(conversationId); - } catch (err) { - return Promise.reject(err); + public async getConversationMembers(context: TurnContext): Promise { + if (!context.activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.getConversationMembers(): missing serviceUrl`); } + if (!context.activity.conversation || !context.activity.conversation.id) { + throw new Error(`BotFrameworkAdapter.getConversationMembers(): missing conversation or conversation.id`); } + const serviceUrl: string = context.activity.serviceUrl; + const conversationId: string = context.activity.conversation.id; + const client: ConnectorClient = this.createConnectorClient(serviceUrl); + + return await client.conversations.getConversationMembers(conversationId); } /** @@ -388,11 +373,11 @@ export class BotFrameworkAdapter extends BotAdapter { * @param contextOrServiceUrl The URL of the channel server to query or a TurnContext. This can be retrieved from `context.activity.serviceUrl`. * @param continuationToken (Optional) token used to fetch the next page of results from the channel server. This should be left as `undefined` to retrieve the first page of results. */ - public getConversations(contextOrServiceUrl: TurnContext|string, continuationToken?: string): Promise { + public async getConversations(contextOrServiceUrl: TurnContext|string, continuationToken?: string): Promise { const url: string = typeof contextOrServiceUrl === 'object' ? contextOrServiceUrl.activity.serviceUrl : contextOrServiceUrl; const client: ConnectorClient = this.createConnectorClient(url); - return client.conversations.getConversations(continuationToken ? { continuationToken: continuationToken } : undefined); + return await client.conversations.getConversations(continuationToken ? { continuationToken: continuationToken } : undefined); } /** @@ -401,20 +386,16 @@ export class BotFrameworkAdapter extends BotAdapter { * @param connectionName Name of the auth connection to use. * @param magicCode (Optional) Optional user entered code to validate. */ - public getUserToken(context: TurnContext, connectionName: string, magicCode?: string): Promise { - try { - if (!context.activity.from || !context.activity.from.id) { - throw new Error(`BotFrameworkAdapter.getUserToken(): missing from or from.id`); - } - this.checkEmulatingOAuthCards(context); - const userId: string = context.activity.from.id; - const url: string = this.oauthApiUrl(context); - const client: OAuthApiClient = this.createOAuthApiClient(url); - - return client.getUserToken(userId, connectionName, magicCode); - } catch (err) { - return Promise.reject(err); + public async getUserToken(context: TurnContext, connectionName: string, magicCode?: string): Promise { + if (!context.activity.from || !context.activity.from.id) { + throw new Error(`BotFrameworkAdapter.getUserToken(): missing from or from.id`); } + this.checkEmulatingOAuthCards(context); + const userId: string = context.activity.from.id; + const url: string = this.oauthApiUrl(context); + const client: OAuthApiClient = this.createOAuthApiClient(url); + + return await client.getUserToken(userId, connectionName, magicCode); } /** @@ -422,20 +403,15 @@ export class BotFrameworkAdapter extends BotAdapter { * @param context Context for the current turn of conversation with the user. * @param connectionName Name of the auth connection to use. */ - public signOutUser(context: TurnContext, connectionName: string): Promise { - try { - if (!context.activity.from || !context.activity.from.id) { - throw new Error(`BotFrameworkAdapter.signOutUser(): missing from or from.id`); - } - this.checkEmulatingOAuthCards(context); - const userId: string = context.activity.from.id; - const url: string = this.oauthApiUrl(context); - const client: OAuthApiClient = this.createOAuthApiClient(url); - - return client.signOutUser(userId, connectionName); - } catch (err) { - return Promise.reject(err); + public async signOutUser(context: TurnContext, connectionName: string): Promise { + if (!context.activity.from || !context.activity.from.id) { + throw new Error(`BotFrameworkAdapter.signOutUser(): missing from or from.id`); } + this.checkEmulatingOAuthCards(context); + const userId: string = context.activity.from.id; + const url: string = this.oauthApiUrl(context); + const client: OAuthApiClient = this.createOAuthApiClient(url); + await client.signOutUser(userId, connectionName); } /** @@ -443,13 +419,13 @@ export class BotFrameworkAdapter extends BotAdapter { * @param context Context for the current turn of conversation with the user. * @param connectionName Name of the auth connection to use. */ - public getSignInLink(context: TurnContext, connectionName: string): Promise { + public async getSignInLink(context: TurnContext, connectionName: string): Promise { this.checkEmulatingOAuthCards(context); const conversation: Partial = TurnContext.getConversationReference(context.activity); const url: string = this.oauthApiUrl(context); const client: OAuthApiClient = this.createOAuthApiClient(url); - return client.getSignInLink(conversation as ConversationReference, connectionName); + return await client.getSignInLink(conversation as ConversationReference, connectionName); } /** @@ -457,20 +433,16 @@ export class BotFrameworkAdapter extends BotAdapter { * @param context Context for the current turn of conversation with the user. * @param connectionName Name of the auth connection to use. */ - public getAadTokens(context: TurnContext, connectionName: string, resourceUrls: string[]): Promise { - try { - if (!context.activity.from || !context.activity.from.id) { - throw new Error(`BotFrameworkAdapter.getAadTokens(): missing from or from.id`); - } - this.checkEmulatingOAuthCards(context); - const userId: string = context.activity.from.id; - const url: string = this.oauthApiUrl(context); - const client: OAuthApiClient = this.createOAuthApiClient(url); - - return client.getAadTokens(userId, connectionName, { resourceUrls: resourceUrls }); - } catch (err) { - return Promise.reject(err); + public async getAadTokens(context: TurnContext, connectionName: string, resourceUrls: string[]): Promise { + if (!context.activity.from || !context.activity.from.id) { + throw new Error(`BotFrameworkAdapter.getAadTokens(): missing from or from.id`); } + this.checkEmulatingOAuthCards(context); + const userId: string = context.activity.from.id; + const url: string = this.oauthApiUrl(context); + const client: OAuthApiClient = this.createOAuthApiClient(url); + + return await client.getAadTokens(userId, connectionName, { resourceUrls: resourceUrls }); } /** @@ -478,12 +450,11 @@ export class BotFrameworkAdapter extends BotAdapter { * @param contextOrServiceUrl The URL of the channel server to query or a TurnContext. This can be retrieved from `context.activity.serviceUrl`. * @param emulate If `true` the token service will emulate the sending of OAuthCards. */ - public emulateOAuthCards(contextOrServiceUrl: TurnContext|string, emulate: boolean): Promise { + public async emulateOAuthCards(contextOrServiceUrl: TurnContext|string, emulate: boolean): Promise { this.isEmulatingOAuthCards = emulate; const url: string = this.oauthApiUrl(contextOrServiceUrl); const client: OAuthApiClient = this.createOAuthApiClient(url); - - return client.emulateOAuthCards(emulate); + await client.emulateOAuthCards(emulate); } /** @@ -541,47 +512,51 @@ export class BotFrameworkAdapter extends BotAdapter { * @param res An Express or Restify style Response object. * @param logic A function handler that will be called to perform the bots logic after the received activity has been pre-processed by the adapter and routed through any middleware for processing. */ - public processActivity(req: WebRequest, res: WebResponse, logic: (context: TurnContext) => Promise): Promise { - // Parse body of request - let errorCode: number = 500; + public async processActivity(req: WebRequest, res: WebResponse, logic: (context: TurnContext) => Promise): Promise { + let body: any; + let status: number; + try { + // Parse body of request + status = 400; + const request = await parseRequest(req); - return parseRequest(req).then((request: Activity) => { // Authenticate the incoming request - errorCode = 401; + status = 401; const authHeader: string = req.headers.authorization || req.headers.Authorization || ''; + await this.authenticateRequest(request, authHeader); + + // Process received activity + status = 500; + const context: TurnContext = this.createContext(request); + await this.runMiddleware(context, logic); + + // Retrieve cached invoke response. + if (request.type === ActivityTypes.Invoke) { + const invokeResponse: any = context.turnState.get(INVOKE_RESPONSE_KEY); + if (invokeResponse && invokeResponse.value) { + const value: InvokeResponse = invokeResponse.value; + status = value.status; + body = value.body; + } else { + throw new Error(`Bot failed to return a valid 'invokeResponse' activity.`); + } + } else { + status = 200; + } + } catch(err) { + body = err.toString(); + } - return this.authenticateRequest(request, authHeader).then(() => { - // Process received activity - errorCode = 500; - const context: TurnContext = this.createContext(request); - - return this.runMiddleware(context, logic as any) - .then(() => { - if (request.type === ActivityTypes.Invoke) { - // Retrieve cached invoke response. - const invokeResponse: any = context.turnState.get(INVOKE_RESPONSE_KEY); - if (invokeResponse && invokeResponse.value) { - const value: InvokeResponse = invokeResponse.value as InvokeResponse; - res.status(value.status); - res.send(value.body); - res.end(); - } else { - throw new Error(`Bot failed to return a valid 'invokeResponse' activity.`); - } - } else { - res.status(202); - res.end(); - } - }); - }); - }).catch((err: Error) => { - // Reject response with error code - console.warn(`BotFrameworkAdapter.processActivity(): ${errorCode} ERROR - ${err.toString()}`); - res.status(errorCode); - res.send(err.toString()); - res.end(); - throw err; - }); + // Return status + res.status(status); + if (body) { res.send(body) } + res.end(); + + // Check for an error + if (status >= 400) { + console.warn(`BotFrameworkAdapter.processActivity(): ${status} ERROR - ${body.toString()}`); + throw new Error(body.toString()); + } } /** @@ -602,70 +577,45 @@ export class BotFrameworkAdapter extends BotAdapter { * @param context Context for the current turn of conversation with the user. * @param activities List of activities to send. */ - public sendActivities(context: TurnContext, activities: Partial[]): Promise { - return new Promise((resolve: any, reject: any): void => { - const responses: ResourceResponse[] = []; - const that: BotFrameworkAdapter = this; - function next(i: number): void { - if (i < activities.length) { - try { - const activity: Partial = activities[i]; - switch (activity.type) { - case 'delay': - setTimeout( - () => { - responses.push({} as ResourceResponse); - next(i + 1); - }, - typeof activity.value === 'number' ? activity.value : 1000 - ); - break; - case 'invokeResponse': - // Cache response to context object. This will be retrieved when turn completes. - context.turnState.set(INVOKE_RESPONSE_KEY, activity); - responses.push({} as ResourceResponse); - next(i + 1); - break; - default: - if (!activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.sendActivity(): missing serviceUrl.`); } - if (!activity.conversation || !activity.conversation.id) { - throw new Error(`BotFrameworkAdapter.sendActivity(): missing conversation id.`); - } - let p: Promise; - const client: ConnectorClient = that.createConnectorClient(activity.serviceUrl); - if (activity.type === 'trace' && activity.channelId !== 'emulator') { - // Just eat activity - p = Promise.resolve({} as ResourceResponse); - } else if (activity.replyToId) { - p = client.conversations.replyToActivity( - activity.conversation.id, - activity.replyToId, - activity as Activity - ); - } else { - p = client.conversations.sendToConversation( - activity.conversation.id, - activity as Activity - ); - } - p.then( - (response: ResourceResponse) => { - responses.push(response); - next(i + 1); - }, - reject - ); - break; - } - } catch (err) { - reject(err); + public async sendActivities(context: TurnContext, activities: Partial[]): Promise { + const responses: ResourceResponse[] = []; + for (let i = 0; i < activities.length; i++) { + const activity: Partial = activities[i]; + switch (activity.type) { + case 'delay': + await delay(typeof activity.value === 'number' ? activity.value : 1000); + responses.push({} as ResourceResponse); + break; + case 'invokeResponse': + // Cache response to context object. This will be retrieved when turn completes. + context.turnState.set(INVOKE_RESPONSE_KEY, activity); + responses.push({} as ResourceResponse); + break; + default: + if (!activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.sendActivity(): missing serviceUrl.`); } + if (!activity.conversation || !activity.conversation.id) { + throw new Error(`BotFrameworkAdapter.sendActivity(): missing conversation id.`); } - } else { - resolve(responses); - } + const client: ConnectorClient = this.createConnectorClient(activity.serviceUrl); + if (activity.type === 'trace' && activity.channelId !== 'emulator') { + // Just eat activity + responses.push({} as ResourceResponse); + } else if (activity.replyToId) { + responses.push(await client.conversations.replyToActivity( + activity.conversation.id, + activity.replyToId, + activity as Activity + )); + } else { + responses.push(await client.conversations.sendToConversation( + activity.conversation.id, + activity as Activity + )); + } + break; } - next(0); - }); + } + return responses; } /** @@ -679,25 +629,18 @@ export class BotFrameworkAdapter extends BotAdapter { * @param context Context for the current turn of conversation with the user. * @param activity New activity to replace a current activity with. */ - public updateActivity(context: TurnContext, activity: Partial): Promise { - try { - if (!activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.updateActivity(): missing serviceUrl`); } - if (!activity.conversation || !activity.conversation.id) { - throw new Error(`BotFrameworkAdapter.updateActivity(): missing conversation or conversation.id`); - } - if (!activity.id) { throw new Error(`BotFrameworkAdapter.updateActivity(): missing activity.id`); } - const client: ConnectorClient = this.createConnectorClient(activity.serviceUrl); - - return client.conversations.updateActivity( - activity.conversation.id, - activity.id, - activity as Activity - ).then(() => { - // noop - }); - } catch (err) { - return Promise.reject(err); + public async updateActivity(context: TurnContext, activity: Partial): Promise { + if (!activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.updateActivity(): missing serviceUrl`); } + if (!activity.conversation || !activity.conversation.id) { + throw new Error(`BotFrameworkAdapter.updateActivity(): missing conversation or conversation.id`); } + if (!activity.id) { throw new Error(`BotFrameworkAdapter.updateActivity(): missing activity.id`); } + const client: ConnectorClient = this.createConnectorClient(activity.serviceUrl); + await client.conversations.updateActivity( + activity.conversation.id, + activity.id, + activity as Activity + ); } /** @@ -705,14 +648,13 @@ export class BotFrameworkAdapter extends BotAdapter { * @param request Received request. * @param authHeader Received authentication header. */ - protected authenticateRequest(request: Partial, authHeader: string): Promise { - return JwtTokenValidation.authenticateRequest( + protected async authenticateRequest(request: Partial, authHeader: string): Promise { + const claims = await JwtTokenValidation.authenticateRequest( request as Activity, authHeader, this.credentialsProvider, this.settings.channelService - ).then((claims: ClaimsIdentity) => { - if (!claims.isAuthenticated) { throw new Error('Unauthorized Access. Request is not authorized'); } - }); + ); + if (!claims.isAuthenticated) { throw new Error('Unauthorized Access. Request is not authorized'); } } /** @@ -803,3 +745,9 @@ function parseRequest(req: WebRequest): Promise { } }); } + +function delay(timeout: number): Promise { + return new Promise((resolve, reject) => { + setTimeout(resolve, timeout); + }); +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/botFrameworkAdapter.test.js b/libraries/botbuilder/tests/botFrameworkAdapter.test.js index 9ac10f6090..d4bc2fd90d 100644 --- a/libraries/botbuilder/tests/botFrameworkAdapter.test.js +++ b/libraries/botbuilder/tests/botFrameworkAdapter.test.js @@ -170,7 +170,7 @@ describe(`BotFrameworkAdapter`, function () { called = true; }).then(() => { assert(called, `bot logic not called.`); - assertResponse(res, 202); + assertResponse(res, 200); done(); }); }); @@ -185,7 +185,7 @@ describe(`BotFrameworkAdapter`, function () { called = true; }).then(() => { assert(called, `bot logic not called.`); - assertResponse(res, 202); + assertResponse(res, 200); done(); }); }); @@ -207,7 +207,7 @@ describe(`BotFrameworkAdapter`, function () { called = true; }).then(() => { assert(called, `bot logic not called.`); - assertResponse(res, 202); + assertResponse(res, 200); done(); }); }); @@ -222,7 +222,7 @@ describe(`BotFrameworkAdapter`, function () { assert(false, `shouldn't have passed.`); }, (err) => { assert(err, `error not returned.`); - assertResponse(res, 500, true); + assertResponse(res, 400, true); done(); }); }); @@ -237,7 +237,7 @@ describe(`BotFrameworkAdapter`, function () { assert(false, `shouldn't have passed.`); }, (err) => { assert(err, `error not returned.`); - assertResponse(res, 500, true); + assertResponse(res, 400, true); done(); }); }); From e7be7d68ee37f4cb0b2a3e3bf6c6bb3950d28ce5 Mon Sep 17 00:00:00 2001 From: Steven Ickman Date: Sat, 13 Oct 2018 19:13:31 -0700 Subject: [PATCH 2/4] Added hooks to listen for incoming and outgoing activities --- .../botbuilder/src/botFrameworkAdapter.ts | 87 +++++++++++++------ 1 file changed, 62 insertions(+), 25 deletions(-) diff --git a/libraries/botbuilder/src/botFrameworkAdapter.ts b/libraries/botbuilder/src/botFrameworkAdapter.ts index 7454836212..436a052325 100644 --- a/libraries/botbuilder/src/botFrameworkAdapter.ts +++ b/libraries/botbuilder/src/botFrameworkAdapter.ts @@ -197,14 +197,13 @@ export class BotFrameworkAdapter extends BotAdapter { * @param logic A function handler that will be called to perform the bots logic after the the adapters middleware has been run. */ public async continueConversation(reference: Partial, logic: (context: TurnContext) => Promise): Promise { - const request: Partial = TurnContext.applyConversationReference( + const activity: Partial = TurnContext.applyConversationReference( {type: 'event', name: 'continueConversation' }, reference, true ); - const context: TurnContext = this.createContext(request); - - await this.runMiddleware(context, logic as any); + const context: TurnContext = this.createContext(activity); + await this.onIncomingActivity(context, logic); } /** @@ -258,17 +257,17 @@ export class BotFrameworkAdapter extends BotAdapter { const response = await client.conversations.createConversation(params); // Initialize request and copy over new conversation ID and updated serviceUrl. - const request: Partial = TurnContext.applyConversationReference( + const activity: Partial = TurnContext.applyConversationReference( {type: 'event', name: 'createConversation' }, reference, true ); - request.conversation = { id: response.id } as ConversationAccount; - if (response.serviceUrl) { request.serviceUrl = response.serviceUrl; } + activity.conversation = { id: response.id } as ConversationAccount; + if (response.serviceUrl) { activity.serviceUrl = response.serviceUrl; } // Create context and run middleware - const context: TurnContext = this.createContext(request); - await this.runMiddleware(context, logic as any); + const context: TurnContext = this.createContext(activity); + await this.onIncomingActivity(context, logic); } /** @@ -518,20 +517,20 @@ export class BotFrameworkAdapter extends BotAdapter { try { // Parse body of request status = 400; - const request = await parseRequest(req); + const activity = await parseRequest(req); // Authenticate the incoming request status = 401; const authHeader: string = req.headers.authorization || req.headers.Authorization || ''; - await this.authenticateRequest(request, authHeader); + await this.authenticateRequest(activity, authHeader); // Process received activity status = 500; - const context: TurnContext = this.createContext(request); - await this.runMiddleware(context, logic); + const context: TurnContext = this.createContext(activity); + await this.onIncomingActivity(context, logic) // Retrieve cached invoke response. - if (request.type === ActivityTypes.Invoke) { + if (activity.type === ActivityTypes.Invoke) { const invokeResponse: any = context.turnState.get(INVOKE_RESPONSE_KEY); if (invokeResponse && invokeResponse.value) { const value: InvokeResponse = invokeResponse.value; @@ -596,21 +595,12 @@ export class BotFrameworkAdapter extends BotAdapter { if (!activity.conversation || !activity.conversation.id) { throw new Error(`BotFrameworkAdapter.sendActivity(): missing conversation id.`); } - const client: ConnectorClient = this.createConnectorClient(activity.serviceUrl); if (activity.type === 'trace' && activity.channelId !== 'emulator') { // Just eat activity responses.push({} as ResourceResponse); - } else if (activity.replyToId) { - responses.push(await client.conversations.replyToActivity( - activity.conversation.id, - activity.replyToId, - activity as Activity - )); } else { - responses.push(await client.conversations.sendToConversation( - activity.conversation.id, - activity as Activity - )); + const response = await this.onOutgoingActivity(context, activity); + responses.push(response); } break; } @@ -643,6 +633,53 @@ export class BotFrameworkAdapter extends BotAdapter { ); } + /** + * Called anytime the adapter receives an incoming activity. + * + * @remarks + * The default implementation calls `this.runMiddleware()`. + * @param context Context for the current turn of conversation with the user. + * @param logic A function handler that will be called to perform the bots logic after the received activity has been pre-processed by the adapter and routed through any middleware for processing. + */ + protected async onIncomingActivity(context: TurnContext, logic: (context: TurnContext) => Promise): Promise { + await this.runMiddleware(context, logic); + } + + /** + * Called anytime the adapter is about to send an activity to a channel server. + * + * @remarks + * The default implementation calls `this.sendActivity()`. + * @param context Context for the current turn of conversation with the user. + * @param activity The activity being sent. + */ + protected async onOutgoingActivity(context: TurnContext, activity: Partial): Promise { + return await this.sendActivity(activity); + } + + /** + * Sends an activity to the channel server. + * @param activity The activity to send. + */ + protected async sendActivity(activity: Partial): Promise { + let response: ResourceResponse; + const client: ConnectorClient = this.createConnectorClient(activity.serviceUrl); + if (activity.replyToId) { + response = await client.conversations.replyToActivity( + activity.conversation.id, + activity.replyToId, + activity as Activity + ); + } else { + response = await client.conversations.sendToConversation( + activity.conversation.id, + activity as Activity + ); + } + + return response; + } + /** * Allows for the overriding of authentication in unit tests. * @param request Received request. From 13ad1fc0a377496ccdebd226adae59a0de2de7d1 Mon Sep 17 00:00:00 2001 From: Steven Ickman Date: Sat, 13 Oct 2018 19:47:34 -0700 Subject: [PATCH 3/4] Added TurnContext.processActivity() --- libraries/botbuilder-core/src/turnContext.ts | 46 +++++++++++++++++++ .../botbuilder/src/botFrameworkAdapter.ts | 29 +++++++----- 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/libraries/botbuilder-core/src/turnContext.ts b/libraries/botbuilder-core/src/turnContext.ts index 6a92dca151..a31d3599d5 100644 --- a/libraries/botbuilder-core/src/turnContext.ts +++ b/libraries/botbuilder-core/src/turnContext.ts @@ -8,6 +8,7 @@ import { Activity, ActivityTypes, ConversationReference, InputHints, ResourceResponse } from 'botframework-schema'; import { BotAdapter } from './botAdapter'; import { shallowCopy } from './internal'; +import { type } from 'os'; /** * Signature implemented by functions registered with `context.onSendActivity()`. @@ -44,6 +45,19 @@ export type DeleteActivityHandler = ( next: () => Promise ) => Promise; +/** + * Signature implemented by functions registered with `context.onProcessActivity()`. + * + * ```TypeScript + * type ProcessActivityHandler = (context: TurnContext, reference: Partial, next: () => Promise) => Promise; + * ``` + */ +export type ProcessActivityHandler = ( + context: TurnContext, + activity: Partial, + next: () => Promise +) => Promise; + // tslint:disable-next-line:no-empty-interface export interface TurnContext {} @@ -62,6 +76,7 @@ export class TurnContext { private _onSendActivities: SendActivitiesHandler[] = []; private _onUpdateActivity: UpdateActivityHandler[] = []; private _onDeleteActivity: DeleteActivityHandler[] = []; + private _onProcessActivity: ProcessActivityHandler[] = []; /** * Creates a new TurnContext instance. @@ -140,6 +155,23 @@ export class TurnContext { return activity; } + /** + * Sends a new activity through the adapters middleware stack and bot logic as if it was just + * received. + * + * @remarks + * By default the activity will be addressed as if it was sent from the current user for the + * current conversation. + * @param activity New activity to process. + */ + public processActivity(activity: Partial): Promise { + const ref: Partial = TurnContext.getConversationReference(this.activity); + const incoming: Partial = TurnContext.applyConversationReference({...activity}, ref, true); + if (!incoming.type) { incoming.type = ActivityTypes.Message; } + + return this.emit(this._onProcessActivity, incoming, () => Promise.resolve()); + } + /** * Sends a single activity or message to the user. * @@ -256,6 +288,20 @@ export class TurnContext { return this.emit(this._onDeleteActivity, reference, () => this.adapter.deleteActivity(this, reference)); } + /** + * Registers a handler to be notified of, and potentially intercept, the processing of an activity. + * + * @remarks + * Adapters should register for this notification to detect and process calls to + * [processActivity()](#processactivity). + * @param handler A function that will be called anytime [processActivity()](#processactivity) is called. The handler should call `next()` to continue processing of the activity. + */ + public onProcessActivity(handler: ProcessActivityHandler): this { + this._onProcessActivity.push(handler); + + return this; + } + /** * Registers a handler to be notified of, and potentially intercept, the sending of activities. * diff --git a/libraries/botbuilder/src/botFrameworkAdapter.ts b/libraries/botbuilder/src/botFrameworkAdapter.ts index 436a052325..1f9b8525ae 100644 --- a/libraries/botbuilder/src/botFrameworkAdapter.ts +++ b/libraries/botbuilder/src/botFrameworkAdapter.ts @@ -202,8 +202,7 @@ export class BotFrameworkAdapter extends BotAdapter { reference, true ); - const context: TurnContext = this.createContext(activity); - await this.onIncomingActivity(context, logic); + await this.dispatchActivity(activity, logic); } /** @@ -265,9 +264,8 @@ export class BotFrameworkAdapter extends BotAdapter { activity.conversation = { id: response.id } as ConversationAccount; if (response.serviceUrl) { activity.serviceUrl = response.serviceUrl; } - // Create context and run middleware - const context: TurnContext = this.createContext(activity); - await this.onIncomingActivity(context, logic); + // Dispatch activity + await this.dispatchActivity(activity, logic); } /** @@ -524,10 +522,9 @@ export class BotFrameworkAdapter extends BotAdapter { const authHeader: string = req.headers.authorization || req.headers.Authorization || ''; await this.authenticateRequest(activity, authHeader); - // Process received activity + // Dispatch activity status = 500; - const context: TurnContext = this.createContext(activity); - await this.onIncomingActivity(context, logic) + const context = await this.dispatchActivity(activity, logic); // Retrieve cached invoke response. if (activity.type === ActivityTypes.Invoke) { @@ -737,10 +734,20 @@ export class BotFrameworkAdapter extends BotAdapter { /** * Allows for the overriding of the context object in unit tests and derived adapters. - * @param request Received request. + * @param activity Received request. */ - protected createContext(request: Partial): TurnContext { - return new TurnContext(this as any, request); + protected createContext(activity: Partial): TurnContext { + return new TurnContext(this as any, activity); + } + + private async dispatchActivity(activity: Partial, logic: (context: TurnContext) => Promise): Promise { + const context: TurnContext = this.createContext(activity); + context.onProcessActivity(async (ctx, a, next) => { + await next(); + await this.dispatchActivity(a, logic); + }); + await this.onIncomingActivity(context, logic); + return context; } } From b0e9cad7891600d2950efb30535f1ebb1d73885f Mon Sep 17 00:00:00 2001 From: Steven Ickman Date: Tue, 16 Oct 2018 12:09:13 -0700 Subject: [PATCH 4/4] Renamed createContext() to onCreateContext() --- .../botbuilder/src/botFrameworkAdapter.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/libraries/botbuilder/src/botFrameworkAdapter.ts b/libraries/botbuilder/src/botFrameworkAdapter.ts index 1f9b8525ae..9664e84c50 100644 --- a/libraries/botbuilder/src/botFrameworkAdapter.ts +++ b/libraries/botbuilder/src/botFrameworkAdapter.ts @@ -630,6 +630,17 @@ export class BotFrameworkAdapter extends BotAdapter { ); } + /** + * Called when a context object for an activity is being created. + * + * @remarks + * Derived classes can override this to create their own extended context object. + * @param activity Received request. + */ + protected onCreateContext(activity: Partial): TurnContext { + return new TurnContext(this as any, activity); + } + /** * Called anytime the adapter receives an incoming activity. * @@ -732,16 +743,8 @@ export class BotFrameworkAdapter extends BotAdapter { } } - /** - * Allows for the overriding of the context object in unit tests and derived adapters. - * @param activity Received request. - */ - protected createContext(activity: Partial): TurnContext { - return new TurnContext(this as any, activity); - } - private async dispatchActivity(activity: Partial, logic: (context: TurnContext) => Promise): Promise { - const context: TurnContext = this.createContext(activity); + const context: TurnContext = this.onCreateContext(activity); context.onProcessActivity(async (ctx, a, next) => { await next(); await this.dispatchActivity(a, logic);