Skip to content

Commit

Permalink
port: update and delete activities from skill handler (#2912)
Browse files Browse the repository at this point in the history
* Properly use RoleTypes

* Add skill roletype, cast to bandaid compiler bug?

* Port anonymous skill changes

* ChannelServiceHandler test

* AppCredentials tests

* SkillsValidation tests

* Fix ClaimsIdentity constructor

* Update/Delete activities from skills

Fixes #1995

* Add comment describing `isAuthenticated` override

* Fake response with activityId

* Stgum/q fixes (#2932)

* [PORT] DialogContextMemoryScope (#2895)

* added DialogContextMemoryScope

* removed setMemory implementation in dialog context memory scope

Co-authored-by: Josh Gummersall <[email protected]>
Co-authored-by: Steven Gum <[email protected]>

* exclude browser*.js from bf-connector .nycrc
* other minor cleanup in bf-connector

* Change vmImage to ubuntu for all JS pipelines (#2931)

* Switch to ubuntu vmimage

* Tweak sed commands

* Add ubuntu to the other builds

* Add var NoPublish

* Fix NoPublish logic

* Rename var to DoNotPublishPackages

* Remove DoNotPublishPackages functionality. Instead we limit automatic releases to main branch.

* Fix Package Names task

Co-authored-by: Steven Gum <[email protected]>

Co-authored-by: Zichuan Ma <[email protected]>
Co-authored-by: Josh Gummersall <[email protected]>
Co-authored-by: BruceHaley <[email protected]>

* Fix skill validation tests

* Fix assert message

* Fix test label

* Cleanup

Co-authored-by: Steven Gum <[email protected]>
Co-authored-by: Zichuan Ma <[email protected]>
Co-authored-by: BruceHaley <[email protected]>
  • Loading branch information
4 people authored Oct 21, 2020
1 parent 4357233 commit b685c12
Show file tree
Hide file tree
Showing 4 changed files with 440 additions and 260 deletions.
2 changes: 1 addition & 1 deletion libraries/botbuilder/src/botFrameworkAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1702,7 +1702,7 @@ export class BotFrameworkAdapter
* Override this in a derived class to modify how the adapter creates a turn context.
*/
protected createContext(request: Partial<Activity>): TurnContext {
return new TurnContext(this as any, request);
return new TurnContext(this, request);
}

/**
Expand Down
200 changes: 146 additions & 54 deletions libraries/botbuilder/src/skills/skillHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ import {
import { ChannelServiceHandler } from '../channelServiceHandler';
import { BotFrameworkAdapter } from '../botFrameworkAdapter';

/**
* Casts adapter to BotFrameworkAdapter only if necessary
* @param adapter adapter to maybe cast as BotFrameworkAdapter
*/
function maybeCastAdapter(adapter: BotAdapter): BotFrameworkAdapter {
return adapter instanceof BotFrameworkAdapter ? adapter : (adapter as BotFrameworkAdapter);
}

/**
* A Bot Framework Handler for skills.
*/
Expand All @@ -40,10 +48,7 @@ export class SkillHandler extends ChannelServiceHandler {
* @remarks
* The value is the same as the SkillConversationReferenceKey exported from botbuilder-core.
*/
public readonly SkillConversationReferenceKey: symbol = SkillConversationReferenceKey;
private readonly adapter: BotAdapter;
private readonly bot: ActivityHandlerBase;
private readonly conversationIdFactory: SkillConversationIdFactoryBase;
public readonly SkillConversationReferenceKey = SkillConversationReferenceKey;

/**
* Initializes a new instance of the SkillHandler class.
Expand All @@ -55,24 +60,26 @@ export class SkillHandler extends ChannelServiceHandler {
* @param channelService The string indicating if the bot is working in Public Azure or in Azure Government (https://aka.ms/AzureGovDocs).
*/
public constructor(
adapter: BotAdapter,
bot: ActivityHandlerBase,
conversationIdFactory: SkillConversationIdFactoryBase,
private readonly adapter: BotAdapter,
private readonly bot: ActivityHandlerBase,
private readonly conversationIdFactory: SkillConversationIdFactoryBase,
credentialProvider: ICredentialProvider,
authConfig: AuthenticationConfiguration,
channelService?: string
) {
super(credentialProvider, authConfig, channelService);

if (!adapter) {
throw new Error('missing adapter.');
}

if (!bot) {
throw new Error('missing bot.');
}

if (!conversationIdFactory) {
throw new Error('missing conversationIdFactory.');
}

this.adapter = adapter;
this.bot = bot;
this.conversationIdFactory = conversationIdFactory;
}

/**
Expand Down Expand Up @@ -136,7 +143,10 @@ export class SkillHandler extends ChannelServiceHandler {
/**
* @private
*/
private static applyEoCToTurnContextActivity(turnContext: TurnContext, endOfConversationActivity: Activity): void {
private static applyEoCToTurnContextActivity(
turnContext: TurnContext,
endOfConversationActivity: Partial<Activity>
): void {
// transform the turnContext.activity to be an EndOfConversation Activity.
turnContext.activity.type = endOfConversationActivity.type;
turnContext.activity.text = endOfConversationActivity.text;
Expand All @@ -154,7 +164,7 @@ export class SkillHandler extends ChannelServiceHandler {
/**
* @private
*/
private static applyEventToTurnContextActivity(turnContext: TurnContext, eventActivity: Activity): void {
private static applyEventToTurnContextActivity(turnContext: TurnContext, eventActivity: Partial<Activity>): void {
// transform the turnContext.activity to be an Event Activity.
turnContext.activity.type = eventActivity.type;
turnContext.activity.name = eventActivity.name;
Expand All @@ -173,13 +183,9 @@ export class SkillHandler extends ChannelServiceHandler {
/**
* @private
*/
private async processActivity(
claimsIdentity: ClaimsIdentity,
conversationId: string,
replyToActivityId: string,
activity: Activity
): Promise<ResourceResponse> {
private async getSkillConversationReference(conversationId: string): Promise<SkillConversationReference> {
let skillConversationReference: SkillConversationReference;

try {
skillConversationReference = await this.conversationIdFactory.getSkillConversationReference(conversationId);
} catch (err) {
Expand All @@ -202,71 +208,157 @@ export class SkillHandler extends ChannelServiceHandler {

if (!skillConversationReference) {
throw new Error('skillConversationReference not found');
}
if (!skillConversationReference.conversationReference) {
} else if (!skillConversationReference.conversationReference) {
throw new Error('conversationReference not found.');
}

return skillConversationReference;
}

/**
* Helper method for forwarding a conversation through the adapter
*/
private async continueConversation(
claimsIdentity: ClaimsIdentity,
conversationId: string,
callback: (adapter: BotFrameworkAdapter, ref: SkillConversationReference, context: TurnContext) => Promise<void>
): Promise<void> {
const ref = await this.getSkillConversationReference(conversationId);

// Add the channel service URL to the trusted services list so we can send messages back.
// the service URL for skills is trusted because it is applied based on the original request
// received by the root bot.
AppCredentials.trustServiceUrl(ref.conversationReference.serviceUrl);

return maybeCastAdapter(this.adapter).continueConversation(
ref.conversationReference,
ref.oAuthScope,
async (context: TurnContext): Promise<void> => {
const adapter = maybeCastAdapter(context.adapter);

// Cache the claimsIdentity and conversation reference
context.turnState.set(adapter.BotIdentityKey, claimsIdentity);
context.turnState.set(this.SkillConversationReferenceKey, ref);

return callback(adapter, ref, context);
}
);
}

private async processActivity(
claimsIdentity: ClaimsIdentity,
conversationId: string,
activityId: string,
activity: Activity
): Promise<ResourceResponse> {
// If an activity is sent, return the ResourceResponse
let resourceResponse: ResourceResponse;

/**
* Callback passed to the BotFrameworkAdapter.createConversation() call.
* This function does the following:
* - Caches the ClaimsIdentity on the TurnContext.turnState
* 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 callback = async (context: TurnContext): Promise<void> => {
const adapter: BotFrameworkAdapter = context.adapter as BotFrameworkAdapter;
// Cache the ClaimsIdentity and ConnectorClient on the context so that it's available inside of the bot's logic.
context.turnState.set(adapter.BotIdentityKey, claimsIdentity);
context.turnState.set(this.SkillConversationReferenceKey, skillConversationReference);
activity = TurnContext.applyConversationReference(
activity,
skillConversationReference.conversationReference
) as Activity;
const client = adapter.createConnectorClient(activity.serviceUrl);
context.turnState.set(adapter.ConnectorClientKey, client);

context.activity.id = replyToActivityId;
await this.continueConversation(claimsIdentity, conversationId, async (adapter, ref, context) => {
const newActivity = TurnContext.applyConversationReference(activity, ref.conversationReference);
context.activity.id = activityId;
context.activity.callerId = `${CallerIdConstants.BotToBotPrefix}${JwtTokenValidation.getAppIdFromClaims(
claimsIdentity.claims
)}`;

switch (activity.type) {

// Cache connector client in turn context
const client = adapter.createConnectorClient(newActivity.serviceUrl);
context.turnState.set(adapter.ConnectorClientKey, client);

switch (newActivity.type) {
case ActivityTypes.EndOfConversation:
await this.conversationIdFactory.deleteConversationReference(conversationId);
SkillHandler.applyEoCToTurnContextActivity(context, activity);
SkillHandler.applyEoCToTurnContextActivity(context, newActivity);
await this.bot.run(context);
break;
case ActivityTypes.Event:
SkillHandler.applyEventToTurnContextActivity(context, activity);
SkillHandler.applyEventToTurnContextActivity(context, newActivity);
await this.bot.run(context);
break;
default:
resourceResponse = await context.sendActivity(activity);
resourceResponse = await context.sendActivity(newActivity);
break;
}
};

// Add the channel service URL to the trusted services list so we can send messages back.
// the service URL for skills is trusted because it is applied based on the original request
// received by the root bot.
AppCredentials.trustServiceUrl(skillConversationReference.conversationReference.serviceUrl);

await (this.adapter as BotFrameworkAdapter).continueConversation(
skillConversationReference.conversationReference,
skillConversationReference.oAuthScope,
callback
);
});

if (!resourceResponse) {
resourceResponse = { id: uuid() };
}

return resourceResponse;
}

/**
*
* UpdateActivity() API for Skill.
* @remarks
* Edit an existing activity.
*
* Some channels allow you to edit an existing activity to reflect the new
* state of a bot conversation.
*
* For example, you can remove buttons after someone has clicked "Approve" button.
* @param claimsIdentity ClaimsIdentity for the bot, should have AudienceClaim, AppIdClaim and ServiceUrlClaim.
* @param conversationId Conversation ID.
* @param activityId activityId to update.
* @param activity replacement Activity.
*/
protected async onUpdateActivity(
claimsIdentity: ClaimsIdentity,
conversationId: string,
activityId: string,
activity: Activity
): Promise<ResourceResponse> {
await this.continueConversation(claimsIdentity, conversationId, async (adapter, ref, context) => {
const newActivity = TurnContext.applyConversationReference(activity, ref.conversationReference);

context.activity.id = activityId;
context.activity.callerId = `${CallerIdConstants.BotToBotPrefix}${JwtTokenValidation.getAppIdFromClaims(
claimsIdentity.claims
)}`;

return context.updateActivity(newActivity);
});

// Note: the original activity ID is passed back here to provide "behavioral" parity with the C# SDK. Due to
// some inconsistent method signatures, the proper response is not propagated back through `context.updateActivity`
// so we have to manually pass this value back.
return { id: activityId };
}

/**
* DeleteActivity() API for Skill.
* @remarks
* Delete an existing activity.
*
* Some channels allow you to delete an existing activity, and if successful
* this method will remove the specified activity.
*
*
* @param claimsIdentity ClaimsIdentity for the bot, should have AudienceClaim, AppIdClaim and ServiceUrlClaim.
* @param conversationId Conversation ID.
* @param activityId activityId to delete.
*/
protected async onDeleteActivity(
claimsIdentity: ClaimsIdentity,
conversationId: string,
activityId: string
): Promise<void> {
// Callback method handles deleting activity
return this.continueConversation(
claimsIdentity,
conversationId,
async (adapter, ref, context): Promise<void> => {
return context.deleteActivity(activityId);
}
);
}
}

// Helper function to generate an UUID.
Expand Down
Loading

0 comments on commit b685c12

Please sign in to comment.