UNPKG

botbuilder

Version:

Bot Builder is a framework for building rich bots on virtually any platform.

236 lines (201 loc) • 8.87 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { ClaimsIdentity, JwtTokenValidation } from 'botframework-connector'; import { v4 as uuid } from 'uuid'; import { Activity, ActivityTypes, BotAdapter, CallerIdConstants, ChannelAccount, ResourceResponse, SkillConversationIdFactoryBase, SkillConversationReference, TurnContext, } from 'botbuilder-core'; /** * @internal */ export class SkillHandlerImpl { /** * @internal */ constructor( private readonly skillConversationReferenceKey: symbol, private readonly adapter: BotAdapter, private readonly logic: (context: TurnContext) => Promise<void>, private readonly conversationIdFactory: SkillConversationIdFactoryBase, private readonly getOauthScope: () => string | undefined = () => undefined, ) {} /** * @internal */ onSendToConversation( claimsIdentity: ClaimsIdentity, conversationId: string, activity: Activity, ): Promise<ResourceResponse> { return this.processActivity(claimsIdentity, conversationId, null, activity); } /** * @internal */ onReplyToActivity( claimsIdentity: ClaimsIdentity, conversationId: string, activityId: string, activity: Activity, ): Promise<ResourceResponse> { return this.processActivity(claimsIdentity, conversationId, activityId, activity); } /** * @internal */ async onUpdateActivity( claimsIdentity: ClaimsIdentity, conversationId: string, activityId: string, activity: Activity, ): Promise<ResourceResponse> { let resourceResponse: ResourceResponse | void; await this.continueConversation(claimsIdentity, conversationId, async (context, ref) => { const newActivity = TurnContext.applyConversationReference(activity, ref.conversationReference); context.activity.id = activityId; context.activity.callerId = `${CallerIdConstants.BotToBotPrefix}${JwtTokenValidation.getAppIdFromClaims( claimsIdentity.claims, )}`; resourceResponse = await context.updateActivity(newActivity); }); return resourceResponse ? resourceResponse : { id: activityId }; } /** * @internal */ async onDeleteActivity(claimsIdentity: ClaimsIdentity, conversationId: string, activityId: string): Promise<void> { return this.continueConversation(claimsIdentity, conversationId, (context) => context.deleteActivity(activityId), ); } /** * @internal */ async onGetMember(claimsIdentity: ClaimsIdentity, userId: string, conversationId: string): Promise<ChannelAccount> { let member: ChannelAccount = null; await this.continueConversation(claimsIdentity, conversationId, async (context) => { const client = context.turnState.get(context.adapter.ConnectorClientKey); const conversationId = context.activity.conversation.id; member = await client.conversations.getConversationMember(conversationId, userId); }); return member; } private async getSkillConversationReference(conversationId: string): Promise<SkillConversationReference> { let skillConversationReference: SkillConversationReference; try { skillConversationReference = await this.conversationIdFactory.getSkillConversationReference(conversationId); } catch (err) { // If the factory has overridden getSkillConversationReference, call the deprecated getConversationReference(). // In this scenario, the oAuthScope paired with the ConversationReference can only be used for talking with // an official channel, not another bot. if (err.message === 'NotImplemented') { skillConversationReference = { conversationReference: await this.conversationIdFactory.getConversationReference(conversationId), oAuthScope: this.getOauthScope(), }; } else { throw err; } } if (!skillConversationReference) { throw new Error('skillConversationReference not found'); } else if (!skillConversationReference.conversationReference) { throw new Error('conversationReference not found.'); } return skillConversationReference; } private async processActivity( claimsIdentity: ClaimsIdentity, conversationId: string, replyToActivityId: string, activity: Activity, ): Promise<ResourceResponse> { let resourceResponse: ResourceResponse; await this.continueConversation(claimsIdentity, conversationId, async (context, ref) => { /** * This callback does the following: * - Applies the correct ConversationReference to the Activity for sending to the user-router conversation. * - For EndOfConversation Activities received from the Skill, removes the ConversationReference from the * ConversationIdFactory */ const newActivity = TurnContext.applyConversationReference(activity, ref.conversationReference); context.activity.id = replyToActivityId; context.activity.callerId = `${CallerIdConstants.BotToBotPrefix}${JwtTokenValidation.getAppIdFromClaims( claimsIdentity.claims, )}`; switch (newActivity.type) { case ActivityTypes.EndOfConversation: await this.conversationIdFactory.deleteConversationReference(conversationId); this.applySkillActivityToTurnContext(context, newActivity); await this.logic(context); break; case ActivityTypes.Event: this.applySkillActivityToTurnContext(context, newActivity); await this.logic(context); break; default: resourceResponse = await context.sendActivity(newActivity); break; } }); if (!resourceResponse) { resourceResponse = { id: uuid() }; } return resourceResponse; } private async continueConversation( claimsIdentity: ClaimsIdentity, conversationId: string, callback: (context: TurnContext, ref: SkillConversationReference) => Promise<void>, ): Promise<void> { const ref = await this.getSkillConversationReference(conversationId); const continueCallback = (context: TurnContext): Promise<void> => { context.turnState.set(this.skillConversationReferenceKey, ref); return callback(context, ref); }; try { await this.adapter.continueConversationAsync( claimsIdentity, ref.conversationReference, ref.oAuthScope, continueCallback, ); } catch (err) { if (err.message === 'NotImplemented') { // We're in the legacy scenario where our adapter does not support passing through claims/audience // explicitly. Stash it in turn context and hope for the best! await this.adapter.continueConversation(ref.conversationReference, async (context) => { context.turnState.set(context.adapter.BotIdentityKey, claimsIdentity); return continueCallback(context); }); } else { throw err; } } } // adapter.continueConversation() sends an event activity with continueConversation in the name. // this warms up the incoming middlewares but once that's done and we hit the custom callback, // we need to swap the values back to the ones received from the skill so the bot gets the actual activity. private applySkillActivityToTurnContext(context: TurnContext, activity: Partial<Activity>): void { context.activity.channelData = activity.channelData; context.activity.code = activity.code; context.activity.entities = activity.entities; context.activity.locale = activity.locale; context.activity.localTimestamp = activity.localTimestamp; context.activity.name = activity.name; context.activity.relatesTo = activity.relatesTo; context.activity.replyToId = activity.replyToId; context.activity.timestamp = activity.timestamp; context.activity.text = activity.text; context.activity.type = activity.type; context.activity.value = activity.value; } }