botbuilder
Version:
Bot Builder is a framework for building rich bots on virtually any platform.
876 lines • 80.7 kB
JavaScript
"use strict";
/**
* @module botbuilder
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BotFrameworkAdapter = exports.USER_AGENT = void 0;
const z = require("zod");
const interfaces_1 = require("./interfaces");
const ms_rest_js_1 = require("@azure/ms-rest-js");
const zod_1 = require("./zod");
const os_1 = require("os");
const botbuilder_stdlib_1 = require("botbuilder-stdlib");
const activityValidator_1 = require("./activityValidator");
const botbuilder_core_1 = require("botbuilder-core");
const botframework_connector_1 = require("botframework-connector");
const botframework_streaming_1 = require("botframework-streaming");
const streaming_1 = require("./streaming");
// Retrieve additional information, i.e., host operating system, host OS release, architecture, Node.js version
const ARCHITECTURE = os_1.arch();
const TYPE = os_1.type();
const RELEASE = os_1.release();
const NODE_VERSION = process.version;
const pjson = require('../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires
exports.USER_AGENT = `Microsoft-BotFramework/3.1 BotBuilder/${pjson.version} (Node.js,Version=${NODE_VERSION}; ${TYPE} ${RELEASE}; ${ARCHITECTURE})`;
const OAUTH_ENDPOINT = 'https://api.botframework.com';
const US_GOV_OAUTH_ENDPOINT = 'https://api.botframework.azure.us';
/**
* A [BotAdapter](xref:botbuilder-core.BotAdapter) that can connect a bot to a service endpoint.
* Implements [IUserTokenProvider](xref:botbuilder-core.IUserTokenProvider).
*
* @remarks
* The bot adapter encapsulates authentication processes and sends activities to and receives
* activities from the Bot Connector Service. When your bot receives an activity, the adapter
* creates a turn context object, passes it to your bot application logic, and sends responses
* back to the user's channel.
*
* The adapter processes and directs incoming activities in through the bot middleware pipeline to
* your bot logic and then back out again. As each activity flows in and out of the bot, each
* piece of middleware can inspect or act upon the activity, both before and after the bot logic runs.
* Use the [use](xref:botbuilder-core.BotAdapter.use) method to add [Middleware](xref:botbuilder-core.Middleware)
* objects to your adapter's middleware collection.
*
* For more information, see the articles on
* [How bots work](https://docs.microsoft.com/azure/bot-service/bot-builder-basics) and
* [Middleware](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-middleware).
*
* For example:
* ```JavaScript
* const { BotFrameworkAdapter } = require('botbuilder');
*
* const adapter = new BotFrameworkAdapter({
* appId: process.env.MicrosoftAppId,
* appPassword: process.env.MicrosoftAppPassword
* });
*
* adapter.onTurnError = async (context, error) => {
* // Catch-all logic for errors.
* };
* ```
*/
/**
* @deprecated Use `CloudAdapter` instead.
*/
class BotFrameworkAdapter extends botbuilder_core_1.BotAdapter {
/**
* Creates a new instance of the [BotFrameworkAdapter](xref:botbuilder.BotFrameworkAdapter) class.
*
* @param settings Optional. The settings to use for this adapter instance.
*
* @remarks
* If the `settings` parameter does not include
* [channelService](xref:botbuilder.BotFrameworkAdapterSettings.channelService) or
* [openIdMetadata](xref:botbuilder.BotFrameworkAdapterSettings.openIdMetadata) values, the
* constructor checks the process' environment variables for these values. These values may be
* set when a bot is provisioned on Azure and if so are required for the bot to work properly
* in the global cloud or in a national cloud.
*
* The [BotFrameworkAdapterSettings](xref:botbuilder.BotFrameworkAdapterSettings) class defines
* the available adapter settings.
*/
constructor(settings) {
super();
// These keys are public to permit access to the keys from the adapter when it's a being
// from library that does not have access to static properties off of BotFrameworkAdapter.
// E.g. botbuilder-dialogs
this.TokenApiClientCredentialsKey = Symbol('TokenApiClientCredentials');
this.settings = Object.assign({ appId: '', appPassword: '' }, settings);
// If settings.certificateThumbprint & settings.certificatePrivateKey are provided,
// use CertificateAppCredentials.
if (this.settings.certificateThumbprint && this.settings.certificatePrivateKey) {
this.credentials = new botframework_connector_1.CertificateAppCredentials(this.settings.appId, settings.certificateThumbprint, settings.certificatePrivateKey, this.settings.channelAuthTenant);
this.credentialsProvider = new botframework_connector_1.SimpleCredentialProvider(this.credentials.appId, '');
}
else {
this.credentials = new botframework_connector_1.MicrosoftAppCredentials(this.settings.appId, this.settings.appPassword || '', this.settings.channelAuthTenant);
this.credentialsProvider = new botframework_connector_1.SimpleCredentialProvider(this.credentials.appId, this.settings.appPassword || '');
}
this.isEmulatingOAuthCards = false;
// If no channelService or openIdMetadata values were passed in the settings, check the process' Environment Variables for values.
// 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.
this.settings.channelService =
this.settings.channelService || process.env[botframework_connector_1.AuthenticationConstants.ChannelService];
this.settings.openIdMetadata =
this.settings.openIdMetadata || process.env[botframework_connector_1.AuthenticationConstants.BotOpenIdMetadataKey];
this.authConfiguration = this.settings.authConfig || new botframework_connector_1.AuthenticationConfiguration();
if (this.settings.openIdMetadata) {
botframework_connector_1.ChannelValidation.OpenIdMetadataEndpoint = this.settings.openIdMetadata;
botframework_connector_1.GovernmentChannelValidation.OpenIdMetadataEndpoint = this.settings.openIdMetadata;
}
if (botframework_connector_1.JwtTokenValidation.isGovernment(this.settings.channelService)) {
this.credentials.oAuthEndpoint = botframework_connector_1.GovernmentConstants.ToChannelFromBotLoginUrl;
this.credentials.oAuthScope = botframework_connector_1.GovernmentConstants.ToChannelFromBotOAuthScope;
}
// If a NodeWebSocketFactoryBase was passed in, set it on the BotFrameworkAdapter.
if (this.settings.webSocketFactory) {
this.webSocketFactory = this.settings.webSocketFactory;
}
// Relocate the tenantId field used by MS Teams to a new location (from channelData to conversation)
// This will only occur on activities from teams that include tenant info in channelData but NOT in conversation,
// thus should be future friendly. However, once the the transition is complete. we can remove this.
this.use((context, next) => __awaiter(this, void 0, void 0, function* () {
if (context.activity.channelId === 'msteams' &&
context.activity &&
context.activity.conversation &&
!context.activity.conversation.tenantId &&
context.activity.channelData &&
context.activity.channelData.tenant) {
context.activity.conversation.tenantId = context.activity.channelData.tenant.id;
}
yield next();
}));
}
/**
* Used in streaming contexts to check if the streaming connection is still open for the bot to send activities.
*
* @returns True if the streaming connection is open, otherwise false.
*/
get isStreamingConnectionOpen() {
var _a, _b;
return (_b = (_a = this.streamingServer) === null || _a === void 0 ? void 0 : _a.isConnected) !== null && _b !== void 0 ? _b : false;
}
/**
* @internal
*/
continueConversation(reference, oAuthScopeOrlogic, maybeLogic) {
return __awaiter(this, void 0, void 0, function* () {
let audience;
if (zod_1.LogicT.check(oAuthScopeOrlogic)) {
// Because the OAuthScope parameter was not provided, get the correct value via the channelService.
// In this scenario, the ConnectorClient for the continued conversation can only communicate with
// official channels, not with other bots.
audience = botframework_connector_1.JwtTokenValidation.isGovernment(this.settings.channelService)
? botframework_connector_1.GovernmentConstants.ToChannelFromBotOAuthScope
: botframework_connector_1.AuthenticationConstants.ToChannelFromBotOAuthScope;
}
else {
audience = z.string().parse(oAuthScopeOrlogic);
}
const logic = zod_1.LogicT.check(oAuthScopeOrlogic) ? oAuthScopeOrlogic : zod_1.LogicT.parse(maybeLogic);
let credentials = this.credentials;
// For authenticated flows (where the bot has an AppId), the ConversationReference's serviceUrl needs
// to be trusted for the bot to acquire a token when sending activities to the conversation.
// For anonymous flows, the serviceUrl should not be trusted.
if (credentials.appId) {
// If the provided OAuthScope doesn't match the current one on the instance's credentials, create
// a new AppCredentials with the correct OAuthScope.
if (credentials.oAuthScope !== audience) {
// The BotFrameworkAdapter JavaScript implementation supports one Bot per instance, so get
// the botAppId from the credentials.
credentials = yield this.buildCredentials(credentials.appId, audience);
}
}
const connectorClient = this.createConnectorClientInternal(reference.serviceUrl, credentials);
const request = botbuilder_core_1.TurnContext.applyConversationReference({ type: botbuilder_core_1.ActivityTypes.Event, name: botbuilder_core_1.ActivityEventNames.ContinueConversation }, reference, true);
const context = this.createContext(request);
context.turnState.set(this.OAuthScopeKey, audience);
context.turnState.set(this.ConnectorClientKey, connectorClient);
yield this.runMiddleware(context, logic);
});
}
/**
* @internal
*/
createConversation(reference, parametersOrLogic, maybeLogic) {
return __awaiter(this, void 0, void 0, function* () {
if (!reference.serviceUrl) {
throw new Error('BotFrameworkAdapter.createConversation(): missing serviceUrl.');
}
const parameters = zod_1.LogicT.check(parametersOrLogic) ? {} : parametersOrLogic;
const logic = zod_1.LogicT.check(parametersOrLogic) ? parametersOrLogic : zod_1.LogicT.parse(maybeLogic);
// Create conversation parameters, taking care to provide defaults that can be
// overridden by passed in parameters
const conversationParameters = Object.assign({}, {
bot: reference.bot,
members: [reference.user],
isGroup: false,
activity: null,
channelData: null,
}, parameters);
const client = this.createConnectorClient(reference.serviceUrl);
// Mix in the tenant ID if specified. This is required for MS Teams.
if (reference.conversation && reference.conversation.tenantId) {
// Putting tenantId in channelData is a temporary solution while we wait for the Teams API to be updated
conversationParameters.channelData = { tenant: { id: reference.conversation.tenantId } };
// Permanent solution is to put tenantId in parameters.tenantId
conversationParameters.tenantId = reference.conversation.tenantId;
}
const response = yield client.conversations.createConversation(conversationParameters);
// Initialize request and copy over new conversation ID and updated serviceUrl.
const request = botbuilder_core_1.TurnContext.applyConversationReference({ type: botbuilder_core_1.ActivityTypes.Event, name: botbuilder_core_1.ActivityEventNames.CreateConversation }, reference, true);
request.conversation = {
id: response.id,
isGroup: conversationParameters.isGroup,
conversationType: null,
tenantId: reference.conversation.tenantId,
name: null,
};
request.channelData = conversationParameters.channelData;
if (response.serviceUrl) {
request.serviceUrl = response.serviceUrl;
}
// Create context and run middleware
const context = this.createContext(request);
yield this.runMiddleware(context, logic);
});
}
/**
* Asynchronously deletes an existing activity.
*
* This interface supports the framework and is not intended to be called directly for your code.
* Use [TurnContext.deleteActivity](xref:botbuilder-core.TurnContext.deleteActivity) to delete
* an activity from your bot code.
*
* @param context The context object for the turn.
* @param reference Conversation reference information for the activity to delete.
*
* @remarks
* Not all channels support this operation. For channels that don't, this call may throw an exception.
*/
deleteActivity(context, reference) {
return __awaiter(this, void 0, void 0, function* () {
if (!reference.serviceUrl) {
throw new Error('BotFrameworkAdapter.deleteActivity(): missing serviceUrl');
}
if (!reference.conversation || !reference.conversation.id) {
throw new Error('BotFrameworkAdapter.deleteActivity(): missing conversation or conversation.id');
}
if (!reference.activityId) {
throw new Error('BotFrameworkAdapter.deleteActivity(): missing activityId');
}
const client = this.getOrCreateConnectorClient(context, reference.serviceUrl, this.credentials);
yield client.conversations.deleteActivity(reference.conversation.id, reference.activityId);
});
}
/**
* Asynchronously removes a member from the current conversation.
*
* @param context The context object for the turn.
* @param memberId The ID of the member to remove from the conversation.
*
* @remarks
* Remove a member's identity information from the conversation.
*
* Not all channels support this operation. For channels that don't, this call may throw an exception.
*/
deleteConversationMember(context, memberId) {
return __awaiter(this, void 0, void 0, function* () {
if (!context.activity.serviceUrl) {
throw new Error('BotFrameworkAdapter.deleteConversationMember(): missing serviceUrl');
}
if (!context.activity.conversation || !context.activity.conversation.id) {
throw new Error('BotFrameworkAdapter.deleteConversationMember(): missing conversation or conversation.id');
}
const serviceUrl = context.activity.serviceUrl;
const conversationId = context.activity.conversation.id;
const client = this.getOrCreateConnectorClient(context, serviceUrl, this.credentials);
yield client.conversations.deleteConversationMember(conversationId, memberId);
});
}
/**
* Asynchronously lists the members of a given activity.
*
* @param context The context object for the turn.
* @param activityId Optional. The ID of the activity to get the members of. If not specified, the current activity ID is used.
*
* @returns An array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for
* the users involved in a given activity.
*
* @remarks
* Returns an array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for
* the users involved in a given activity.
*
* This is different from [getConversationMembers](xref:botbuilder.BotFrameworkAdapter.getConversationMembers)
* in that it will return only those users directly involved in the activity, not all members of the conversation.
*/
getActivityMembers(context, activityId) {
return __awaiter(this, void 0, void 0, function* () {
if (!activityId) {
activityId = context.activity.id;
}
if (!context.activity.serviceUrl) {
throw new Error('BotFrameworkAdapter.getActivityMembers(): missing serviceUrl');
}
if (!context.activity.conversation || !context.activity.conversation.id) {
throw new Error('BotFrameworkAdapter.getActivityMembers(): missing conversation or conversation.id');
}
if (!activityId) {
throw new Error('BotFrameworkAdapter.getActivityMembers(): missing both activityId and context.activity.id');
}
const serviceUrl = context.activity.serviceUrl;
const conversationId = context.activity.conversation.id;
const client = this.getOrCreateConnectorClient(context, serviceUrl, this.credentials);
return yield client.conversations.getActivityMembers(conversationId, activityId);
});
}
/**
* Asynchronously lists the members of the current conversation.
*
* @param context The context object for the turn.
*
* @returns An array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for
* all users currently involved in a conversation.
*
* @remarks
* Returns an array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for
* all users currently involved in a conversation.
*
* This is different from [getActivityMembers](xref:botbuilder.BotFrameworkAdapter.getActivityMembers)
* in that it will return all members of the conversation, not just those directly involved in a specific activity.
*/
getConversationMembers(context) {
return __awaiter(this, void 0, void 0, function* () {
if (!context.activity.serviceUrl) {
throw new Error('BotFrameworkAdapter.getConversationMembers(): missing serviceUrl');
}
if (!context.activity.conversation || !context.activity.conversation.id) {
throw new Error('BotFrameworkAdapter.getConversationMembers(): missing conversation or conversation.id');
}
const serviceUrl = context.activity.serviceUrl;
const conversationId = context.activity.conversation.id;
const client = this.getOrCreateConnectorClient(context, serviceUrl, this.credentials);
return yield client.conversations.getConversationMembers(conversationId);
});
}
/**
* For the specified channel, asynchronously gets a page of the conversations in which this bot has participated.
*
* @param contextOrServiceUrl The URL of the channel server to query or a
* [TurnContext](xref:botbuilder-core.TurnContext) object from a conversation on the channel.
* @param continuationToken Optional. The continuation token from the previous page of results.
* Omit this parameter or use `undefined` to retrieve the first page of results.
*
* @returns A [ConversationsResult](xref:botframework-schema.ConversationsResult) object containing a page of results
* and a continuation token.
*
* @remarks
* The the return value's [conversations](xref:botframework-schema.ConversationsResult.conversations) property contains a page of
* [ConversationMembers](xref:botframework-schema.ConversationMembers) objects. Each object's
* [id](xref:botframework-schema.ConversationMembers.id) is the ID of a conversation in which the bot has participated on this channel.
* This method can be called from outside the context of a conversation, as only the bot's service URL and credentials are required.
*
* The channel batches results in pages. If the result's
* [continuationToken](xref:botframework-schema.ConversationsResult.continuationToken) property is not empty, then
* there are more pages to get. Use the returned token to get the next page of results.
* If the `contextOrServiceUrl` parameter is a [TurnContext](xref:botbuilder-core.TurnContext), the URL of the channel server is
* retrieved from
* `contextOrServiceUrl`.[activity](xref:botbuilder-core.TurnContext.activity).[serviceUrl](xref:botframework-schema.Activity.serviceUrl).
*/
getConversations(contextOrServiceUrl, continuationToken) {
return __awaiter(this, void 0, void 0, function* () {
let client;
if (typeof contextOrServiceUrl === 'object') {
const context = contextOrServiceUrl;
client = this.getOrCreateConnectorClient(context, context.activity.serviceUrl, this.credentials);
}
else {
client = this.createConnectorClient(contextOrServiceUrl);
}
return yield client.conversations.getConversations(continuationToken ? { continuationToken: continuationToken } : undefined);
});
}
/**
* Asynchronously attempts to retrieve the token for a user that's in a login flow.
*
* @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn.
* @param connectionName The name of the auth connection to use.
* @param magicCode Optional. The validation code the user entered.
* @param oAuthAppCredentials Optional. [AppCredentials](xref:botframework-connector.AppCredentials) for OAuth.
*
* @returns A [TokenResponse](xref:botframework-schema.TokenResponse) object that contains the user token.
*/
getUserToken(context, connectionName, magicCode, oAuthAppCredentials) {
return __awaiter(this, void 0, void 0, function* () {
if (!context.activity.from || !context.activity.from.id) {
throw new Error('BotFrameworkAdapter.getUserToken(): missing from or from.id');
}
if (!connectionName) {
throw new Error('getUserToken() requires a connectionName but none was provided.');
}
this.checkEmulatingOAuthCards(context);
const userId = context.activity.from.id;
const url = this.oauthApiUrl(context);
const client = this.createTokenApiClient(url, oAuthAppCredentials);
context.turnState.set(this.TokenApiClientCredentialsKey, client);
const result = yield client.userToken.getToken(userId, connectionName, { code: magicCode, channelId: context.activity.channelId });
if (!result || !result.token || result._response.status == 404) {
return undefined;
}
else {
return result;
}
});
}
/**
* Asynchronously signs out the user from the token server.
*
* @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn.
* @param connectionName Optional. The name of the auth connection to use.
* @param userId Optional. The ID of the user to sign out.
* @param oAuthAppCredentials Optional. [AppCredentials](xref:botframework-connector.AppCredentials) for OAuth.
*/
signOutUser(context, connectionName, userId, oAuthAppCredentials) {
return __awaiter(this, void 0, void 0, function* () {
if (!context.activity.from || !context.activity.from.id) {
throw new Error('BotFrameworkAdapter.signOutUser(): missing from or from.id');
}
if (!userId) {
userId = context.activity.from.id;
}
this.checkEmulatingOAuthCards(context);
const url = this.oauthApiUrl(context);
const client = this.createTokenApiClient(url, oAuthAppCredentials);
context.turnState.set(this.TokenApiClientCredentialsKey, client);
yield client.userToken.signOut(userId, {
connectionName: connectionName,
channelId: context.activity.channelId,
});
});
}
/**
* Asynchronously gets a sign-in link from the token server that can be sent as part
* of a [SigninCard](xref:botframework-schema.SigninCard).
*
* @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn.
* @param connectionName The name of the auth connection to use.
* @param oAuthAppCredentials Optional. [AppCredentials](xref:botframework-connector.AppCredentials) for OAuth.
* @param userId Optional. The user id that will be associated with the token.
* @param finalRedirect Optional. The final URL that the OAuth flow will redirect to.
* @returns The sign in link.
*/
getSignInLink(context, connectionName, oAuthAppCredentials, userId, finalRedirect) {
return __awaiter(this, void 0, void 0, function* () {
if (userId && userId != context.activity.from.id) {
throw new ReferenceError("cannot retrieve OAuth signin link for a user that's different from the conversation");
}
this.checkEmulatingOAuthCards(context);
const conversation = botbuilder_core_1.TurnContext.getConversationReference(context.activity);
const url = this.oauthApiUrl(context);
const client = this.createTokenApiClient(url, oAuthAppCredentials);
context.turnState.set(this.TokenApiClientCredentialsKey, client);
const state = {
ConnectionName: connectionName,
Conversation: conversation,
MsAppId: client.credentials.appId,
RelatesTo: context.activity.relatesTo,
};
const finalState = Buffer.from(JSON.stringify(state)).toString('base64');
return (yield client.botSignIn.getSignInUrl(finalState, { channelId: context.activity.channelId, finalRedirect }))._response.bodyAsText;
});
}
/**
* Asynchronously retrieves the token status for each configured connection for the given user.
*
* @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn.
* @param userId Optional. If present, the ID of the user to retrieve the token status for.
* Otherwise, the ID of the user who sent the current activity is used.
* @param includeFilter Optional. A comma-separated list of connection's to include. If present,
* the `includeFilter` parameter limits the tokens this method returns.
* @param oAuthAppCredentials Optional. [AppCredentials](xref:botframework-connector.AppCredentials) for OAuth.
*
* @returns The [TokenStatus](xref:botframework-connector.TokenStatus) objects retrieved.
*/
getTokenStatus(context, userId, includeFilter, oAuthAppCredentials) {
return __awaiter(this, void 0, void 0, function* () {
if (!userId && (!context.activity.from || !context.activity.from.id)) {
throw new Error('BotFrameworkAdapter.getTokenStatus(): missing from or from.id');
}
this.checkEmulatingOAuthCards(context);
userId = userId || context.activity.from.id;
const url = this.oauthApiUrl(context);
const client = this.createTokenApiClient(url, oAuthAppCredentials);
context.turnState.set(this.TokenApiClientCredentialsKey, client);
return (yield client.userToken.getTokenStatus(userId, {
channelId: context.activity.channelId,
include: includeFilter,
}))._response.parsedBody;
});
}
/**
* Asynchronously signs out the user from the token server.
*
* @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn.
* @param connectionName The name of the auth connection to use.
* @param resourceUrls The list of resource URLs to retrieve tokens for.
* @param oAuthAppCredentials Optional. [AppCredentials](xref:botframework-connector.AppCredentials) for OAuth.
*
* @returns A map of the [TokenResponse](xref:botframework-schema.TokenResponse) objects by resource URL.
*/
getAadTokens(context, connectionName, resourceUrls, oAuthAppCredentials) {
return __awaiter(this, void 0, void 0, function* () {
if (!context.activity.from || !context.activity.from.id) {
throw new Error('BotFrameworkAdapter.getAadTokens(): missing from or from.id');
}
this.checkEmulatingOAuthCards(context);
const userId = context.activity.from.id;
const url = this.oauthApiUrl(context);
const client = this.createTokenApiClient(url, oAuthAppCredentials);
context.turnState.set(this.TokenApiClientCredentialsKey, client);
return (yield client.userToken.getAadTokens(userId, connectionName, { resourceUrls: resourceUrls }, { channelId: context.activity.channelId }))._response.parsedBody;
});
}
/**
* Asynchronously Get the raw signin resource to be sent to the user for signin.
*
* @param context The context object for the turn.
* @param connectionName The name of the auth connection to use.
* @param userId The user id that will be associated with the token.
* @param finalRedirect The final URL that the OAuth flow will redirect to.
* @param appCredentials Optional. The CoreAppCredentials for OAuth.
* @returns The [BotSignInGetSignInResourceResponse](xref:botframework-connector.BotSignInGetSignInResourceResponse) object.
*/
getSignInResource(context, connectionName, userId, finalRedirect, appCredentials) {
return __awaiter(this, void 0, void 0, function* () {
if (!connectionName) {
throw new Error('getUserToken() requires a connectionName but none was provided.');
}
if (!context.activity.from || !context.activity.from.id) {
throw new Error('BotFrameworkAdapter.getSignInResource(): missing from or from.id');
}
// The provided userId doesn't match the from.id on the activity. (same for finalRedirect)
if (userId && context.activity.from.id !== userId) {
throw new Error('BotFrameworkAdapter.getSiginInResource(): cannot get signin resource for a user that is different from the conversation');
}
const url = this.oauthApiUrl(context);
const credentials = appCredentials;
const client = this.createTokenApiClient(url, credentials);
const conversation = botbuilder_core_1.TurnContext.getConversationReference(context.activity);
const state = {
ConnectionName: connectionName,
Conversation: conversation,
relatesTo: context.activity.relatesTo,
MSAppId: client.credentials.appId,
};
const finalState = Buffer.from(JSON.stringify(state)).toString('base64');
const options = { finalRedirect: finalRedirect };
return yield client.botSignIn.getSignInResource(finalState, options);
});
}
/**
* Asynchronously Performs a token exchange operation such as for single sign-on.
*
* @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn.
* @param connectionName Name of the auth connection to use.
* @param userId The user id that will be associated with the token.
* @param tokenExchangeRequest The [TokenExchangeRequest](xref:botbuilder-schema.TokenExchangeRequest), either a token to exchange or a uri to exchange.
* @param appCredentials Optional. [AppCredentials](xref:botframework-connector.AppCredentials) for OAuth.
* @returns A `Promise` representing the exchanged [TokenResponse](xref:botframework-schema.TokenResponse).
*/
exchangeToken(context, connectionName, userId, tokenExchangeRequest, appCredentials) {
return __awaiter(this, void 0, void 0, function* () {
if (!connectionName) {
throw new Error('exchangeToken() requires a connectionName but none was provided.');
}
if (!userId) {
throw new Error('exchangeToken() requires an userId but none was provided.');
}
if (tokenExchangeRequest && !tokenExchangeRequest.token && !tokenExchangeRequest.uri) {
throw new Error('BotFrameworkAdapter.exchangeToken(): Either a Token or Uri property is required on the TokenExchangeRequest');
}
const url = this.oauthApiUrl(context);
const client = this.createTokenApiClient(url, appCredentials);
return (yield client.userToken.exchangeAsync(userId, connectionName, context.activity.channelId, tokenExchangeRequest))._response.parsedBody;
});
}
/**
* Asynchronously sends an emulated OAuth card for a channel.
*
* This method supports the framework and is not intended to be called directly for your code.
*
* @param contextOrServiceUrl The URL of the emulator.
* @param emulate `true` to send an emulated OAuth card to the emulator; or `false` to not send the card.
*
* @remarks
* When testing a bot in the Bot Framework Emulator, this method can emulate the OAuth card interaction.
*/
emulateOAuthCards(contextOrServiceUrl, emulate) {
return __awaiter(this, void 0, void 0, function* () {
this.isEmulatingOAuthCards = emulate;
const url = this.oauthApiUrl(contextOrServiceUrl);
yield botframework_connector_1.EmulatorApiClient.emulateOAuthCards(this.credentials, url, emulate);
});
}
/**
* Asynchronously creates a turn context and runs the middleware pipeline for an incoming activity.
*
* @param req An Express or Restify style request object.
* @param res An Express or Restify style response object.
* @param logic The function to call at the end of the middleware pipeline.
*
* @remarks
* This is the main way a bot receives incoming messages and defines a turn in the conversation. This method:
*
* 1. Parses and authenticates an incoming request.
* - The activity is read from the body of the incoming request. An error will be returned
* if the activity can't be parsed.
* - The identity of the sender is authenticated as either the Emulator or a valid Microsoft
* server, using the bot's `appId` and `appPassword`. The request is rejected if the sender's
* identity is not verified.
* 1. Creates a [TurnContext](xref:botbuilder-core.TurnContext) object for the received activity.
* - This object is wrapped with a [revocable proxy](https://www.ecma-international.org/ecma-262/6.0/#sec-proxy.revocable).
* - When this method completes, the proxy is revoked.
* 1. Sends the turn context through the adapter's middleware pipeline.
* 1. Sends the turn context to the `logic` function.
* - The bot may perform additional routing or processing at this time.
* Returning a promise (or providing an `async` handler) will cause the adapter to wait for any asynchronous operations to complete.
* - After the `logic` function completes, the promise chain set up by the middleware is resolved.
*
* > [!TIP]
* > If you see the error `TypeError: Cannot perform 'set' on a proxy that has been revoked`
* > in your bot's console output, the likely cause is that an async function was used
* > without using the `await` keyword. Make sure all async functions use await!
*
* Middleware can _short circuit_ a turn. When this happens, subsequent middleware and the
* `logic` function is not called; however, all middleware prior to this point still run to completion.
* For more information about the middleware pipeline, see the
* [how bots work](https://docs.microsoft.com/azure/bot-service/bot-builder-basics) and
* [middleware](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-middleware) articles.
* Use the adapter's [use](xref:botbuilder-core.BotAdapter.use) method to add middleware to the adapter.
*
* For example:
* ```JavaScript
* server.post('/api/messages', (req, res) => {
* // Route received request to adapter for processing
* adapter.processActivity(req, res, async (context) => {
* // Process any messages received
* if (context.activity.type === ActivityTypes.Message) {
* await context.sendActivity(`Hello World`);
* }
* });
* });
* ```
*/
processActivity(req, res, logic) {
return __awaiter(this, void 0, void 0, function* () {
let body;
let status;
let processError;
try {
// Parse body of request
status = 400;
const request = yield parseRequest(req);
// Authenticate the incoming request
status = 401;
const authHeader = req.headers.authorization || req.headers.Authorization || '';
const identity = yield this.authenticateRequestInternal(request, authHeader);
// Set the correct callerId value and discard values received over the wire
request.callerId = yield this.generateCallerId(identity);
// Process received activity
status = 500;
const context = this.createContext(request);
context.turnState.set(this.BotIdentityKey, identity);
const connectorClient = yield this.createConnectorClientWithIdentity(request.serviceUrl, identity);
context.turnState.set(this.ConnectorClientKey, connectorClient);
const oAuthScope = botframework_connector_1.SkillValidation.isSkillClaim(identity.claims)
? botframework_connector_1.JwtTokenValidation.getAppIdFromClaims(identity.claims)
: this.credentials.oAuthScope;
context.turnState.set(this.OAuthScopeKey, oAuthScope);
context.turnState.set(botbuilder_core_1.BotCallbackHandlerKey, logic);
yield this.runMiddleware(context, logic);
// NOTE: The factoring of the code differs here when compared to C# as processActivity() returns Promise<void>.
// This is due to the fact that the response to the incoming activity is sent from inside this implementation.
// In C#, ProcessActivityAsync() returns Task<InvokeResponse> and ASP.NET handles sending of the response.
if (request.deliveryMode === botbuilder_core_1.DeliveryModes.ExpectReplies) {
// Handle "expectReplies" scenarios where all the activities have been buffered and sent back at once
// in an invoke response.
let activities = context.bufferedReplyActivities;
// If the channel is not the emulator, do not send trace activities.
// Fixes: https://github.com/microsoft/botbuilder-js/issues/2732
if (request.channelId !== botbuilder_core_1.Channels.Emulator) {
activities = activities.filter((a) => a.type !== botbuilder_core_1.ActivityTypes.Trace);
}
body = { activities };
status = botbuilder_core_1.StatusCodes.OK;
}
else if (request.type === botbuilder_core_1.ActivityTypes.Invoke) {
// Retrieve a cached Invoke response to handle Invoke scenarios.
// These scenarios deviate from the request/request model as the Bot should return a specific body and status.
const invokeResponse = context.turnState.get(botbuilder_core_1.INVOKE_RESPONSE_KEY);
if (invokeResponse && invokeResponse.value) {
const value = invokeResponse.value;
status = value.status;
body = value.body;
}
else {
status = botbuilder_core_1.StatusCodes.NOT_IMPLEMENTED;
}
}
else {
status = botbuilder_core_1.StatusCodes.OK;
}
}
catch (err) {
// Catch the error to try and throw the stacktrace out of processActivity()
processError = err;
body = err.toString();
}
// Return status
res.status(status);
if (body) {
res.send(body);
}
res.end();
// Check for an error
if (status >= 400) {
if (processError && processError.stack) {
throw new Error(`BotFrameworkAdapter.processActivity(): ${status} ERROR\n ${processError.stack}`);
}
else {
throw new Error(`BotFrameworkAdapter.processActivity(): ${status} ERROR`);
}
}
});
}
/**
* Asynchronously creates a turn context and runs the middleware pipeline for an incoming activity.
*
* @param activity The activity to process.
* @param logic The function to call at the end of the middleware pipeline.
*
* @remarks
* This is the main way a bot receives incoming messages and defines a turn in the conversation. This method:
*
* 1. Creates a [TurnContext](xref:botbuilder-core.TurnContext) object for the received activity.
* - This object is wrapped with a [revocable proxy](https://www.ecma-international.org/ecma-262/6.0/#sec-proxy.revocable).
* - When this method completes, the proxy is revoked.
* 1. Sends the turn context through the adapter's middleware pipeline.
* 1. Sends the turn context to the `logic` function.
* - The bot may perform additional routing or processing at this time.
* Returning a promise (or providing an `async` handler) will cause the adapter to wait for any asynchronous operations to complete.
* - After the `logic` function completes, the promise chain set up by the middleware is resolved.
*
* Middleware can _short circuit_ a turn. When this happens, subsequent middleware and the
* `logic` function is not called; however, all middleware prior to this point still run to completion.
* For more information about the middleware pipeline, see the
* [how bots work](https://docs.microsoft.com/azure/bot-service/bot-builder-basics) and
* [middleware](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-middleware) articles.
* Use the adapter's [use](xref:botbuilder-core.BotAdapter.use) method to add middleware to the adapter.
*/
processActivityDirect(activity, logic) {
return __awaiter(this, void 0, void 0, function* () {
let processError;
try {
// Process activity
const context = this.createContext(activity);
context.turnState.set(botbuilder_core_1.BotCallbackHandlerKey, logic);
yield this.runMiddleware(context, logic);
}
catch (err) {
// Catch the error to try and throw the stacktrace out of processActivity()
processError = err;
}
if (processError) {
if (processError && processError.stack) {
throw new Error(`BotFrameworkAdapter.processActivityDirect(): ERROR\n ${processError.stack}`);
}
else {
throw new Error('BotFrameworkAdapter.processActivityDirect(): ERROR');
}
}
});
}
/**
* Asynchronously sends a set of outgoing activities to a channel server.
*
* This method supports the framework and is not intended to be called directly for your code.
* Use the turn context's [sendActivity](xref:botbuilder-core.TurnContext.sendActivity) or
* [sendActivities](xref:botbuilder-core.TurnContext.sendActivities) method from your bot code.
*
* @param context The context object for the turn.
* @param activities The activities to send.
*
* @returns An array of [ResourceResponse](xref:)
*
* @remarks
* The activities will be sent one after another in the order in which they're received. A
* response object will be returned for each sent activity. For `message` activities this will
* contain the ID of the delivered message.
*/
sendActivities(context, activities) {
return __awaiter(this, void 0, void 0, function* () {
const responses = [];
for (let i = 0; i < activities.length; i++) {
const activity = activities[i];
switch (activity.type) {
case 'delay':
yield botbuilder_stdlib_1.delay(typeof activity.value === 'number' ? activity.value : 1000);
responses.push({});
break;
case botbuilder_core_1.ActivityTypes.InvokeResponse:
// Cache response to context object. This will be retrieved when turn completes.
context.turnState.set(botbuilder_core_1.INVOKE_RESPONSE_KEY, activity);
responses.push({});
break;
default: {
if (!activity.serviceUrl) {
throw new Error('BotFrameworkAdapter.sendActivity(): missing serviceUrl.');
}
if (!activity.conversation || !activity.conversation.id) {
throw new Error('BotFrameworkAdapter.sendActivity(): missing conversation id.');
}
if (activity && BotFrameworkAdapter.isStreamingServiceUrl(activity.serviceUrl)) {
if (!this.isStreamingConnectionOpen) {
throw new Error('BotFrameworkAdapter.sendActivities(): Unable to send activity as Streaming connection is closed.');
}
streaming_1.TokenResolver.checkForOAuthCards(this, context, activity);
}
const client = this.getOrCreateConnectorClient(context, activity.serviceUrl, this.credentials);
if (activity.type === botbuilder_core_1.ActivityTypes.Trace && activity.channelId !== botbuilder_core_1.Channels.Emulator) {
// Just eat activity
responses.push({});
}
else if (activity.replyToId) {
responses.push(yield client.conversations.replyToActivity(activity.conversation.id, activity.replyToId, activity));
}
else {
responses.push(yield client.conversations.sendToConversation(activity.conversation.id, activity));
}
break;
}
}
}
return responses;
});
}
/**
* Asynchronously replaces a previous activity with an updated version.
*
* This interface supports the framework and is not intended to be called directly for your code.
* Use [TurnContext.updateActivity](xref:botbuilder-core.TurnContext.updateActivity) to update
* an activity from your bot code.
*
*