UNPKG

7.83 kBPlain TextView Raw
1/**
2 * @module botbuilder
3 */
4/**
5 * Copyright (c) Microsoft Corporation. All rights reserved.
6 * Licensed under the MIT License.
7 */
8
9import axios from 'axios';
10import { Activity, BotFrameworkClient, ChannelAccount, InvokeResponse, RoleTypes } from 'botbuilder-core';
11
12import {
13 AppCredentials,
14 AuthenticationConstants,
15 ConversationConstants,
16 GovernmentConstants,
17 ICredentialProvider,
18 JwtTokenValidation,
19 MicrosoftAppCredentials,
20} from 'botframework-connector';
21
22import { USER_AGENT } from './botFrameworkAdapter';
23
24/**
25 * HttpClient for calling skills from a Node.js BotBuilder V4 SDK bot.
26 */
27export class BotFrameworkHttpClient implements BotFrameworkClient {
28 protected readonly channelService: string;
29
30 /**
31 * Cache for appCredentials to speed up token acquisition (a token is not requested unless is expired)
32 * AppCredentials are cached using appId + scope (this last parameter is only used if the app credentials are used to call a skill)
33 */
34 private static readonly appCredentialMapCache: Map<string, AppCredentials> = new Map<string, AppCredentials>();
35 private readonly credentialProvider: ICredentialProvider;
36
37 /**
38 * Creates a new instance of the [BotFrameworkHttpClient](xref:botbuilder.BotFrameworkHttpClient) class
39 * @param credentialProvider An instance of [ICredentialProvider](xref:botframework-connector.ICredentialProvider).
40 * @param channelService Optional. The channel service.
41 */
42 public constructor(credentialProvider: ICredentialProvider, channelService?: string) {
43 if (!credentialProvider) {
44 throw new Error('BotFrameworkHttpClient(): missing credentialProvider');
45 }
46
47 this.credentialProvider = credentialProvider;
48 this.channelService = channelService || process.env[AuthenticationConstants.ChannelService];
49 }
50
51 /**
52 * Forwards an activity to another bot.
53 * @remarks
54 *
55 * @template T The type of body in the InvokeResponse.
56 * @param fromBotId The MicrosoftAppId of the bot sending the activity.
57 * @param toBotId The MicrosoftAppId of the bot receiving the activity.
58 * @param toUrl The URL of the bot receiving the activity.
59 * @param serviceUrl The callback Url for the skill host.
60 * @param conversationId A conversation ID to use for the conversation with the skill.
61 * @param activity Activity to forward.
62 */
63 public async postActivity<T = any>(
64 fromBotId: string,
65 toBotId: string,
66 toUrl: string,
67 serviceUrl: string,
68 conversationId: string,
69 activity: Activity
70 ): Promise<InvokeResponse<T>> {
71 const appCredentials = await this.getAppCredentials(fromBotId, toBotId);
72 if (!appCredentials) {
73 throw new Error(
74 'BotFrameworkHttpClient.postActivity(): Unable to get appCredentials to connect to the skill'
75 );
76 }
77
78 if (!activity) {
79 throw new Error('BotFrameworkHttpClient.postActivity(): missing activity');
80 }
81
82 if (activity.conversation === undefined) {
83 throw new Error('BotFrameworkHttpClient.postActivity(): Activity must have a ConversationReference');
84 }
85
86 // Get token for the skill call
87 const token = appCredentials.appId ? await appCredentials.getToken() : null;
88
89 // Capture current activity settings before changing them.
90 // TODO: DO we need to set the activity ID? (events that are created manually don't have it).
91 const originalConversationId = activity.conversation.id;
92 const originalServiceUrl = activity.serviceUrl;
93 const originalRelatesTo = activity.relatesTo;
94 const originalRecipient = activity.recipient;
95
96 try {
97 activity.relatesTo = {
98 serviceUrl: activity.serviceUrl,
99 activityId: activity.id,
100 channelId: activity.channelId,
101 conversation: {
102 id: activity.conversation.id,
103 name: activity.conversation.name,
104 conversationType: activity.conversation.conversationType,
105 aadObjectId: activity.conversation.aadObjectId,
106 isGroup: activity.conversation.isGroup,
107 properties: activity.conversation.properties,
108 role: activity.conversation.role,
109 tenantId: activity.conversation.tenantId,
110 },
111 bot: null,
112 };
113 activity.conversation.id = conversationId;
114 activity.serviceUrl = serviceUrl;
115
116 // Fixes: https://github.com/microsoft/botframework-sdk/issues/5785
117 if (!activity.recipient) {
118 activity.recipient = {} as ChannelAccount;
119 }
120 activity.recipient.role = RoleTypes.Skill;
121
122 const config: { headers: Record<string, string>; validateStatus: () => boolean } = {
123 headers: {
124 Accept: 'application/json',
125 [ConversationConstants.ConversationIdHttpHeaderName]: conversationId,
126 'Content-Type': 'application/json',
127 'User-Agent': USER_AGENT,
128 },
129 validateStatus: (): boolean => true,
130 };
131
132 if (token) {
133 config.headers.Authorization = `Bearer ${token}`;
134 }
135
136 const response = await axios.post<T>(toUrl, activity, config);
137 return { status: response.status, body: response.data };
138 } finally {
139 // Restore activity properties.
140 activity.conversation.id = originalConversationId;
141 activity.serviceUrl = originalServiceUrl;
142 activity.relatesTo = originalRelatesTo;
143 activity.recipient = originalRecipient;
144 }
145 }
146
147 /**
148 * Logic to build an [AppCredentials](xref:botframework-connector.AppCredentials) to be used to acquire tokens for this `HttpClient`.
149 * @param appId The application id.
150 * @param oAuthScope Optional. The OAuth scope.
151 *
152 * @returns The app credentials to be used to acquire tokens.
153 */
154 protected async buildCredentials(appId: string, oAuthScope?: string): Promise<AppCredentials> {
155 const appPassword = await this.credentialProvider.getAppPassword(appId);
156 if (JwtTokenValidation.isGovernment(this.channelService)) {
157 const appCredentials = new MicrosoftAppCredentials(appId, appPassword, undefined, oAuthScope);
158 appCredentials.oAuthEndpoint = GovernmentConstants.ToChannelFromBotLoginUrl;
159 return appCredentials;
160 } else {
161 return new MicrosoftAppCredentials(appId, appPassword, undefined, oAuthScope);
162 }
163 }
164
165 /**
166 * Gets the application credentials. App Credentials are cached so as to ensure we are not refreshing
167 * token every time.
168 * @private
169 * @param appId The application identifier (AAD Id for the bot).
170 * @param oAuthScope The scope for the token, skills will use the Skill App Id.
171 */
172 private async getAppCredentials(appId: string, oAuthScope?: string): Promise<AppCredentials> {
173 if (!appId) {
174 return new MicrosoftAppCredentials('', '');
175 }
176
177 const cacheKey = `${appId}${oAuthScope}`;
178 let appCredentials = BotFrameworkHttpClient.appCredentialMapCache.get(cacheKey);
179 if (appCredentials) {
180 return appCredentials;
181 }
182
183 // Credentials not found in cache, build them
184 appCredentials = await this.buildCredentials(appId, oAuthScope);
185
186 // Cache the credentials for later use
187 BotFrameworkHttpClient.appCredentialMapCache.set(cacheKey, appCredentials);
188 return appCredentials;
189 }
190}