Skip to content

Commit

Permalink
Johtaylo/skills protocol (#1472)
Browse files Browse the repository at this point in the history
* init commit

* controller equivalent in typescript
  • Loading branch information
johnataylor authored Dec 5, 2019
1 parent 6775828 commit 1f236f3
Show file tree
Hide file tree
Showing 6 changed files with 346 additions and 51 deletions.
1 change: 1 addition & 0 deletions libraries/botbuilder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"dependencies": {
"@azure/ms-rest-js": "1.2.6",
"@types/node": "^10.12.18",
"axios": "^0.19.0",
"botbuilder-core": "4.1.6",
"botframework-connector": "4.1.6",
"botframework-streaming": "4.1.6",
Expand Down
7 changes: 7 additions & 0 deletions libraries/botbuilder/src/botFrameworkAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ export enum StatusCodes {
NOT_IMPLEMENTED = 501,
}

export class StatusCodeError extends Error {
constructor(public readonly statusCode: StatusCodes, message?: string) {
super(message);
this.statusCode = statusCode;
}
}

/**
* Represents an Express or Restify request object.
*
Expand Down
52 changes: 22 additions & 30 deletions libraries/botbuilder/src/botFrameworkHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
* Licensed under the MIT License.
*/

import { HttpClient, WebResource } from '@azure/ms-rest-js';
import {
AuthenticationConstants,
GovernmentConstants,
ICredentialProvider,
JwtTokenValidation,
MicrosoftAppCredentials
} from 'botframework-connector';

import axios from 'axios';

import { Activity } from 'botbuilder-core';

import { InvokeResponse, USER_AGENT } from './botFrameworkAdapter';
Expand All @@ -28,26 +30,18 @@ export class BotFrameworkHttpClient {
*/
private static readonly appCredentialMapCache: Map<string, MicrosoftAppCredentials> = new Map<string, MicrosoftAppCredentials>();

constructor(
private readonly credentialProvider: ICredentialProvider,
private readonly channelService?: string,
private readonly httpClient?: HttpClient,
) {
if (!this.channelService) {
this.channelService = process.env[AuthenticationConstants.ChannelService];
}
if (!this.httpClient) {
throw new Error('BotFrameworkHttpClient(): missing httpClient');
}
constructor(private readonly credentialProvider: ICredentialProvider, private readonly channelService?: string) {
if (!this.credentialProvider) {
throw new Error('BotFrameworkHttpClient(): missing credentialProvider');
}
if (!this.channelService) {
this.channelService = process.env[AuthenticationConstants.ChannelService];
}
}

/**
* Forwards an activity to a skill (bot).
* Forwards an activity to a another bot.
* @remarks
* NOTE: Forwarding an activity to a skill will flush UserState and ConversationState changes so that skill has accurate state.
*
* @param fromBotId The MicrosoftAppId of the bot sending the activity.
* @param toBotId The MicrosoftAppId of the bot receiving the activity.
Expand All @@ -59,11 +53,11 @@ export class BotFrameworkHttpClient {
public async postActivity(fromBotId: string, toBotId: string, toUrl: string, serviceUrl: string, conversationId: string, activity: Activity): Promise<InvokeResponse> {
const appCredentials = await this.getAppCredentials(fromBotId, toBotId);
if (!appCredentials) {
throw new Error('Unable to get appCredentials to connect to the skill');
throw new Error('BotFrameworkHttpClient.postActivity(): Unable to get appCredentials to connect to the skill');
}

// Get token for the skill call
const token = await appCredentials.getToken();
const token = appCredentials.appId === '' && appCredentials.appPassword === '' ? null : await appCredentials.getToken();

// Capture current activity settings before changing them.
// TODO: DO we need to set the activity ID? (events that are created manually don't have it).
Expand All @@ -72,23 +66,21 @@ export class BotFrameworkHttpClient {
try {
activity.conversation.id = conversationId;
activity.serviceUrl = serviceUrl;
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${ token }`,
'Content-Type': 'application/json',
'User-Agent': USER_AGENT
const config = {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': USER_AGENT
}
};

const request = new WebResource(
toUrl,
'POST',
activity,
undefined,
headers
);
if (token !== null) {
config.headers['Authorization'] = `Bearer ${ token }`;
}

const response = await axios.post(toUrl, activity, config);

const response = await this.httpClient.sendRequest(request);
const invokeResponse: InvokeResponse = { status: response.status, body: response.parsedBody };
const invokeResponse: InvokeResponse = { status: response.status, body: response.data };

return invokeResponse;
} finally {
Expand Down
55 changes: 34 additions & 21 deletions libraries/botbuilder/src/channelServiceHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
ICredentialProvider,
JwtTokenValidation
} from 'botframework-connector';
import { StatusCodes } from './botFrameworkAdapter';
import { StatusCodeError, StatusCodes } from './botFrameworkAdapter';

/**
* The ChannelServiceHandler implements API to forward activity to a skill and
Expand Down Expand Up @@ -79,17 +79,17 @@ export class ChannelServiceHandler {
return await this.onGetActivityMembers(claimsIdentity, conversationId, activityId);
}

public async handleCreateConversation(authHeader: string, conversationId: string, parameters: ConversationParameters): Promise<ConversationResourceResponse> {
public async handleCreateConversation(authHeader: string, parameters: ConversationParameters): Promise<ConversationResourceResponse> {
const claimsIdentity = await this.authenticate(authHeader);
return await this.onCreateConversation(claimsIdentity, conversationId, parameters);
return await this.onCreateConversation(claimsIdentity, parameters);
}

public async handleGetConversations(authHeader: string, conversationId: string, continuationToken?: string /* some default */): Promise<ConversationsResult> {
const claimsIdentity = await this.authenticate(authHeader);
return await this.onGetConversations(claimsIdentity, conversationId, continuationToken);
}

public async HandleGetConversationMembers(authHeader: string, conversationId: string): Promise<ChannelAccount[]> {
public async handleGetConversationMembers(authHeader: string, conversationId: string): Promise<ChannelAccount[]> {
const claimsIdentity = await this.authenticate(authHeader);
return await this.onGetConversationMembers(claimsIdentity, conversationId);
}
Expand Down Expand Up @@ -163,7 +163,7 @@ export class ChannelServiceHandler {
* @param activity Activity to send.
*/
protected async onReplyToActivity(claimsIdentity: ClaimsIdentity, conversationId: string, activityId: string, activity: Activity): Promise<ResourceResponse> {
throw new Error(`ChannelServiceHandler.onReplyToActivity(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
throw new StatusCodeError(StatusCodes.NOT_IMPLEMENTED, `ChannelServiceHandler.onReplyToActivity(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
}

/**
Expand All @@ -181,7 +181,7 @@ export class ChannelServiceHandler {
* @param activity replacement Activity.
*/
protected async onUpdateActivity(claimsIdentity: ClaimsIdentity, conversationId: string, activityId: string, activity: Activity): Promise<ResourceResponse> {
throw new Error(`ChannelServiceHandler.onUpdateActivity(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
throw new StatusCodeError(StatusCodes.NOT_IMPLEMENTED, `ChannelServiceHandler.onUpdateActivity(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
}

/**
Expand All @@ -198,7 +198,7 @@ export class ChannelServiceHandler {
* @param activityId activityId to delete.
*/
protected async onDeleteActivity(claimsIdentity: ClaimsIdentity, conversationId: string, activityId: string): Promise<void> {
throw new Error(`ChannelServiceHandler.onDeleteActivity(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
throw new StatusCodeError(StatusCodes.NOT_IMPLEMENTED, `ChannelServiceHandler.onDeleteActivity(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
}

/**
Expand All @@ -213,9 +213,8 @@ export class ChannelServiceHandler {
* @param conversationId Conversation ID.
* @param activityId Activity ID.
*/
protected async onGetActivityMembers(claimsIdentity: ClaimsIdentity, conversationId: string, activityId: string): Promise<ChannelAccount[]>
{
throw new Error(`ChannelServiceHandler.onGetActivityMembers(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
protected async onGetActivityMembers(claimsIdentity: ClaimsIdentity, conversationId: string, activityId: string): Promise<ChannelAccount[]> {
throw new StatusCodeError(StatusCodes.NOT_IMPLEMENTED, `ChannelServiceHandler.onGetActivityMembers(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
}

/**
Expand All @@ -237,8 +236,8 @@ export class ChannelServiceHandler {
* @param conversationId Conversation ID.
* @param parameters Parameters to create the conversation from.
*/
protected async onCreateConversation(claimsIdentity: ClaimsIdentity, conversationId: string, parameters: ConversationParameters): Promise<ConversationResourceResponse> {
throw new Error(`ChannelServiceHandler.onCreateConversation(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
protected async onCreateConversation(claimsIdentity: ClaimsIdentity, parameters: ConversationParameters): Promise<ConversationResourceResponse> {
throw new StatusCodeError(StatusCodes.NOT_IMPLEMENTED, `ChannelServiceHandler.onCreateConversation(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
}

/**
Expand All @@ -260,7 +259,7 @@ export class ChannelServiceHandler {
* @param continuationToken Skip or continuation token.
*/
protected async onGetConversations(claimsIdentity: ClaimsIdentity, conversationId: string, continuationToken?: string): Promise<ConversationsResult> {
throw new Error(`ChannelServiceHandler.onGetConversations(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
throw new StatusCodeError(StatusCodes.NOT_IMPLEMENTED, `ChannelServiceHandler.onGetConversations(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
}

/**
Expand All @@ -274,7 +273,7 @@ export class ChannelServiceHandler {
* @param conversationId Conversation ID.
*/
protected async onGetConversationMembers(claimsIdentity: ClaimsIdentity, conversationId: string): Promise<ChannelAccount[]> {
throw new Error(`ChannelServiceHandler.onGetConversationMembers(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
throw new StatusCodeError(StatusCodes.NOT_IMPLEMENTED, `ChannelServiceHandler.onGetConversationMembers(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
}

/**
Expand Down Expand Up @@ -302,9 +301,8 @@ export class ChannelServiceHandler {
* @param pageSize Suggested page size.
* @param continuationToken Continuation Token.
*/
protected async onGetConversationPagedMembers(claimsIdentity: ClaimsIdentity, conversationId: string, pageSize: number = -1, continuationToken?: string): Promise<PagedMembersResult>
{
throw new Error(`ChannelServiceHandler.onGetConversationPagedMembers(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
protected async onGetConversationPagedMembers(claimsIdentity: ClaimsIdentity, conversationId: string, pageSize: number = -1, continuationToken?: string): Promise<PagedMembersResult> {
throw new StatusCodeError(StatusCodes.NOT_IMPLEMENTED, `ChannelServiceHandler.onGetConversationPagedMembers(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
}

/**
Expand All @@ -320,7 +318,7 @@ export class ChannelServiceHandler {
* @param memberId ID of the member to delete from this conversation.
*/
protected async onDeleteConversationMember(claimsIdentity: ClaimsIdentity, conversationId: string, memberId: string): Promise<void> {
throw new Error(`ChannelServiceHandler.onDeleteConversationMember(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
throw new StatusCodeError(StatusCodes.NOT_IMPLEMENTED, `ChannelServiceHandler.onDeleteConversationMember(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
}

/**
Expand All @@ -338,7 +336,7 @@ export class ChannelServiceHandler {
* @param transcript Transcript of activities.
*/
protected async onSendConversationHistory(claimsIdentity: ClaimsIdentity, conversationId: string, transcript: Transcript): Promise<ResourceResponse> {
throw new Error(`ChannelServiceHandler.onSendConversationHistory(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
throw new StatusCodeError(StatusCodes.NOT_IMPLEMENTED, `ChannelServiceHandler.onSendConversationHistory(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
}

/**
Expand All @@ -356,11 +354,26 @@ export class ChannelServiceHandler {
* @param attachmentUpload Attachment data.
*/
protected async onUploadAttachment(claimsIdentity: ClaimsIdentity, conversationId: string, attachmentUpload: AttachmentData): Promise<ResourceResponse> {
throw new Error(`ChannelServiceHandler.onUploadAttachment(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
throw new StatusCodeError(StatusCodes.NOT_IMPLEMENTED, `ChannelServiceHandler.onUploadAttachment(): ${StatusCodes.NOT_IMPLEMENTED}: ${STATUS_CODES[StatusCodes.NOT_IMPLEMENTED]}`);
}

private async authenticate(authHeader: string): Promise<ClaimsIdentity> {
return await JwtTokenValidation.validateAuthHeader(authHeader, this.credentialProvider, this.channelService, 'unknown', undefined, this.authConfig);
try {
if (!authHeader) {
const isAuthDisable = this.credentialProvider.isAuthenticationDisabled()
if (isAuthDisable) {
// In the scenario where Auth is disabled, we still want to have the
// IsAuthenticated flag set in the ClaimsIdentity. To do this requires
// adding in an empty claim.
return new ClaimsIdentity([], false);
}
}

return await JwtTokenValidation.validateAuthHeader(authHeader, this.credentialProvider, this.channelService, 'unknown', undefined, this.authConfig);
}
catch (err) {
throw new StatusCodeError(StatusCodes.UNAUTHORIZED);
}
}
}

Loading

0 comments on commit 1f236f3

Please sign in to comment.