1 | ;
|
2 | /**
|
3 | * @module botbuilder
|
4 | */
|
5 | /**
|
6 | * Copyright (c) Microsoft Corporation. All rights reserved.
|
7 | * Licensed under the MIT License.
|
8 | */
|
9 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
10 | return new (P || (P = Promise))(function (resolve, reject) {
|
11 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
12 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
13 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
14 | step((generator = generator.apply(thisArg, _arguments || [])).next());
|
15 | });
|
16 | };
|
17 | Object.defineProperty(exports, "__esModule", { value: true });
|
18 | const os_1 = require("os");
|
19 | const botbuilder_core_1 = require("botbuilder-core");
|
20 | const botframework_connector_1 = require("botframework-connector");
|
21 | const botframework_streaming_1 = require("botframework-streaming");
|
22 | const streaming_1 = require("./streaming");
|
23 | const activityValidator_1 = require("./activityValidator");
|
24 | // Retrieve additional information, i.e., host operating system, host OS release, architecture, Node.js version
|
25 | const ARCHITECTURE = os_1.arch();
|
26 | const TYPE = os_1.type();
|
27 | const RELEASE = os_1.release();
|
28 | const NODE_VERSION = process.version;
|
29 | // eslint-disable-next-line @typescript-eslint/no-var-requires
|
30 | const pjson = require('../package.json');
|
31 | exports.USER_AGENT = `Microsoft-BotFramework/3.1 BotBuilder/${pjson.version} ` +
|
32 | `(Node.js,Version=${NODE_VERSION}; ${TYPE} ${RELEASE}; ${ARCHITECTURE})`;
|
33 | const OAUTH_ENDPOINT = 'https://api.botframework.com';
|
34 | const US_GOV_OAUTH_ENDPOINT = 'https://api.botframework.azure.us';
|
35 | /**
|
36 | * A [BotAdapter](xref:botbuilder-core.BotAdapter) that can connect a bot to a service endpoint.
|
37 | * Implements [IUserTokenProvider](xref:botbuilder-core.IUserTokenProvider).
|
38 | *
|
39 | * @remarks
|
40 | * The bot adapter encapsulates authentication processes and sends activities to and receives
|
41 | * activities from the Bot Connector Service. When your bot receives an activity, the adapter
|
42 | * creates a turn context object, passes it to your bot application logic, and sends responses
|
43 | * back to the user's channel.
|
44 | *
|
45 | * The adapter processes and directs incoming activities in through the bot middleware pipeline to
|
46 | * your bot logic and then back out again. As each activity flows in and out of the bot, each
|
47 | * piece of middleware can inspect or act upon the activity, both before and after the bot logic runs.
|
48 | * Use the [use](xref:botbuilder-core.BotAdapter.use) method to add [Middleware](xref:botbuilder-core.Middleware)
|
49 | * objects to your adapter's middleware collection.
|
50 | *
|
51 | * For more information, see the articles on
|
52 | * [How bots work](https://docs.microsoft.com/azure/bot-service/bot-builder-basics) and
|
53 | * [Middleware](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-middleware).
|
54 | *
|
55 | * For example:
|
56 | * ```JavaScript
|
57 | * const { BotFrameworkAdapter } = require('botbuilder');
|
58 | *
|
59 | * const adapter = new BotFrameworkAdapter({
|
60 | * appId: process.env.MicrosoftAppId,
|
61 | * appPassword: process.env.MicrosoftAppPassword
|
62 | * });
|
63 | *
|
64 | * adapter.onTurnError = async (context, error) => {
|
65 | * // Catch-all logic for errors.
|
66 | * };
|
67 | * ```
|
68 | */
|
69 | class BotFrameworkAdapter extends botbuilder_core_1.BotAdapter {
|
70 | /**
|
71 | * Creates a new instance of the [BotFrameworkAdapter](xref:botbuilder.BotFrameworkAdapter) class.
|
72 | *
|
73 | * @param settings Optional. The settings to use for this adapter instance.
|
74 | *
|
75 | * @remarks
|
76 | * If the `settings` parameter does not include
|
77 | * [channelService](xref:botbuilder.BotFrameworkAdapterSettings.channelService) or
|
78 | * [openIdMetadata](xref:botbuilder.BotFrameworkAdapterSettings.openIdMetadata) values, the
|
79 | * constructor checks the process' environment variables for these values. These values may be
|
80 | * set when a bot is provisioned on Azure and if so are required for the bot to work properly
|
81 | * in the global cloud or in a national cloud.
|
82 | *
|
83 | * The [BotFrameworkAdapterSettings](xref:botbuilder.BotFrameworkAdapterSettings) class defines
|
84 | * the available adapter settings.
|
85 | */
|
86 | constructor(settings) {
|
87 | super();
|
88 | // These keys are public to permit access to the keys from the adapter when it's a being
|
89 | // from library that does not have access to static properties off of BotFrameworkAdapter.
|
90 | // E.g. botbuilder-dialogs
|
91 | this.ConnectorClientKey = Symbol('ConnectorClient');
|
92 | this.TokenApiClientCredentialsKey = Symbol('TokenApiClientCredentials');
|
93 | this.settings = Object.assign({ appId: '', appPassword: '' }, settings);
|
94 | // If settings.certificateThumbprint & settings.certificatePrivateKey are provided,
|
95 | // use CertificateAppCredentials.
|
96 | if (this.settings.certificateThumbprint && this.settings.certificatePrivateKey) {
|
97 | this.credentials = new botframework_connector_1.CertificateAppCredentials(this.settings.appId, settings.certificateThumbprint, settings.certificatePrivateKey, this.settings.channelAuthTenant);
|
98 | this.credentialsProvider = new botframework_connector_1.SimpleCredentialProvider(this.credentials.appId, '');
|
99 | }
|
100 | else {
|
101 | this.credentials = new botframework_connector_1.MicrosoftAppCredentials(this.settings.appId, this.settings.appPassword || '', this.settings.channelAuthTenant);
|
102 | this.credentialsProvider = new botframework_connector_1.SimpleCredentialProvider(this.credentials.appId, this.settings.appPassword || '');
|
103 | }
|
104 | this.isEmulatingOAuthCards = false;
|
105 | // If no channelService or openIdMetadata values were passed in the settings, check the process' Environment Variables for values.
|
106 | // These values may be set when a bot is provisioned on Azure and if so are required for the bot to properly work in Public Azure or a National Cloud.
|
107 | this.settings.channelService = this.settings.channelService || process.env[botframework_connector_1.AuthenticationConstants.ChannelService];
|
108 | this.settings.openIdMetadata = this.settings.openIdMetadata || process.env[botframework_connector_1.AuthenticationConstants.BotOpenIdMetadataKey];
|
109 | this.authConfiguration = this.settings.authConfig || new botframework_connector_1.AuthenticationConfiguration();
|
110 | if (this.settings.openIdMetadata) {
|
111 | botframework_connector_1.ChannelValidation.OpenIdMetadataEndpoint = this.settings.openIdMetadata;
|
112 | botframework_connector_1.GovernmentChannelValidation.OpenIdMetadataEndpoint = this.settings.openIdMetadata;
|
113 | }
|
114 | if (botframework_connector_1.JwtTokenValidation.isGovernment(this.settings.channelService)) {
|
115 | this.credentials.oAuthEndpoint = botframework_connector_1.GovernmentConstants.ToChannelFromBotLoginUrl;
|
116 | this.credentials.oAuthScope = botframework_connector_1.GovernmentConstants.ToChannelFromBotOAuthScope;
|
117 | }
|
118 | // If a NodeWebSocketFactoryBase was passed in, set it on the BotFrameworkAdapter.
|
119 | if (this.settings.webSocketFactory) {
|
120 | this.webSocketFactory = this.settings.webSocketFactory;
|
121 | }
|
122 | // Relocate the tenantId field used by MS Teams to a new location (from channelData to conversation)
|
123 | // This will only occur on activities from teams that include tenant info in channelData but NOT in conversation,
|
124 | // thus should be future friendly. However, once the the transition is complete. we can remove this.
|
125 | this.use((context, next) => __awaiter(this, void 0, void 0, function* () {
|
126 | if (context.activity.channelId === 'msteams' && context.activity && context.activity.conversation && !context.activity.conversation.tenantId && context.activity.channelData && context.activity.channelData.tenant) {
|
127 | context.activity.conversation.tenantId = context.activity.channelData.tenant.id;
|
128 | }
|
129 | yield next();
|
130 | }));
|
131 | }
|
132 | /**
|
133 | * Used in streaming contexts to check if the streaming connection is still open for the bot to send activities.
|
134 | */
|
135 | get isStreamingConnectionOpen() {
|
136 | return this.streamingServer.isConnected;
|
137 | }
|
138 | continueConversation(reference, oAuthScopeOrlogic, logic) {
|
139 | return __awaiter(this, void 0, void 0, function* () {
|
140 | let audience = oAuthScopeOrlogic;
|
141 | let callback = typeof oAuthScopeOrlogic === 'function' ? oAuthScopeOrlogic : logic;
|
142 | if (typeof oAuthScopeOrlogic === 'function') {
|
143 | // Because the OAuthScope parameter was not provided, get the correct value via the channelService.
|
144 | // In this scenario, the ConnectorClient for the continued conversation can only communicate with
|
145 | // official channels, not with other bots.
|
146 | audience = botframework_connector_1.JwtTokenValidation.isGovernment(this.settings.channelService) ? botframework_connector_1.GovernmentConstants.ToChannelFromBotOAuthScope : botframework_connector_1.AuthenticationConstants.ToChannelFromBotOAuthScope;
|
147 | }
|
148 | let credentials = this.credentials;
|
149 | // For authenticated flows (where the bot has an AppId), the ConversationReference's serviceUrl needs
|
150 | // to be trusted for the bot to acquire a token when sending activities to the conversation.
|
151 | // For anonymous flows, the serviceUrl should not be trusted.
|
152 | if (credentials.appId) {
|
153 | botframework_connector_1.AppCredentials.trustServiceUrl(reference.serviceUrl);
|
154 | // If the provided OAuthScope doesn't match the current one on the instance's credentials, create
|
155 | // a new AppCredentials with the correct OAuthScope.
|
156 | if (credentials.oAuthScope !== audience) {
|
157 | // The BotFrameworkAdapter JavaScript implementation supports one Bot per instance, so get
|
158 | // the botAppId from the credentials.
|
159 | credentials = yield this.buildCredentials(credentials.appId, audience);
|
160 | }
|
161 | }
|
162 | const connectorClient = this.createConnectorClientInternal(reference.serviceUrl, credentials);
|
163 | const request = botbuilder_core_1.TurnContext.applyConversationReference({ type: 'event', name: 'continueConversation' }, reference, true);
|
164 | const context = this.createContext(request);
|
165 | context.turnState.set(this.OAuthScopeKey, audience);
|
166 | context.turnState.set(this.ConnectorClientKey, connectorClient);
|
167 | yield this.runMiddleware(context, callback);
|
168 | });
|
169 | }
|
170 | /**
|
171 | * Asynchronously creates and starts a conversation with a user on a channel.
|
172 | *
|
173 | * @param reference A reference for the conversation to create.
|
174 | * @param logic The asynchronous method to call after the adapter middleware runs.
|
175 | *
|
176 | * @remarks
|
177 | * To use this method, you need both the bot's and the user's account information on a channel.
|
178 | * The Bot Connector service supports the creating of group conversations; however, this
|
179 | * method and most channels only support initiating a direct message (non-group) conversation.
|
180 | *
|
181 | * To create and start a new conversation:
|
182 | * 1. Get a copy of a [ConversationReference](xref:botframework-schema.ConversationReference) from an incoming activity.
|
183 | * 1. Set the [user](xref:botframework-schema.ConversationReference.user) property to the
|
184 | * [ChannelAccount](xref:botframework-schema.ChannelAccount) value for the intended recipient.
|
185 | * 1. Call this method to request that the channel create a new conversation with the specified user.
|
186 | * 1. On success, the adapter generates a turn context and calls the `logic` function handler.
|
187 | *
|
188 | * To get the initial reference, use the
|
189 | * [TurnContext.getConversationReference](xref:botbuilder-core.TurnContext.getConversationReference)
|
190 | * method on any incoming activity in the conversation.
|
191 | *
|
192 | * If the channel establishes the conversation, the generated event activity's
|
193 | * [conversation](xref:botframework-schema.Activity.conversation) property will contain the
|
194 | * ID of the new conversation.
|
195 | *
|
196 | * This method is similar to the [processActivity](xref:botbuilder.BotFrameworkAdapter.processActivity) method.
|
197 | * The adapter creates a [TurnContext](xref:botbuilder-core.TurnContext) and routes it through
|
198 | * middleware before calling the `logic` handler. The created activity will have a
|
199 | * [type](xref:botframework-schema.Activity.type) of 'event' and a
|
200 | * [name](xref:botframework-schema.Activity.name) of 'createConversation'.
|
201 | *
|
202 | * For example:
|
203 | * ```JavaScript
|
204 | * // Get group members conversation reference
|
205 | * const reference = TurnContext.getConversationReference(context.activity);
|
206 | *
|
207 | * // ...
|
208 | * // Start a new conversation with the user
|
209 | * await adapter.createConversation(reference, async (ctx) => {
|
210 | * await ctx.sendActivity(`Hi (in private)`);
|
211 | * });
|
212 | * ```
|
213 | */
|
214 | createConversation(reference, logic) {
|
215 | return __awaiter(this, void 0, void 0, function* () {
|
216 | if (!reference.serviceUrl) {
|
217 | throw new Error(`BotFrameworkAdapter.createConversation(): missing serviceUrl.`);
|
218 | }
|
219 | // Create conversation
|
220 | const parameters = { bot: reference.bot, members: [reference.user], isGroup: false, activity: null, channelData: null };
|
221 | const client = this.createConnectorClient(reference.serviceUrl);
|
222 | // Mix in the tenant ID if specified. This is required for MS Teams.
|
223 | if (reference.conversation && reference.conversation.tenantId) {
|
224 | // Putting tenantId in channelData is a temporary solution while we wait for the Teams API to be updated
|
225 | parameters.channelData = { tenant: { id: reference.conversation.tenantId } };
|
226 | // Permanent solution is to put tenantId in parameters.tenantId
|
227 | parameters.tenantId = reference.conversation.tenantId;
|
228 | }
|
229 | const response = yield client.conversations.createConversation(parameters);
|
230 | // Initialize request and copy over new conversation ID and updated serviceUrl.
|
231 | const request = botbuilder_core_1.TurnContext.applyConversationReference({ type: 'event', name: 'createConversation' }, reference, true);
|
232 | const conversation = {
|
233 | id: response.id,
|
234 | isGroup: false,
|
235 | conversationType: null,
|
236 | tenantId: reference.conversation.tenantId,
|
237 | name: null,
|
238 | };
|
239 | request.conversation = conversation;
|
240 | request.channelData = parameters.channelData;
|
241 | if (response.serviceUrl) {
|
242 | request.serviceUrl = response.serviceUrl;
|
243 | }
|
244 | // Create context and run middleware
|
245 | const context = this.createContext(request);
|
246 | yield this.runMiddleware(context, logic);
|
247 | });
|
248 | }
|
249 | /**
|
250 | * Asynchronously deletes an existing activity.
|
251 | *
|
252 | * This interface supports the framework and is not intended to be called directly for your code.
|
253 | * Use [TurnContext.deleteActivity](xref:botbuilder-core.TurnContext.deleteActivity) to delete
|
254 | * an activity from your bot code.
|
255 | *
|
256 | * @param context The context object for the turn.
|
257 | * @param reference Conversation reference information for the activity to delete.
|
258 | *
|
259 | * @remarks
|
260 | * Not all channels support this operation. For channels that don't, this call may throw an exception.
|
261 | */
|
262 | deleteActivity(context, reference) {
|
263 | return __awaiter(this, void 0, void 0, function* () {
|
264 | if (!reference.serviceUrl) {
|
265 | throw new Error(`BotFrameworkAdapter.deleteActivity(): missing serviceUrl`);
|
266 | }
|
267 | if (!reference.conversation || !reference.conversation.id) {
|
268 | throw new Error(`BotFrameworkAdapter.deleteActivity(): missing conversation or conversation.id`);
|
269 | }
|
270 | if (!reference.activityId) {
|
271 | throw new Error(`BotFrameworkAdapter.deleteActivity(): missing activityId`);
|
272 | }
|
273 | const client = this.getOrCreateConnectorClient(context, reference.serviceUrl, this.credentials);
|
274 | yield client.conversations.deleteActivity(reference.conversation.id, reference.activityId);
|
275 | });
|
276 | }
|
277 | /**
|
278 | * Asynchronously removes a member from the current conversation.
|
279 | *
|
280 | * @param context The context object for the turn.
|
281 | * @param memberId The ID of the member to remove from the conversation.
|
282 | *
|
283 | * @remarks
|
284 | * Remove a member's identity information from the conversation.
|
285 | *
|
286 | * Not all channels support this operation. For channels that don't, this call may throw an exception.
|
287 | */
|
288 | deleteConversationMember(context, memberId) {
|
289 | return __awaiter(this, void 0, void 0, function* () {
|
290 | if (!context.activity.serviceUrl) {
|
291 | throw new Error(`BotFrameworkAdapter.deleteConversationMember(): missing serviceUrl`);
|
292 | }
|
293 | if (!context.activity.conversation || !context.activity.conversation.id) {
|
294 | throw new Error(`BotFrameworkAdapter.deleteConversationMember(): missing conversation or conversation.id`);
|
295 | }
|
296 | const serviceUrl = context.activity.serviceUrl;
|
297 | const conversationId = context.activity.conversation.id;
|
298 | const client = this.getOrCreateConnectorClient(context, serviceUrl, this.credentials);
|
299 | yield client.conversations.deleteConversationMember(conversationId, memberId);
|
300 | });
|
301 | }
|
302 | /**
|
303 | * Asynchronously lists the members of a given activity.
|
304 | *
|
305 | * @param context The context object for the turn.
|
306 | * @param activityId Optional. The ID of the activity to get the members of. If not specified, the current activity ID is used.
|
307 | *
|
308 | * @returns An array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for
|
309 | * the users involved in a given activity.
|
310 | *
|
311 | * @remarks
|
312 | * Returns an array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for
|
313 | * the users involved in a given activity.
|
314 | *
|
315 | * This is different from [getConversationMembers](xref:botbuilder.BotFrameworkAdapter.getConversationMembers)
|
316 | * in that it will return only those users directly involved in the activity, not all members of the conversation.
|
317 | */
|
318 | getActivityMembers(context, activityId) {
|
319 | return __awaiter(this, void 0, void 0, function* () {
|
320 | if (!activityId) {
|
321 | activityId = context.activity.id;
|
322 | }
|
323 | if (!context.activity.serviceUrl) {
|
324 | throw new Error(`BotFrameworkAdapter.getActivityMembers(): missing serviceUrl`);
|
325 | }
|
326 | if (!context.activity.conversation || !context.activity.conversation.id) {
|
327 | throw new Error(`BotFrameworkAdapter.getActivityMembers(): missing conversation or conversation.id`);
|
328 | }
|
329 | if (!activityId) {
|
330 | throw new Error(`BotFrameworkAdapter.getActivityMembers(): missing both activityId and context.activity.id`);
|
331 | }
|
332 | const serviceUrl = context.activity.serviceUrl;
|
333 | const conversationId = context.activity.conversation.id;
|
334 | const client = this.getOrCreateConnectorClient(context, serviceUrl, this.credentials);
|
335 | return yield client.conversations.getActivityMembers(conversationId, activityId);
|
336 | });
|
337 | }
|
338 | /**
|
339 | * Asynchronously lists the members of the current conversation.
|
340 | *
|
341 | * @param context The context object for the turn.
|
342 | *
|
343 | * @returns An array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for
|
344 | * all users currently involved in a conversation.
|
345 | *
|
346 | * @remarks
|
347 | * Returns an array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for
|
348 | * all users currently involved in a conversation.
|
349 | *
|
350 | * This is different from [getActivityMembers](xref:botbuilder.BotFrameworkAdapter.getActivityMembers)
|
351 | * in that it will return all members of the conversation, not just those directly involved in a specific activity.
|
352 | */
|
353 | getConversationMembers(context) {
|
354 | return __awaiter(this, void 0, void 0, function* () {
|
355 | if (!context.activity.serviceUrl) {
|
356 | throw new Error(`BotFrameworkAdapter.getConversationMembers(): missing serviceUrl`);
|
357 | }
|
358 | if (!context.activity.conversation || !context.activity.conversation.id) {
|
359 | throw new Error(`BotFrameworkAdapter.getConversationMembers(): missing conversation or conversation.id`);
|
360 | }
|
361 | const serviceUrl = context.activity.serviceUrl;
|
362 | const conversationId = context.activity.conversation.id;
|
363 | const client = this.getOrCreateConnectorClient(context, serviceUrl, this.credentials);
|
364 | return yield client.conversations.getConversationMembers(conversationId);
|
365 | });
|
366 | }
|
367 | /**
|
368 | * For the specified channel, asynchronously gets a page of the conversations in which this bot has participated.
|
369 | *
|
370 | * @param contextOrServiceUrl The URL of the channel server to query or a
|
371 | * [TurnContext](xref:botbuilder-core.TurnContext) object from a conversation on the channel.
|
372 | * @param continuationToken Optional. The continuation token from the previous page of results.
|
373 | * Omit this parameter or use `undefined` to retrieve the first page of results.
|
374 | *
|
375 | * @returns A [ConversationsResult](xref:botframework-schema.ConversationsResult) object containing a page of results
|
376 | * and a continuation token.
|
377 | *
|
378 | * @remarks
|
379 | * The the return value's [conversations](xref:botframework-schema.ConversationsResult.conversations) property contains a page of
|
380 | * [ConversationMembers](xref:botframework-schema.ConversationMembers) objects. Each object's
|
381 | * [id](xref:botframework-schema.ConversationMembers.id) is the ID of a conversation in which the bot has participated on this channel.
|
382 | * This method can be called from outside the context of a conversation, as only the bot's service URL and credentials are required.
|
383 | *
|
384 | * The channel batches results in pages. If the result's
|
385 | * [continuationToken](xref:botframework-schema.ConversationsResult.continuationToken) property is not empty, then
|
386 | * there are more pages to get. Use the returned token to get the next page of results.
|
387 | * If the `contextOrServiceUrl` parameter is a [TurnContext](xref:botbuilder-core.TurnContext), the URL of the channel server is
|
388 | * retrieved from
|
389 | * `contextOrServiceUrl`.[activity](xref:botbuilder-core.TurnContext.activity).[serviceUrl](xref:botframework-schema.Activity.serviceUrl).
|
390 | */
|
391 | getConversations(contextOrServiceUrl, continuationToken) {
|
392 | return __awaiter(this, void 0, void 0, function* () {
|
393 | let client;
|
394 | if (typeof contextOrServiceUrl === 'object') {
|
395 | const context = contextOrServiceUrl;
|
396 | client = this.getOrCreateConnectorClient(context, context.activity.serviceUrl, this.credentials);
|
397 | }
|
398 | else {
|
399 | client = this.createConnectorClient(contextOrServiceUrl);
|
400 | }
|
401 | return yield client.conversations.getConversations(continuationToken ? { continuationToken: continuationToken } : undefined);
|
402 | });
|
403 | }
|
404 | getUserToken(context, connectionName, magicCode, oAuthAppCredentials) {
|
405 | return __awaiter(this, void 0, void 0, function* () {
|
406 | if (!context.activity.from || !context.activity.from.id) {
|
407 | throw new Error(`BotFrameworkAdapter.getUserToken(): missing from or from.id`);
|
408 | }
|
409 | if (!connectionName) {
|
410 | throw new Error('getUserToken() requires a connectionName but none was provided.');
|
411 | }
|
412 | this.checkEmulatingOAuthCards(context);
|
413 | const userId = context.activity.from.id;
|
414 | const url = this.oauthApiUrl(context);
|
415 | const client = this.createTokenApiClient(url, oAuthAppCredentials);
|
416 | context.turnState.set(this.TokenApiClientCredentialsKey, client);
|
417 | const result = yield client.userToken.getToken(userId, connectionName, { code: magicCode, channelId: context.activity.channelId });
|
418 | if (!result || !result.token || result._response.status == 404) {
|
419 | return undefined;
|
420 | }
|
421 | else {
|
422 | return result;
|
423 | }
|
424 | });
|
425 | }
|
426 | signOutUser(context, connectionName, userId, oAuthAppCredentials) {
|
427 | return __awaiter(this, void 0, void 0, function* () {
|
428 | if (!context.activity.from || !context.activity.from.id) {
|
429 | throw new Error(`BotFrameworkAdapter.signOutUser(): missing from or from.id`);
|
430 | }
|
431 | if (!userId) {
|
432 | userId = context.activity.from.id;
|
433 | }
|
434 | this.checkEmulatingOAuthCards(context);
|
435 | const url = this.oauthApiUrl(context);
|
436 | const client = this.createTokenApiClient(url, oAuthAppCredentials);
|
437 | context.turnState.set(this.TokenApiClientCredentialsKey, client);
|
438 | yield client.userToken.signOut(userId, { connectionName: connectionName, channelId: context.activity.channelId });
|
439 | });
|
440 | }
|
441 | getSignInLink(context, connectionName, oAuthAppCredentials, userId, finalRedirect) {
|
442 | return __awaiter(this, void 0, void 0, function* () {
|
443 | if (userId && userId != context.activity.from.id) {
|
444 | throw new ReferenceError(`cannot retrieve OAuth signin link for a user that's different from the conversation`);
|
445 | }
|
446 | this.checkEmulatingOAuthCards(context);
|
447 | const conversation = botbuilder_core_1.TurnContext.getConversationReference(context.activity);
|
448 | const url = this.oauthApiUrl(context);
|
449 | const client = this.createTokenApiClient(url, oAuthAppCredentials);
|
450 | context.turnState.set(this.TokenApiClientCredentialsKey, client);
|
451 | const state = {
|
452 | ConnectionName: connectionName,
|
453 | Conversation: conversation,
|
454 | MsAppId: client.credentials.appId,
|
455 | RelatesTo: context.activity.relatesTo
|
456 | };
|
457 | const finalState = Buffer.from(JSON.stringify(state)).toString('base64');
|
458 | return (yield client.botSignIn.getSignInUrl(finalState, { channelId: context.activity.channelId, finalRedirect }))._response.bodyAsText;
|
459 | });
|
460 | }
|
461 | getTokenStatus(context, userId, includeFilter, oAuthAppCredentials) {
|
462 | return __awaiter(this, void 0, void 0, function* () {
|
463 | if (!userId && (!context.activity.from || !context.activity.from.id)) {
|
464 | throw new Error(`BotFrameworkAdapter.getTokenStatus(): missing from or from.id`);
|
465 | }
|
466 | this.checkEmulatingOAuthCards(context);
|
467 | userId = userId || context.activity.from.id;
|
468 | const url = this.oauthApiUrl(context);
|
469 | const client = this.createTokenApiClient(url, oAuthAppCredentials);
|
470 | context.turnState.set(this.TokenApiClientCredentialsKey, client);
|
471 | return (yield client.userToken.getTokenStatus(userId, { channelId: context.activity.channelId, include: includeFilter }))._response.parsedBody;
|
472 | });
|
473 | }
|
474 | getAadTokens(context, connectionName, resourceUrls, oAuthAppCredentials) {
|
475 | return __awaiter(this, void 0, void 0, function* () {
|
476 | if (!context.activity.from || !context.activity.from.id) {
|
477 | throw new Error(`BotFrameworkAdapter.getAadTokens(): missing from or from.id`);
|
478 | }
|
479 | this.checkEmulatingOAuthCards(context);
|
480 | const userId = context.activity.from.id;
|
481 | const url = this.oauthApiUrl(context);
|
482 | const client = this.createTokenApiClient(url, oAuthAppCredentials);
|
483 | context.turnState.set(this.TokenApiClientCredentialsKey, client);
|
484 | return (yield client.userToken.getAadTokens(userId, connectionName, { resourceUrls: resourceUrls }, { channelId: context.activity.channelId }))._response.parsedBody;
|
485 | });
|
486 | }
|
487 | /**
|
488 | * Asynchronously Get the raw signin resource to be sent to the user for signin.
|
489 | *
|
490 | * @param context The context object for the turn.
|
491 | * @param connectionName The name of the auth connection to use.
|
492 | * @param userId The user id that will be associated with the token.
|
493 | * @param finalRedirect The final URL that the OAuth flow will redirect to.
|
494 | *
|
495 | * @returns The [BotSignInGetSignInResourceResponse](xref:botframework-connector.BotSignInGetSignInResourceResponse) object.
|
496 | */
|
497 | getSignInResource(context, connectionName, userId, finalRedirect, appCredentials) {
|
498 | return __awaiter(this, void 0, void 0, function* () {
|
499 | if (!connectionName) {
|
500 | throw new Error('getUserToken() requires a connectionName but none was provided.');
|
501 | }
|
502 | if (!context.activity.from || !context.activity.from.id) {
|
503 | throw new Error(`BotFrameworkAdapter.getSignInResource(): missing from or from.id`);
|
504 | }
|
505 | // The provided userId doesn't match the from.id on the activity. (same for finalRedirect)
|
506 | if (userId && context.activity.from.id !== userId) {
|
507 | throw new Error('BotFrameworkAdapter.getSiginInResource(): cannot get signin resource for a user that is different from the conversation');
|
508 | }
|
509 | const url = this.oauthApiUrl(context);
|
510 | const credentials = appCredentials;
|
511 | const client = this.createTokenApiClient(url, credentials);
|
512 | const conversation = botbuilder_core_1.TurnContext.getConversationReference(context.activity);
|
513 | const state = {
|
514 | ConnectionName: connectionName,
|
515 | Conversation: conversation,
|
516 | relatesTo: context.activity.relatesTo,
|
517 | MSAppId: client.credentials.appId
|
518 | };
|
519 | const finalState = Buffer.from(JSON.stringify(state)).toString('base64');
|
520 | const options = { finalRedirect: finalRedirect };
|
521 | return yield (client.botSignIn.getSignInResource(finalState, options));
|
522 | });
|
523 | }
|
524 | exchangeToken(context, connectionName, userId, tokenExchangeRequest, appCredentials) {
|
525 | return __awaiter(this, void 0, void 0, function* () {
|
526 | if (!connectionName) {
|
527 | throw new Error('exchangeToken() requires a connectionName but none was provided.');
|
528 | }
|
529 | if (!userId) {
|
530 | throw new Error('exchangeToken() requires an userId but none was provided.');
|
531 | }
|
532 | if (tokenExchangeRequest && !tokenExchangeRequest.token && !tokenExchangeRequest.uri) {
|
533 | throw new Error('BotFrameworkAdapter.exchangeToken(): Either a Token or Uri property is required on the TokenExchangeRequest');
|
534 | }
|
535 | const url = this.oauthApiUrl(context);
|
536 | const client = this.createTokenApiClient(url, appCredentials);
|
537 | return (yield client.userToken.exchangeAsync(userId, connectionName, context.activity.channelId, tokenExchangeRequest))._response.parsedBody;
|
538 | });
|
539 | }
|
540 | /**
|
541 | * Asynchronously sends an emulated OAuth card for a channel.
|
542 | *
|
543 | * This method supports the framework and is not intended to be called directly for your code.
|
544 | *
|
545 | * @param contextOrServiceUrl The URL of the emulator.
|
546 | * @param emulate `true` to send an emulated OAuth card to the emulator; or `false` to not send the card.
|
547 | *
|
548 | * @remarks
|
549 | * When testing a bot in the Bot Framework Emulator, this method can emulate the OAuth card interaction.
|
550 | */
|
551 | emulateOAuthCards(contextOrServiceUrl, emulate) {
|
552 | return __awaiter(this, void 0, void 0, function* () {
|
553 | this.isEmulatingOAuthCards = emulate;
|
554 | const url = this.oauthApiUrl(contextOrServiceUrl);
|
555 | yield botframework_connector_1.EmulatorApiClient.emulateOAuthCards(this.credentials, url, emulate);
|
556 | });
|
557 | }
|
558 | /**
|
559 | * Asynchronously creates a turn context and runs the middleware pipeline for an incoming activity.
|
560 | *
|
561 | * @param req An Express or Restify style request object.
|
562 | * @param res An Express or Restify style response object.
|
563 | * @param logic The function to call at the end of the middleware pipeline.
|
564 | *
|
565 | * @remarks
|
566 | * This is the main way a bot receives incoming messages and defines a turn in the conversation. This method:
|
567 | *
|
568 | * 1. Parses and authenticates an incoming request.
|
569 | * - The activity is read from the body of the incoming request. An error will be returned
|
570 | * if the activity can't be parsed.
|
571 | * - The identity of the sender is authenticated as either the Emulator or a valid Microsoft
|
572 | * server, using the bot's `appId` and `appPassword`. The request is rejected if the sender's
|
573 | * identity is not verified.
|
574 | * 1. Creates a [TurnContext](xref:botbuilder-core.TurnContext) object for the received activity.
|
575 | * - This object is wrapped with a [revocable proxy](https://www.ecma-international.org/ecma-262/6.0/#sec-proxy.revocable).
|
576 | * - When this method completes, the proxy is revoked.
|
577 | * 1. Sends the turn context through the adapter's middleware pipeline.
|
578 | * 1. Sends the turn context to the `logic` function.
|
579 | * - The bot may perform additional routing or processing at this time.
|
580 | * Returning a promise (or providing an `async` handler) will cause the adapter to wait for any asynchronous operations to complete.
|
581 | * - After the `logic` function completes, the promise chain set up by the middleware is resolved.
|
582 | *
|
583 | * > [!TIP]
|
584 | * > If you see the error `TypeError: Cannot perform 'set' on a proxy that has been revoked`
|
585 | * > in your bot's console output, the likely cause is that an async function was used
|
586 | * > without using the `await` keyword. Make sure all async functions use await!
|
587 | *
|
588 | * Middleware can _short circuit_ a turn. When this happens, subsequent middleware and the
|
589 | * `logic` function is not called; however, all middleware prior to this point still run to completion.
|
590 | * For more information about the middleware pipeline, see the
|
591 | * [how bots work](https://docs.microsoft.com/azure/bot-service/bot-builder-basics) and
|
592 | * [middleware](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-middleware) articles.
|
593 | * Use the adapter's [use](xref:botbuilder-core.BotAdapter.use) method to add middleware to the adapter.
|
594 | *
|
595 | * For example:
|
596 | * ```JavaScript
|
597 | * server.post('/api/messages', (req, res) => {
|
598 | * // Route received request to adapter for processing
|
599 | * adapter.processActivity(req, res, async (context) => {
|
600 | * // Process any messages received
|
601 | * if (context.activity.type === ActivityTypes.Message) {
|
602 | * await context.sendActivity(`Hello World`);
|
603 | * }
|
604 | * });
|
605 | * });
|
606 | * ```
|
607 | */
|
608 | processActivity(req, res, logic) {
|
609 | return __awaiter(this, void 0, void 0, function* () {
|
610 | let body;
|
611 | let status;
|
612 | let processError;
|
613 | try {
|
614 | // Parse body of request
|
615 | status = 400;
|
616 | const request = yield parseRequest(req);
|
617 | // Authenticate the incoming request
|
618 | status = 401;
|
619 | const authHeader = req.headers.authorization || req.headers.Authorization || '';
|
620 | const identity = yield this.authenticateRequestInternal(request, authHeader);
|
621 | // Set the correct callerId value and discard values received over the wire
|
622 | request.callerId = yield this.generateCallerId(identity);
|
623 | // Process received activity
|
624 | status = 500;
|
625 | const context = this.createContext(request);
|
626 | context.turnState.set(this.BotIdentityKey, identity);
|
627 | const connectorClient = yield this.createConnectorClientWithIdentity(request.serviceUrl, identity);
|
628 | context.turnState.set(this.ConnectorClientKey, connectorClient);
|
629 | const oAuthScope = botframework_connector_1.SkillValidation.isSkillClaim(identity.claims) ? botframework_connector_1.JwtTokenValidation.getAppIdFromClaims(identity.claims) : this.credentials.oAuthScope;
|
630 | context.turnState.set(this.OAuthScopeKey, oAuthScope);
|
631 | context.turnState.set(botbuilder_core_1.BotCallbackHandlerKey, logic);
|
632 | yield this.runMiddleware(context, logic);
|
633 | // NOTE: The factoring of the code differs here when compared to C# as processActivity() returns Promise<void>.
|
634 | // This is due to the fact that the response to the incoming activity is sent from inside this implementation.
|
635 | // In C#, ProcessActivityAsync() returns Task<InvokeResponse> and ASP.NET handles sending of the response.
|
636 | if (request.deliveryMode === botbuilder_core_1.DeliveryModes.ExpectReplies) {
|
637 | // Handle "expectReplies" scenarios where all the activities have been buffered and sent back at once
|
638 | // in an invoke response.
|
639 | const expectedReplies = { activities: context.bufferedReplyActivities };
|
640 | body = expectedReplies;
|
641 | status = botbuilder_core_1.StatusCodes.OK;
|
642 | }
|
643 | else if (request.type === botbuilder_core_1.ActivityTypes.Invoke) {
|
644 | // Retrieve a cached Invoke response to handle Invoke scenarios.
|
645 | // These scenarios deviate from the request/request model as the Bot should return a specific body and status.
|
646 | const invokeResponse = context.turnState.get(botbuilder_core_1.INVOKE_RESPONSE_KEY);
|
647 | if (invokeResponse && invokeResponse.value) {
|
648 | const value = invokeResponse.value;
|
649 | status = value.status;
|
650 | body = value.body;
|
651 | }
|
652 | else {
|
653 | status = botbuilder_core_1.StatusCodes.NOT_IMPLEMENTED;
|
654 | }
|
655 | }
|
656 | else {
|
657 | status = botbuilder_core_1.StatusCodes.OK;
|
658 | }
|
659 | }
|
660 | catch (err) {
|
661 | // Catch the error to try and throw the stacktrace out of processActivity()
|
662 | processError = err;
|
663 | body = err.toString();
|
664 | }
|
665 | // Return status
|
666 | res.status(status);
|
667 | if (body) {
|
668 | res.send(body);
|
669 | }
|
670 | res.end();
|
671 | // Check for an error
|
672 | if (status >= 400) {
|
673 | if (processError && processError.stack) {
|
674 | throw new Error(`BotFrameworkAdapter.processActivity(): ${status} ERROR\n ${processError.stack}`);
|
675 | }
|
676 | else {
|
677 | throw new Error(`BotFrameworkAdapter.processActivity(): ${status} ERROR`);
|
678 | }
|
679 | }
|
680 | });
|
681 | }
|
682 | /**
|
683 | * Asynchronously creates a turn context and runs the middleware pipeline for an incoming activity.
|
684 | *
|
685 | * @param activity The activity to process.
|
686 | * @param logic The function to call at the end of the middleware pipeline.
|
687 | *
|
688 | * @remarks
|
689 | * This is the main way a bot receives incoming messages and defines a turn in the conversation. This method:
|
690 | *
|
691 | * 1. Creates a [TurnContext](xref:botbuilder-core.TurnContext) object for the received activity.
|
692 | * - This object is wrapped with a [revocable proxy](https://www.ecma-international.org/ecma-262/6.0/#sec-proxy.revocable).
|
693 | * - When this method completes, the proxy is revoked.
|
694 | * 1. Sends the turn context through the adapter's middleware pipeline.
|
695 | * 1. Sends the turn context to the `logic` function.
|
696 | * - The bot may perform additional routing or processing at this time.
|
697 | * Returning a promise (or providing an `async` handler) will cause the adapter to wait for any asynchronous operations to complete.
|
698 | * - After the `logic` function completes, the promise chain set up by the middleware is resolved.
|
699 | *
|
700 | * Middleware can _short circuit_ a turn. When this happens, subsequent middleware and the
|
701 | * `logic` function is not called; however, all middleware prior to this point still run to completion.
|
702 | * For more information about the middleware pipeline, see the
|
703 | * [how bots work](https://docs.microsoft.com/azure/bot-service/bot-builder-basics) and
|
704 | * [middleware](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-middleware) articles.
|
705 | * Use the adapter's [use](xref:botbuilder-core.BotAdapter.use) method to add middleware to the adapter.
|
706 | */
|
707 | processActivityDirect(activity, logic) {
|
708 | return __awaiter(this, void 0, void 0, function* () {
|
709 | let processError;
|
710 | try {
|
711 | // Process activity
|
712 | const context = this.createContext(activity);
|
713 | context.turnState.set(botbuilder_core_1.BotCallbackHandlerKey, logic);
|
714 | yield this.runMiddleware(context, logic);
|
715 | }
|
716 | catch (err) {
|
717 | // Catch the error to try and throw the stacktrace out of processActivity()
|
718 | processError = err;
|
719 | }
|
720 | if (processError) {
|
721 | if (processError && processError.stack) {
|
722 | throw new Error(`BotFrameworkAdapter.processActivityDirect(): ERROR\n ${processError.stack}`);
|
723 | }
|
724 | else {
|
725 | throw new Error(`BotFrameworkAdapter.processActivityDirect(): ERROR`);
|
726 | }
|
727 | }
|
728 | });
|
729 | }
|
730 | /**
|
731 | * Asynchronously sends a set of outgoing activities to a channel server.
|
732 | *
|
733 | * This method supports the framework and is not intended to be called directly for your code.
|
734 | * Use the turn context's [sendActivity](xref:botbuilder-core.TurnContext.sendActivity) or
|
735 | * [sendActivities](xref:botbuilder-core.TurnContext.sendActivities) method from your bot code.
|
736 | *
|
737 | * @param context The context object for the turn.
|
738 | * @param activities The activities to send.
|
739 | *
|
740 | * @returns An array of [ResourceResponse](xref:)
|
741 | *
|
742 | * @remarks
|
743 | * The activities will be sent one after another in the order in which they're received. A
|
744 | * response object will be returned for each sent activity. For `message` activities this will
|
745 | * contain the ID of the delivered message.
|
746 | */
|
747 | sendActivities(context, activities) {
|
748 | return __awaiter(this, void 0, void 0, function* () {
|
749 | const responses = [];
|
750 | for (let i = 0; i < activities.length; i++) {
|
751 | const activity = activities[i];
|
752 | switch (activity.type) {
|
753 | case 'delay':
|
754 | yield delay(typeof activity.value === 'number' ? activity.value : 1000);
|
755 | responses.push({});
|
756 | break;
|
757 | case 'invokeResponse':
|
758 | // Cache response to context object. This will be retrieved when turn completes.
|
759 | context.turnState.set(botbuilder_core_1.INVOKE_RESPONSE_KEY, activity);
|
760 | responses.push({});
|
761 | break;
|
762 | default:
|
763 | if (!activity.serviceUrl) {
|
764 | throw new Error(`BotFrameworkAdapter.sendActivity(): missing serviceUrl.`);
|
765 | }
|
766 | if (!activity.conversation || !activity.conversation.id) {
|
767 | throw new Error(`BotFrameworkAdapter.sendActivity(): missing conversation id.`);
|
768 | }
|
769 | if (activity && BotFrameworkAdapter.isStreamingServiceUrl(activity.serviceUrl)) {
|
770 | if (!this.isStreamingConnectionOpen) {
|
771 | throw new Error('BotFrameworkAdapter.sendActivities(): Unable to send activity as Streaming connection is closed.');
|
772 | }
|
773 | streaming_1.TokenResolver.checkForOAuthCards(this, context, activity);
|
774 | }
|
775 | const client = this.getOrCreateConnectorClient(context, activity.serviceUrl, this.credentials);
|
776 | if (activity.type === 'trace' && activity.channelId !== 'emulator') {
|
777 | // Just eat activity
|
778 | responses.push({});
|
779 | }
|
780 | else if (activity.replyToId) {
|
781 | responses.push(yield client.conversations.replyToActivity(activity.conversation.id, activity.replyToId, activity));
|
782 | }
|
783 | else {
|
784 | responses.push(yield client.conversations.sendToConversation(activity.conversation.id, activity));
|
785 | }
|
786 | break;
|
787 | }
|
788 | }
|
789 | return responses;
|
790 | });
|
791 | }
|
792 | /**
|
793 | * Asynchronously replaces a previous activity with an updated version.
|
794 | *
|
795 | * This interface supports the framework and is not intended to be called directly for your code.
|
796 | * Use [TurnContext.updateActivity](xref:botbuilder-core.TurnContext.updateActivity) to update
|
797 | * an activity from your bot code.
|
798 | *
|
799 | * @param context The context object for the turn.
|
800 | * @param activity The updated version of the activity to replace.
|
801 | *
|
802 | * @remarks
|
803 | * Not all channels support this operation. For channels that don't, this call may throw an exception.
|
804 | */
|
805 | updateActivity(context, activity) {
|
806 | return __awaiter(this, void 0, void 0, function* () {
|
807 | if (!activity.serviceUrl) {
|
808 | throw new Error(`BotFrameworkAdapter.updateActivity(): missing serviceUrl`);
|
809 | }
|
810 | if (!activity.conversation || !activity.conversation.id) {
|
811 | throw new Error(`BotFrameworkAdapter.updateActivity(): missing conversation or conversation.id`);
|
812 | }
|
813 | if (!activity.id) {
|
814 | throw new Error(`BotFrameworkAdapter.updateActivity(): missing activity.id`);
|
815 | }
|
816 | const client = this.getOrCreateConnectorClient(context, activity.serviceUrl, this.credentials);
|
817 | yield client.conversations.updateActivity(activity.conversation.id, activity.id, activity);
|
818 | });
|
819 | }
|
820 | /**
|
821 | * Creates a connector client.
|
822 | *
|
823 | * @param serviceUrl The client's service URL.
|
824 | *
|
825 | * @remarks
|
826 | * Override this in a derived class to create a mock connector client for unit testing.
|
827 | */
|
828 | createConnectorClient(serviceUrl) {
|
829 | return this.createConnectorClientInternal(serviceUrl, this.credentials);
|
830 | }
|
831 | createConnectorClientWithIdentity(serviceUrl, identity, audience) {
|
832 | return __awaiter(this, void 0, void 0, function* () {
|
833 | if (!identity) {
|
834 | throw new Error('BotFrameworkAdapter.createConnectorClientWithIdentity(): invalid identity parameter.');
|
835 | }
|
836 | const botAppId = identity.getClaimValue(botframework_connector_1.AuthenticationConstants.AudienceClaim) ||
|
837 | identity.getClaimValue(botframework_connector_1.AuthenticationConstants.AppIdClaim);
|
838 | // Check if the audience is a string and when trimmed doesn't have a length of 0.
|
839 | const validAudience = typeof audience === 'string' && audience.trim().length > 0;
|
840 | const oAuthScope = validAudience ? audience : yield this.getOAuthScope(botAppId, identity.claims);
|
841 | const credentials = yield this.buildCredentials(botAppId, oAuthScope);
|
842 | const client = this.createConnectorClientInternal(serviceUrl, credentials);
|
843 | return client;
|
844 | });
|
845 | }
|
846 | /**
|
847 | * @private
|
848 | * @param serviceUrl The client's service URL.
|
849 | * @param credentials AppCredentials instance to construct the ConnectorClient with.
|
850 | */
|
851 | createConnectorClientInternal(serviceUrl, credentials) {
|
852 | if (BotFrameworkAdapter.isStreamingServiceUrl(serviceUrl)) {
|
853 | // Check if we have a streaming server. Otherwise, requesting a connector client
|
854 | // for a non-existent streaming connection results in an error
|
855 | if (!this.streamingServer) {
|
856 | throw new Error(`Cannot create streaming connector client for serviceUrl ${serviceUrl} without a streaming connection. Call 'useWebSocket' or 'useNamedPipe' to start a streaming connection.`);
|
857 | }
|
858 | const clientOptions = this.getClientOptions(serviceUrl, new streaming_1.StreamingHttpClient(this.streamingServer));
|
859 | return new botframework_connector_1.ConnectorClient(credentials, clientOptions);
|
860 | }
|
861 | const clientOptions = this.getClientOptions(serviceUrl);
|
862 | return new botframework_connector_1.ConnectorClient(credentials, clientOptions);
|
863 | }
|
864 | /**
|
865 | * @private
|
866 | * @param serviceUrl The service URL to use for the new ConnectorClientOptions.
|
867 | * @param httpClient Optional. The @azure/ms-rest-js.HttpClient to use for the new ConnectorClientOptions.
|
868 | */
|
869 | getClientOptions(serviceUrl, httpClient) {
|
870 | const options = Object.assign({ baseUri: serviceUrl }, this.settings.clientOptions);
|
871 | if (httpClient) {
|
872 | options.httpClient = httpClient;
|
873 | }
|
874 | options.userAgent = `${exports.USER_AGENT}${options.userAgent || ''}`;
|
875 | return options;
|
876 | }
|
877 | /**
|
878 | * @private
|
879 | * Retrieves the ConnectorClient from the TurnContext or creates a new ConnectorClient with the provided serviceUrl and credentials.
|
880 | * @param context
|
881 | * @param serviceUrl
|
882 | * @param credentials
|
883 | */
|
884 | getOrCreateConnectorClient(context, serviceUrl, credentials) {
|
885 | if (!context || !context.turnState)
|
886 | throw new Error('invalid context parameter');
|
887 | if (!serviceUrl)
|
888 | throw new Error('invalid serviceUrl');
|
889 | if (!credentials)
|
890 | throw new Error('invalid credentials');
|
891 | let client = context.turnState.get(this.ConnectorClientKey);
|
892 | // Inspect the retrieved client to confirm that the serviceUrl is correct, if it isn't, create a new one.
|
893 | if (!client || client['baseUri'] !== serviceUrl) {
|
894 | client = this.createConnectorClientInternal(serviceUrl, credentials);
|
895 | }
|
896 | return client;
|
897 | }
|
898 | getOAuthScope(botAppId, claims) {
|
899 | return __awaiter(this, void 0, void 0, function* () {
|
900 | // If the Claims are for skills, we need to create an AppCredentials instance with
|
901 | // the correct scope for communication between the caller and the skill.
|
902 | if (botAppId && botframework_connector_1.SkillValidation.isSkillClaim(claims)) {
|
903 | return botframework_connector_1.JwtTokenValidation.getAppIdFromClaims(claims);
|
904 | }
|
905 | // Return the current credentials' OAuthScope.
|
906 | return this.credentials.oAuthScope;
|
907 | });
|
908 | }
|
909 | /**
|
910 | *
|
911 | * @remarks
|
912 | * When building credentials for bot-to-bot communication, oAuthScope must be passed in.
|
913 | * @param appId
|
914 | * @param oAuthScope
|
915 | */
|
916 | buildCredentials(appId, oAuthScope) {
|
917 | return __awaiter(this, void 0, void 0, function* () {
|
918 | // There is no cache for AppCredentials in JS as opposed to C#.
|
919 | // Instead of retrieving an AppCredentials from the Adapter instance, generate a new one
|
920 | const appPassword = yield this.credentialsProvider.getAppPassword(appId);
|
921 | const credentials = new botframework_connector_1.MicrosoftAppCredentials(appId, appPassword, this.settings.channelAuthTenant, oAuthScope);
|
922 | if (botframework_connector_1.JwtTokenValidation.isGovernment(this.settings.channelService)) {
|
923 | credentials.oAuthEndpoint = botframework_connector_1.GovernmentConstants.ToChannelFromBotLoginUrl;
|
924 | credentials.oAuthScope = oAuthScope || botframework_connector_1.GovernmentConstants.ToChannelFromBotOAuthScope;
|
925 | }
|
926 | return credentials;
|
927 | });
|
928 | }
|
929 | createTokenApiClient(serviceUrl, oAuthAppCredentials) {
|
930 | const tokenApiClientCredentials = oAuthAppCredentials ? oAuthAppCredentials : this.credentials;
|
931 | const client = new botframework_connector_1.TokenApiClient(tokenApiClientCredentials, { baseUri: serviceUrl, userAgent: exports.USER_AGENT });
|
932 | return client;
|
933 | }
|
934 | /**
|
935 | * Allows for the overriding of authentication in unit tests.
|
936 | * @param request Received request.
|
937 | * @param authHeader Received authentication header.
|
938 | */
|
939 | authenticateRequest(request, authHeader) {
|
940 | return __awaiter(this, void 0, void 0, function* () {
|
941 | const identity = yield this.authenticateRequestInternal(request, authHeader);
|
942 | if (!identity.isAuthenticated) {
|
943 | throw new Error('Unauthorized Access. Request is not authorized');
|
944 | }
|
945 | // Set the correct callerId value and discard values received over the wire
|
946 | request.callerId = yield this.generateCallerId(identity);
|
947 | });
|
948 | }
|
949 | /**
|
950 | * @ignore
|
951 | * @private
|
952 | * Returns the actual ClaimsIdentity from the JwtTokenValidation.authenticateRequest() call.
|
953 | * @remarks
|
954 | * This method is used instead of authenticateRequest() in processActivity() to obtain the ClaimsIdentity for caching in the TurnContext.turnState.
|
955 | *
|
956 | * @param request Received request.
|
957 | * @param authHeader Received authentication header.
|
958 | */
|
959 | authenticateRequestInternal(request, authHeader) {
|
960 | return botframework_connector_1.JwtTokenValidation.authenticateRequest(request, authHeader, this.credentialsProvider, this.settings.channelService, this.authConfiguration);
|
961 | }
|
962 | /**
|
963 | * Generates the CallerId property for the activity based on
|
964 | * https://github.com/microsoft/botframework-obi/blob/master/protocols/botframework-activity/botframework-activity.md#appendix-v---caller-id-values.
|
965 | * @param identity
|
966 | */
|
967 | generateCallerId(identity) {
|
968 | return __awaiter(this, void 0, void 0, function* () {
|
969 | if (!identity) {
|
970 | throw new TypeError('BotFrameworkAdapter.generateCallerId(): Missing identity parameter.');
|
971 | }
|
972 | // Is the bot accepting all incoming messages?
|
973 | const isAuthDisabled = yield this.credentialsProvider.isAuthenticationDisabled();
|
974 | if (isAuthDisabled) {
|
975 | // Return undefined so that the callerId is cleared.
|
976 | return;
|
977 | }
|
978 | // Is the activity from another bot?
|
979 | if (botframework_connector_1.SkillValidation.isSkillClaim(identity.claims)) {
|
980 | const callerId = botframework_connector_1.JwtTokenValidation.getAppIdFromClaims(identity.claims);
|
981 | return `${botbuilder_core_1.CallerIdConstants.BotToBotPrefix}${callerId}`;
|
982 | }
|
983 | // Is the activity from Public Azure?
|
984 | if (!this.settings.channelService || this.settings.channelService.length === 0) {
|
985 | return botbuilder_core_1.CallerIdConstants.PublicAzureChannel;
|
986 | }
|
987 | // Is the activity from Azure Gov?
|
988 | if (botframework_connector_1.JwtTokenValidation.isGovernment(this.settings.channelService)) {
|
989 | return botbuilder_core_1.CallerIdConstants.USGovChannel;
|
990 | }
|
991 | // Return undefined so that the callerId is cleared.
|
992 | });
|
993 | }
|
994 | /**
|
995 | * Gets the OAuth API endpoint.
|
996 | *
|
997 | * @param contextOrServiceUrl The URL of the channel server to query or
|
998 | * a [TurnContext](xref:botbuilder-core.TurnContext). For a turn context, the context's
|
999 | * [activity](xref:botbuilder-core.TurnContext.activity).[serviceUrl](xref:botframework-schema.Activity.serviceUrl)
|
1000 | * is used for the URL.
|
1001 | *
|
1002 | * @remarks
|
1003 | * Override this in a derived class to create a mock OAuth API endpoint for unit testing.
|
1004 | */
|
1005 | oauthApiUrl(contextOrServiceUrl) {
|
1006 | return this.isEmulatingOAuthCards ?
|
1007 | (typeof contextOrServiceUrl === 'object' ? contextOrServiceUrl.activity.serviceUrl : contextOrServiceUrl) :
|
1008 | (this.settings.oAuthEndpoint ? this.settings.oAuthEndpoint :
|
1009 | botframework_connector_1.JwtTokenValidation.isGovernment(this.settings.channelService) ?
|
1010 | US_GOV_OAUTH_ENDPOINT : OAUTH_ENDPOINT);
|
1011 | }
|
1012 | /**
|
1013 | * Checks the environment and can set a flag to emulate OAuth cards.
|
1014 | *
|
1015 | * @param context The context object for the turn.
|
1016 | *
|
1017 | * @remarks
|
1018 | * Override this in a derived class to control how OAuth cards are emulated for unit testing.
|
1019 | */
|
1020 | checkEmulatingOAuthCards(context) {
|
1021 | if (!this.isEmulatingOAuthCards &&
|
1022 | context.activity.channelId === 'emulator' &&
|
1023 | (!this.credentials.appId)) {
|
1024 | this.isEmulatingOAuthCards = true;
|
1025 | }
|
1026 | }
|
1027 | /**
|
1028 | * Creates a turn context.
|
1029 | *
|
1030 | * @param request An incoming request body.
|
1031 | *
|
1032 | * @remarks
|
1033 | * Override this in a derived class to modify how the adapter creates a turn context.
|
1034 | */
|
1035 | createContext(request) {
|
1036 | return new botbuilder_core_1.TurnContext(this, request);
|
1037 | }
|
1038 | /**
|
1039 | * Checks the validity of the request and attempts to map it the correct virtual endpoint,
|
1040 | * then generates and returns a response if appropriate.
|
1041 | * @param request A ReceiveRequest from the connected channel.
|
1042 | * @returns A response created by the BotAdapter to be sent to the client that originated the request.
|
1043 | */
|
1044 | processRequest(request) {
|
1045 | return __awaiter(this, void 0, void 0, function* () {
|
1046 | let response = new botframework_streaming_1.StreamingResponse();
|
1047 | if (!request) {
|
1048 | response.statusCode = botbuilder_core_1.StatusCodes.BAD_REQUEST;
|
1049 | response.setBody(`No request provided.`);
|
1050 | return response;
|
1051 | }
|
1052 | if (!request.verb || !request.path) {
|
1053 | response.statusCode = botbuilder_core_1.StatusCodes.BAD_REQUEST;
|
1054 | response.setBody(`Request missing verb and/or path. Verb: ${request.verb}. Path: ${request.path}`);
|
1055 | return response;
|
1056 | }
|
1057 | if (request.verb.toLocaleUpperCase() !== streaming_1.POST && request.verb.toLocaleUpperCase() !== streaming_1.GET) {
|
1058 | response.statusCode = botbuilder_core_1.StatusCodes.METHOD_NOT_ALLOWED;
|
1059 | response.setBody(`Invalid verb received. Only GET and POST are accepted. Verb: ${request.verb}`);
|
1060 | }
|
1061 | if (request.path.toLocaleLowerCase() === streaming_1.VERSION_PATH) {
|
1062 | return yield this.handleVersionRequest(request, response);
|
1063 | }
|
1064 | // Convert the StreamingRequest into an activity the Adapter can understand.
|
1065 | let body;
|
1066 | try {
|
1067 | body = yield this.readRequestBodyAsString(request);
|
1068 | }
|
1069 | catch (error) {
|
1070 | response.statusCode = botbuilder_core_1.StatusCodes.BAD_REQUEST;
|
1071 | response.setBody(`Request body missing or malformed: ${error}`);
|
1072 | return response;
|
1073 | }
|
1074 | if (request.path.toLocaleLowerCase() !== streaming_1.MESSAGES_PATH) {
|
1075 | response.statusCode = botbuilder_core_1.StatusCodes.NOT_FOUND;
|
1076 | response.setBody(`Path ${request.path.toLocaleLowerCase()} not not found. Expected ${streaming_1.MESSAGES_PATH}}.`);
|
1077 | return response;
|
1078 | }
|
1079 | if (request.verb.toLocaleUpperCase() !== streaming_1.POST) {
|
1080 | response.statusCode = botbuilder_core_1.StatusCodes.METHOD_NOT_ALLOWED;
|
1081 | response.setBody(`Invalid verb received for ${request.verb.toLocaleLowerCase()}. Only GET and POST are accepted. Verb: ${request.verb}`);
|
1082 | return response;
|
1083 | }
|
1084 | try {
|
1085 | let context = new botbuilder_core_1.TurnContext(this, body);
|
1086 | yield this.runMiddleware(context, this.logic);
|
1087 | if (body.type === botbuilder_core_1.ActivityTypes.Invoke) {
|
1088 | let invokeResponse = context.turnState.get(botbuilder_core_1.INVOKE_RESPONSE_KEY);
|
1089 | if (invokeResponse && invokeResponse.value) {
|
1090 | const value = invokeResponse.value;
|
1091 | response.statusCode = value.status;
|
1092 | if (value.body) {
|
1093 | response.setBody(value.body);
|
1094 | }
|
1095 | }
|
1096 | else {
|
1097 | response.statusCode = botbuilder_core_1.StatusCodes.NOT_IMPLEMENTED;
|
1098 | }
|
1099 | }
|
1100 | else if (body.deliveryMode === botbuilder_core_1.DeliveryModes.ExpectReplies) {
|
1101 | const replies = { activities: context.bufferedReplyActivities };
|
1102 | response.setBody(replies);
|
1103 | response.statusCode = botbuilder_core_1.StatusCodes.OK;
|
1104 | }
|
1105 | else {
|
1106 | response.statusCode = botbuilder_core_1.StatusCodes.OK;
|
1107 | }
|
1108 | }
|
1109 | catch (error) {
|
1110 | response.statusCode = botbuilder_core_1.StatusCodes.INTERNAL_SERVER_ERROR;
|
1111 | response.setBody(error);
|
1112 | return response;
|
1113 | }
|
1114 | return response;
|
1115 | });
|
1116 | }
|
1117 | healthCheck(context) {
|
1118 | return __awaiter(this, void 0, void 0, function* () {
|
1119 | const healthResults = {
|
1120 | success: true,
|
1121 | "user-agent": exports.USER_AGENT,
|
1122 | messages: ['Health check succeeded.']
|
1123 | };
|
1124 | if (!(yield this.credentialsProvider.isAuthenticationDisabled())) {
|
1125 | const credentials = context.turnState.get(this.ConnectorClientKey).credentials || this.credentials;
|
1126 | const token = yield credentials.getToken();
|
1127 | healthResults.authorization = `Bearer ${token}`;
|
1128 | }
|
1129 | return { healthResults: healthResults };
|
1130 | });
|
1131 | }
|
1132 | /**
|
1133 | * Connects the handler to a Named Pipe server and begins listening for incoming requests.
|
1134 | * @param pipeName The name of the named pipe to use when creating the server.
|
1135 | * @param logic The logic that will handle incoming requests.
|
1136 | */
|
1137 | useNamedPipe(logic, pipeName = streaming_1.defaultPipeName) {
|
1138 | return __awaiter(this, void 0, void 0, function* () {
|
1139 | if (!logic) {
|
1140 | throw new Error('Bot logic needs to be provided to `useNamedPipe`');
|
1141 | }
|
1142 | this.logic = logic;
|
1143 | this.streamingServer = new botframework_streaming_1.NamedPipeServer(pipeName, this);
|
1144 | yield this.streamingServer.start();
|
1145 | });
|
1146 | }
|
1147 | /**
|
1148 | * Process the initial request to establish a long lived connection via a streaming server.
|
1149 | * @param req The connection request.
|
1150 | * @param socket The raw socket connection between the bot (server) and channel/caller (client).
|
1151 | * @param head The first packet of the upgraded stream.
|
1152 | * @param logic The logic that handles incoming streaming requests for the lifetime of the WebSocket connection.
|
1153 | */
|
1154 | useWebSocket(req, socket, head, logic) {
|
1155 | return __awaiter(this, void 0, void 0, function* () {
|
1156 | // Use the provided NodeWebSocketFactoryBase on BotFrameworkAdapter construction,
|
1157 | // otherwise create a new NodeWebSocketFactory.
|
1158 | const webSocketFactory = this.webSocketFactory || new botframework_streaming_1.NodeWebSocketFactory();
|
1159 | if (!logic) {
|
1160 | throw new Error('Streaming logic needs to be provided to `useWebSocket`');
|
1161 | }
|
1162 | this.logic = logic;
|
1163 | try {
|
1164 | yield this.authenticateConnection(req, this.settings.channelService);
|
1165 | }
|
1166 | catch (err) {
|
1167 | abortWebSocketUpgrade(socket, err);
|
1168 | throw err;
|
1169 | }
|
1170 | const nodeWebSocket = yield webSocketFactory.createWebSocket(req, socket, head);
|
1171 | yield this.startWebSocket(nodeWebSocket);
|
1172 | });
|
1173 | }
|
1174 | authenticateConnection(req, channelService) {
|
1175 | return __awaiter(this, void 0, void 0, function* () {
|
1176 | if (!this.credentials.appId) {
|
1177 | // auth is disabled
|
1178 | return;
|
1179 | }
|
1180 | const authHeader = req.headers.authorization || req.headers.Authorization || '';
|
1181 | const channelIdHeader = req.headers.channelid || req.headers.ChannelId || req.headers.ChannelID || '';
|
1182 | // Validate the received Upgrade request from the channel.
|
1183 | const claims = yield botframework_connector_1.JwtTokenValidation.validateAuthHeader(authHeader, this.credentialsProvider, channelService, channelIdHeader);
|
1184 | // Add serviceUrl from claim to static cache to trigger token refreshes.
|
1185 | const serviceUrl = claims.getClaimValue(botframework_connector_1.AuthenticationConstants.ServiceUrlClaim);
|
1186 | botframework_connector_1.AppCredentials.trustServiceUrl(serviceUrl);
|
1187 | if (!claims.isAuthenticated) {
|
1188 | throw new botframework_connector_1.AuthenticationError('Unauthorized Access. Request is not authorized', botbuilder_core_1.StatusCodes.UNAUTHORIZED);
|
1189 | }
|
1190 | });
|
1191 | }
|
1192 | /**
|
1193 | * Connects the handler to a WebSocket server and begins listening for incoming requests.
|
1194 | * @param socket The socket to use when creating the server.
|
1195 | */
|
1196 | startWebSocket(socket) {
|
1197 | return __awaiter(this, void 0, void 0, function* () {
|
1198 | this.streamingServer = new botframework_streaming_1.WebSocketServer(socket, this);
|
1199 | yield this.streamingServer.start();
|
1200 | });
|
1201 | }
|
1202 | readRequestBodyAsString(request) {
|
1203 | return __awaiter(this, void 0, void 0, function* () {
|
1204 | const contentStream = request.streams[0];
|
1205 | return yield contentStream.readAsJson();
|
1206 | });
|
1207 | }
|
1208 | handleVersionRequest(request, response) {
|
1209 | return __awaiter(this, void 0, void 0, function* () {
|
1210 | if (request.verb.toLocaleUpperCase() === streaming_1.GET) {
|
1211 | response.statusCode = botbuilder_core_1.StatusCodes.OK;
|
1212 | if (!this.credentials.appId) {
|
1213 | response.setBody({ UserAgent: exports.USER_AGENT });
|
1214 | return response;
|
1215 | }
|
1216 | let token = '';
|
1217 | try {
|
1218 | token = yield this.credentials.getToken();
|
1219 | }
|
1220 | catch (err) {
|
1221 | /**
|
1222 | * In reality a missing BotToken will cause the channel to close the connection,
|
1223 | * but we still send the response and allow the channel to make that decision
|
1224 | * instead of proactively disconnecting. This allows the channel to know why
|
1225 | * the connection has been closed and make the choice not to make endless reconnection
|
1226 | * attempts that will end up right back here.
|
1227 | */
|
1228 | console.error(err.message);
|
1229 | }
|
1230 | response.setBody({ UserAgent: exports.USER_AGENT, BotToken: token });
|
1231 | }
|
1232 | else {
|
1233 | response.statusCode = botbuilder_core_1.StatusCodes.METHOD_NOT_ALLOWED;
|
1234 | response.setBody(`Invalid verb received for path: ${request.path}. Only GET is accepted. Verb: ${request.verb}`);
|
1235 | }
|
1236 | return response;
|
1237 | });
|
1238 | }
|
1239 | /**
|
1240 | * Determine if the serviceUrl was sent via an Http/Https connection or Streaming
|
1241 | * This can be determined by looking at the ServiceUrl property:
|
1242 | * (1) All channels that send messages via http/https are not streaming
|
1243 | * (2) Channels that send messages via streaming have a ServiceUrl that does not begin with http/https.
|
1244 | * @param serviceUrl the serviceUrl provided in the resquest.
|
1245 | */
|
1246 | static isStreamingServiceUrl(serviceUrl) {
|
1247 | return serviceUrl && !serviceUrl.toLowerCase().startsWith('http');
|
1248 | }
|
1249 | }
|
1250 | exports.BotFrameworkAdapter = BotFrameworkAdapter;
|
1251 | /**
|
1252 | * Handles incoming webhooks from the botframework
|
1253 | * @private
|
1254 | * @param req incoming web request
|
1255 | */
|
1256 | function parseRequest(req) {
|
1257 | return new Promise((resolve, reject) => {
|
1258 | if (req.body) {
|
1259 | try {
|
1260 | const activity = activityValidator_1.validateAndFixActivity(req.body);
|
1261 | resolve(activity);
|
1262 | }
|
1263 | catch (err) {
|
1264 | reject(err);
|
1265 | }
|
1266 | }
|
1267 | else {
|
1268 | let requestData = '';
|
1269 | req.on('data', (chunk) => {
|
1270 | requestData += chunk;
|
1271 | });
|
1272 | req.on('end', () => {
|
1273 | try {
|
1274 | req.body = JSON.parse(requestData);
|
1275 | const activity = activityValidator_1.validateAndFixActivity(req.body);
|
1276 | resolve(activity);
|
1277 | }
|
1278 | catch (err) {
|
1279 | reject(err);
|
1280 | }
|
1281 | });
|
1282 | }
|
1283 | });
|
1284 | }
|
1285 | function delay(timeout) {
|
1286 | return new Promise((resolve) => {
|
1287 | setTimeout(resolve, timeout);
|
1288 | });
|
1289 | }
|
1290 | /**
|
1291 | * Creates an error message with status code to write to socket, then closes the connection.
|
1292 | *
|
1293 | * @param socket The raw socket connection between the bot (server) and channel/caller (client).
|
1294 | * @param err The error. If the error includes a status code, it will be included in the message written to the socket.
|
1295 | */
|
1296 | function abortWebSocketUpgrade(socket, err) {
|
1297 | if (socket.writable) {
|
1298 | const connectionHeader = `Connection: 'close'\r\n`;
|
1299 | let message = '';
|
1300 | botframework_connector_1.AuthenticationError.isStatusCodeError(err) ?
|
1301 | message = `HTTP/1.1 ${err.statusCode} ${botbuilder_core_1.StatusCodes[err.statusCode]}\r\n${err.message}\r\n${connectionHeader}\r\n`
|
1302 | : message = botframework_connector_1.AuthenticationError.determineStatusCodeAndBuildMessage(err);
|
1303 | socket.write(message);
|
1304 | }
|
1305 | socket.destroy();
|
1306 | }
|
1307 | //# sourceMappingURL=botFrameworkAdapter.js.map |
\ | No newline at end of file |