1492 lines
No EOL
80 KiB
JavaScript
1492 lines
No EOL
80 KiB
JavaScript
"use strict";
|
|
/**
|
|
* @module botbuilder
|
|
*/
|
|
/**
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
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 = __importStar(require("zod"));
|
|
const interfaces_1 = require("./interfaces");
|
|
const core_http_1 = require("@azure/core-http");
|
|
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 = (0, os_1.arch)();
|
|
const TYPE = (0, os_1.type)();
|
|
const RELEASE = (0, os_1.release)();
|
|
const NODE_VERSION = process.version;
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
const pjson = require('../package.json');
|
|
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 {
|
|
if (botframework_connector_1.JwtTokenValidation.isGovernment(this.settings.channelService)) {
|
|
this.credentials = new botframework_connector_1.MicrosoftGovernmentAppCredentials(this.settings.appId, this.settings.appPassword || '', this.settings.channelAuthTenant);
|
|
}
|
|
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 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.safeParse(oAuthScopeOrlogic).success) {
|
|
// 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 logicParse = zod_1.LogicT.safeParse(oAuthScopeOrlogic);
|
|
const logic = logicParse.success ? logicParse.data : 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 logicParse = zod_1.LogicT.safeParse(parametersOrLogic);
|
|
const parameterParse = botbuilder_core_1.conversationParametersObject.partial().safeParse(parametersOrLogic);
|
|
const parameters = parameterParse.success ? parameterParse.data : {};
|
|
const logic = logicParse.success ? logicParse.data : 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.
|
|
*
|
|
* Use [CloudAdapter.processActivityDirect] instead.
|
|
*
|
|
* @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 (0, 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.
|
|
*
|
|
* @param context The context object for the turn.
|
|
* @param activity The updated version of the activity to replace.
|
|
* @returns A `Promise` representing the [ResourceResponse](xref:botframework-schema.ResourceResponse) for the operation.
|
|
* @remarks
|
|
* Not all channels support this operation. For channels that don't, this call may throw an exception.
|
|
*/
|
|
updateActivity(context, activity) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (!activity.serviceUrl) {
|
|
throw new Error('BotFrameworkAdapter.updateActivity(): missing serviceUrl');
|
|
}
|
|
if (!activity.conversation || !activity.conversation.id) {
|
|
throw new Error('BotFrameworkAdapter.updateActivity(): missing conversation or conversation.id');
|
|
}
|
|
if (!activity.id) {
|
|
throw new Error('BotFrameworkAdapter.updateActivity(): missing activity.id');
|
|
}
|
|
const client = this.getOrCreateConnectorClient(context, activity.serviceUrl, this.credentials);
|
|
return client.conversations.updateActivity(activity.conversation.id, activity.id, activity);
|
|
});
|
|
}
|
|
/**
|
|
* Creates a connector client.
|
|
*
|
|
* @param serviceUrl The client's service URL.
|
|
* @returns The [ConnectorClient](xref:botbuilder-connector.ConnectorClient) instance.
|
|
* @remarks
|
|
* Override this in a derived class to create a mock connector client for unit testing.
|
|
*/
|
|
createConnectorClient(serviceUrl) {
|
|
return this.createConnectorClientInternal(serviceUrl, this.credentials);
|
|
}
|
|
/**
|
|
* Create a [ConnectorClient](xref:botbuilder-connector.ConnectorClient) with a [ClaimsIdentity](xref:botbuilder-connector.ClaimsIdentity).
|
|
*
|
|
* @remarks
|
|
* If the [ClaimsIdentity](xref:botbuilder-connector.ClaimsIdentity) contains the claims for a Skills request, create a [ConnectorClient](xref:botbuilder-connector.ConnectorClient) for use with Skills.
|
|
* Derives the correct audience from the [ClaimsIdentity](xref:botbuilder-connector.ClaimsIdentity), or the instance's credentials property.
|
|
* @param serviceUrl The client's service URL.
|
|
* @param identity [ClaimsIdentity](xref:botbuilder-connector.ClaimsIdentity).
|
|
* @param audience Optional. The recipient of the [ConnectorClient](xref:botbuilder-connector.ConnectorClient)'s messages. Normally the Bot Framework Channel Service or the AppId of another bot.
|
|
* @returns The client.
|
|
*/
|
|
createConnectorClientWithIdentity(serviceUrl, identity, audience) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (!identity) {
|
|
throw new Error('BotFrameworkAdapter.createConnectorClientWithIdentity(): invalid identity parameter.');
|
|
}
|
|
const botAppId = identity.getClaimValue(botframework_connector_1.AuthenticationConstants.AudienceClaim) ||
|
|
identity.getClaimValue(botframework_connector_1.AuthenticationConstants.AppIdClaim);
|
|
// Check if the audience is a string and when trimmed doesn't have a length of 0.
|
|
const validAudience = typeof audience === 'string' && audience.trim().length > 0;
|
|
const oAuthScope = validAudience ? audience : yield this.getOAuthScope(botAppId, identity.claims);
|
|
const credentials = yield this.buildCredentials(botAppId, oAuthScope);
|
|
const client = this.createConnectorClientInternal(serviceUrl, credentials);
|
|
return client;
|
|
});
|
|
}
|
|
createConnectorClientInternal(serviceUrl, credentials) {
|
|
if (BotFrameworkAdapter.isStreamingServiceUrl(serviceUrl)) {
|
|
// Check if we have a streaming server. Otherwise, requesting a connector client
|
|
// for a non-existent streaming connection results in an error
|
|
if (!this.streamingServer) {
|
|
throw new Error(`Cannot create streaming connector client for serviceUrl ${serviceUrl} without a streaming connection. Call 'useWebSocket' or 'useNamedPipe' to start a streaming connection.`);
|
|
}
|
|
const clientOptions = this.getClientOptions(serviceUrl, new streaming_1.StreamingHttpClient(this.streamingServer));
|
|
return new botframework_connector_1.ConnectorClient(credentials, clientOptions);
|
|
}
|
|
const clientOptions = this.getClientOptions(serviceUrl);
|
|
return new botframework_connector_1.ConnectorClient(credentials, clientOptions);
|
|
}
|
|
getClientOptions(serviceUrl, httpClient) {
|
|
var _a;
|
|
const _b = (_a = this.settings.clientOptions) !== null && _a !== void 0 ? _a : {}, { requestPolicyFactories } = _b, clientOptions = __rest(_b, ["requestPolicyFactories"]);
|
|
const options = Object.assign({}, { baseUri: serviceUrl }, clientOptions);
|
|
if (httpClient) {
|
|
options.httpClient = httpClient;
|
|
}
|
|
const userAgent = typeof options.userAgent === 'function' ? options.userAgent(exports.USER_AGENT) : options.userAgent;
|
|
const setUserAgent = (0, core_http_1.userAgentPolicy)({
|
|
value: `${exports.USER_AGENT}${userAgent !== null && userAgent !== void 0 ? userAgent : ''}`,
|
|
});
|
|
const acceptHeader = {
|
|
create: (nextPolicy) => ({
|
|
sendRequest: (httpRequest) => {
|
|
if (!httpRequest.headers.contains('accept')) {
|
|
httpRequest.headers.set('accept', '*/*');
|
|
}
|
|
return nextPolicy.sendRequest(httpRequest);
|
|
},
|
|
}),
|
|
};
|
|
// Resolve any user request policy factories, then include our user agent via a factory policy
|
|
options.requestPolicyFactories = (defaultRequestPolicyFactories) => {
|
|
let defaultFactories = [];
|
|
if (requestPolicyFactories) {
|
|
if (typeof requestPolicyFactories === 'function') {
|
|
const newDefaultFactories = requestPolicyFactories(defaultRequestPolicyFactories);
|
|
if (newDefaultFactories) {
|
|
defaultFactories = newDefaultFactories;
|
|
}
|
|
}
|
|
else if (requestPolicyFactories) {
|
|
defaultFactories = [...requestPolicyFactories];
|
|
}
|
|
// If the user has supplied custom factories, allow them to optionally set user agent
|
|
// before we do.
|
|
defaultFactories = [...defaultFactories, acceptHeader, setUserAgent];
|
|
}
|
|
else {
|
|
// In the case that there are no user supplied factories, inject our user agent as
|
|
// the first policy to ensure none of the default policies override it.
|
|
defaultFactories = [acceptHeader, setUserAgent, ...defaultRequestPolicyFactories];
|
|
}
|
|
return defaultFactories;
|
|
};
|
|
return options;
|
|
}
|
|
// Retrieves the ConnectorClient from the TurnContext or creates a new ConnectorClient with the provided serviceUrl and credentials.
|
|
getOrCreateConnectorClient(context, serviceUrl, credentials) {
|
|
if (!context || !context.turnState)
|
|
throw new Error('invalid context parameter');
|
|
if (!serviceUrl)
|
|
throw new Error('invalid serviceUrl');
|
|
if (!credentials)
|
|
throw new Error('invalid credentials');
|
|
let client = context.turnState.get(this.ConnectorClientKey);
|
|
// Inspect the retrieved client to confirm that the serviceUrl is correct, if it isn't, create a new one.
|
|
if (!client || client['baseUri'] !== serviceUrl) {
|
|
client = this.createConnectorClientInternal(serviceUrl, credentials);
|
|
}
|
|
return client;
|
|
}
|
|
/**
|
|
* Returns the correct [OAuthScope](xref:botframework-connector.AppCredentials.OAuthScope) for [AppCredentials](xref:botframework-connector.AppCredentials).
|
|
*
|
|
* @param botAppId The bot's AppId.
|
|
* @param claims The [Claim](xref:botbuilder-connector.Claim) list to check.
|
|
* @returns The current credentials' OAuthScope.
|
|
*/
|
|
getOAuthScope(botAppId, claims) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
// If the Claims are for skills, we need to create an AppCredentials instance with
|
|
// the correct scope for communication between the caller and the skill.
|
|
if (botAppId && botframework_connector_1.SkillValidation.isSkillClaim(claims)) {
|
|
return botframework_connector_1.JwtTokenValidation.getAppIdFromClaims(claims);
|
|
}
|
|
// Return the current credentials' OAuthScope.
|
|
return this.credentials.oAuthScope;
|
|
});
|
|
}
|
|
/**
|
|
*
|
|
* @remarks
|
|
* When building credentials for bot-to-bot communication, oAuthScope must be passed in.
|
|
* @param appId The application id.
|
|
* @param oAuthScope The optional OAuth scope.
|
|
* @returns The app credentials to be used to acquire tokens.
|
|
*/
|
|
buildCredentials(appId, oAuthScope) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
// There is no cache for AppCredentials in JS as opposed to C#.
|
|
// Instead of retrieving an AppCredentials from the Adapter instance, generate a new one
|
|
const appPassword = yield this.credentialsProvider.getAppPassword(appId);
|
|
let credentials;
|
|
if (this.settings.certificateThumbprint && this.settings.certificatePrivateKey) {
|
|
credentials = new botframework_connector_1.CertificateAppCredentials(appId, this.settings.certificateThumbprint, this.settings.certificatePrivateKey, this.settings.channelAuthTenant);
|
|
}
|
|
else {
|
|
if (botframework_connector_1.JwtTokenValidation.isGovernment(this.settings.channelService)) {
|
|
credentials = new botframework_connector_1.MicrosoftGovernmentAppCredentials(appId, appPassword, this.settings.channelAuthTenant, oAuthScope);
|
|
}
|
|
else {
|
|
credentials = new botframework_connector_1.MicrosoftAppCredentials(appId, appPassword, this.settings.channelAuthTenant, oAuthScope);
|
|
}
|
|
}
|
|
return credentials;
|
|
});
|
|
}
|
|
/**
|
|
* Creates an OAuth API client.
|
|
*
|
|
* @param serviceUrl The client's service URL.
|
|
* @param oAuthAppCredentials Optional. The [AppCredentials](xref:botframework-connector.AppCredentials)for OAuth.
|
|
* @remarks
|
|
* Override this in a derived class to create a mock OAuth API client for unit testing.
|
|
* @returns The client.
|
|
*/
|
|
createTokenApiClient(serviceUrl, oAuthAppCredentials) {
|
|
const tokenApiClientCredentials = oAuthAppCredentials ? oAuthAppCredentials : this.credentials;
|
|
const client = new botframework_connector_1.TokenApiClient(tokenApiClientCredentials, { baseUri: serviceUrl, userAgent: exports.USER_AGENT });
|
|
return client;
|
|
}
|
|
/**
|
|
* Allows for the overriding of authentication in unit tests.
|
|
*
|
|
* @param request Received request.
|
|
* @param authHeader Received authentication header.
|
|
*/
|
|
authenticateRequest(request, authHeader) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const identity = yield this.authenticateRequestInternal(request, authHeader);
|
|
if (!identity.isAuthenticated) {
|
|
throw new Error('Unauthorized Access. Request is not authorized');
|
|
}
|
|
// Set the correct callerId value and discard values received over the wire
|
|
request.callerId = yield this.generateCallerId(identity);
|
|
});
|
|
}
|
|
/**
|
|
* @ignore
|
|
* @private
|
|
* Returns the actual ClaimsIdentity from the JwtTokenValidation.authenticateRequest() call.
|
|
* @remarks
|
|
* This method is used instead of authenticateRequest() in processActivity() to obtain the ClaimsIdentity for caching in the TurnContext.turnState.
|
|
*
|
|
* @param request Received request.
|
|
* @param authHeader Received authentication header.
|
|
*/
|
|
authenticateRequestInternal(request, authHeader) {
|
|
return botframework_connector_1.JwtTokenValidation.authenticateRequest(request, authHeader, this.credentialsProvider, this.settings.channelService, this.authConfiguration);
|
|
}
|
|
/**
|
|
* Generates the CallerId property for the activity based on
|
|
* https://github.com/microsoft/botframework-obi/blob/main/protocols/botframework-activity/botframework-activity.md#appendix-v---caller-id-values.
|
|
*
|
|
* @param identity The inbound claims.
|
|
* @returns {Promise<string>} a promise representing the generated callerId.
|
|
*/
|
|
generateCallerId(identity) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (!identity) {
|
|
throw new TypeError('BotFrameworkAdapter.generateCallerId(): Missing identity parameter.');
|
|
}
|
|
// Is the bot accepting all incoming messages?
|
|
const isAuthDisabled = yield this.credentialsProvider.isAuthenticationDisabled();
|
|
if (isAuthDisabled) {
|
|
// Return undefined so that the callerId is cleared.
|
|
return;
|
|
}
|
|
// Is the activity from another bot?
|
|
if (botframework_connector_1.SkillValidation.isSkillClaim(identity.claims)) {
|
|
const callerId = botframework_connector_1.JwtTokenValidation.getAppIdFromClaims(identity.claims);
|
|
return `${botbuilder_core_1.CallerIdConstants.BotToBotPrefix}${callerId}`;
|
|
}
|
|
// Is the activity from Public Azure?
|
|
if (!this.settings.channelService || this.settings.channelService.length === 0) {
|
|
return botbuilder_core_1.CallerIdConstants.PublicAzureChannel;
|
|
}
|
|
// Is the activity from Azure Gov?
|
|
if (botframework_connector_1.JwtTokenValidation.isGovernment(this.settings.channelService)) {
|
|
return botbuilder_core_1.CallerIdConstants.USGovChannel;
|
|
}
|
|
// Return undefined so that the callerId is cleared.
|
|
});
|
|
}
|
|
/**
|
|
* Gets the OAuth API endpoint.
|
|
*
|
|
* @param contextOrServiceUrl The URL of the channel server to query or
|
|
* a [TurnContext](xref:botbuilder-core.TurnContext). For a turn context, the context's
|
|
* [activity](xref:botbuilder-core.TurnContext.activity).[serviceUrl](xref:botframework-schema.Activity.serviceUrl)
|
|
* is used for the URL.
|
|
* @returns The endpoint used for the API requests.
|
|
* @remarks
|
|
* Override this in a derived class to create a mock OAuth API endpoint for unit testing.
|
|
*/
|
|
oauthApiUrl(contextOrServiceUrl) {
|
|
return this.isEmulatingOAuthCards
|
|
? typeof contextOrServiceUrl === 'object'
|
|
? contextOrServiceUrl.activity.serviceUrl
|
|
: contextOrServiceUrl
|
|
: this.settings.oAuthEndpoint
|
|
? this.settings.oAuthEndpoint
|
|
: botframework_connector_1.JwtTokenValidation.isGovernment(this.settings.channelService)
|
|
? US_GOV_OAUTH_ENDPOINT
|
|
: OAUTH_ENDPOINT;
|
|
}
|
|
/**
|
|
* Checks the environment and can set a flag to emulate OAuth cards.
|
|
*
|
|
* @param context The context object for the turn.
|
|
* @remarks
|
|
* Override this in a derived class to control how OAuth cards are emulated for unit testing.
|
|
*/
|
|
checkEmulatingOAuthCards(context) {
|
|
if (!this.isEmulatingOAuthCards && context.activity.channelId === 'emulator' && !this.credentials.appId) {
|
|
this.isEmulatingOAuthCards = true;
|
|
}
|
|
}
|
|
/**
|
|
* Creates a turn context.
|
|
*
|
|
* @param request An incoming request body.
|
|
* @returns A new [TurnContext](xref:botbuilder-core.TurnContext) instance.
|
|
* @remarks
|
|
* Override this in a derived class to modify how the adapter creates a turn context.
|
|
*/
|
|
createContext(request) {
|
|
return new botbuilder_core_1.TurnContext(this, request);
|
|
}
|
|
/**
|
|
* Checks the validity of the request and attempts to map it the correct virtual endpoint,
|
|
* then generates and returns a response if appropriate.
|
|
*
|
|
* @param request A ReceiveRequest from the connected channel.
|
|
* @returns A response created by the BotAdapter to be sent to the client that originated the request.
|
|
*/
|
|
processRequest(request) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const response = new botframework_streaming_1.StreamingResponse();
|
|
if (!request) {
|
|
response.statusCode = botbuilder_core_1.StatusCodes.BAD_REQUEST;
|
|
response.setBody('No request provided.');
|
|
return response;
|
|
}
|
|
if (!request.verb || !request.path) {
|
|
response.statusCode = botbuilder_core_1.StatusCodes.BAD_REQUEST;
|
|
response.setBody(`Request missing verb and/or path. Verb: ${request.verb}. Path: ${request.path}`);
|
|
return response;
|
|
}
|
|
if (request.verb.toLocaleUpperCase() !== streaming_1.POST && request.verb.toLocaleUpperCase() !== streaming_1.GET) {
|
|
response.statusCode = botbuilder_core_1.StatusCodes.METHOD_NOT_ALLOWED;
|
|
response.setBody(`Invalid verb received. Only GET and POST are accepted. Verb: ${request.verb}`);
|
|
}
|
|
if (request.path.toLocaleLowerCase() === streaming_1.VERSION_PATH) {
|
|
return yield this.handleVersionRequest(request, response);
|
|
}
|
|
// Convert the StreamingRequest into an activity the Adapter can understand.
|
|
let body;
|
|
try {
|
|
body = yield this.readRequestBodyAsString(request);
|
|
}
|
|
catch (error) {
|
|
response.statusCode = botbuilder_core_1.StatusCodes.BAD_REQUEST;
|
|
response.setBody(`Request body missing or malformed: ${error}`);
|
|
return response;
|
|
}
|
|
if (request.path.toLocaleLowerCase() !== streaming_1.MESSAGES_PATH) {
|
|
response.statusCode = botbuilder_core_1.StatusCodes.NOT_FOUND;
|
|
response.setBody(`Path ${request.path.toLocaleLowerCase()} not not found. Expected ${streaming_1.MESSAGES_PATH}}.`);
|
|
return response;
|
|
}
|
|
if (request.verb.toLocaleUpperCase() !== streaming_1.POST) {
|
|
response.statusCode = botbuilder_core_1.StatusCodes.METHOD_NOT_ALLOWED;
|
|
response.setBody(`Invalid verb received for ${request.verb.toLocaleLowerCase()}. Only GET and POST are accepted. Verb: ${request.verb}`);
|
|
return response;
|
|
}
|
|
try {
|
|
const context = new botbuilder_core_1.TurnContext(this, body);
|
|
yield this.runMiddleware(context, this.logic);
|
|
if (body.type === botbuilder_core_1.ActivityTypes.Invoke) {
|
|
const invokeResponse = context.turnState.get(botbuilder_core_1.INVOKE_RESPONSE_KEY);
|
|
if (invokeResponse && invokeResponse.value) {
|
|
const value = invokeResponse.value;
|
|
response.statusCode = value.status;
|
|
if (value.body) {
|
|
response.setBody(value.body);
|
|
}
|
|
}
|
|
else {
|
|
response.statusCode = botbuilder_core_1.StatusCodes.NOT_IMPLEMENTED;
|
|
}
|
|
}
|
|
else if (body.deliveryMode === botbuilder_core_1.DeliveryModes.ExpectReplies) {
|
|
const replies = { activities: context.bufferedReplyActivities };
|
|
response.setBody(replies);
|
|
response.statusCode = botbuilder_core_1.StatusCodes.OK;
|
|
}
|
|
else {
|
|
response.statusCode = botbuilder_core_1.StatusCodes.OK;
|
|
}
|
|
}
|
|
catch (error) {
|
|
response.statusCode = botbuilder_core_1.StatusCodes.INTERNAL_SERVER_ERROR;
|
|
response.setBody(error);
|
|
return response;
|
|
}
|
|
return response;
|
|
});
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
process(req, resOrSocket, logicOrHead, maybeLogic) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (maybeLogic) {
|
|
return this.useWebSocket(req, zod_1.INodeSocketT.parse(resOrSocket), zod_1.INodeBufferT.parse(logicOrHead), zod_1.LogicT.parse(maybeLogic));
|
|
}
|
|
else {
|
|
return this.processActivity(req, interfaces_1.ResponseT.parse(resOrSocket), zod_1.LogicT.parse(logicOrHead));
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Connects the handler to a Named Pipe server and begins listening for incoming requests.
|
|
*
|
|
* @param logic The logic that will handle incoming requests.
|
|
* @param pipeName The name of the named pipe to use when creating the server.
|
|
* @param retryCount Number of times to attempt to bind incoming and outgoing pipe
|
|
* @param onListen Optional callback that fires once when server is listening on both incoming and outgoing pipe
|
|
*/
|
|
useNamedPipe(logic, pipeName = streaming_1.defaultPipeName, retryCount = 7, onListen) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (!logic) {
|
|
throw new Error('Bot logic needs to be provided to `useNamedPipe`');
|
|
}
|
|
if (this.isStreamingConnectionOpen) {
|
|
if (this.namedPipeName === pipeName) {
|
|
// Idempotent operation
|
|
return;
|
|
}
|
|
else {
|
|
// BotFrameworkAdapters are scoped to one streaming connection.
|
|
// Switching streams is an advanced scenario, one not innately supported by the SDK.
|
|
// Each BotFrameworkAdapter instance is scoped to a stream, so switching streams
|
|
// results in dropped conversations that the bot cannot reconnect to.
|
|
throw new Error('This BotFrameworkAdapter instance is already connected to a different stream. Use a new instance to connect to the provided pipeName.');
|
|
}
|
|
}
|
|
this.logic = logic;
|
|
yield (0, botbuilder_stdlib_1.retry)(() => this.startNamedPipeServer(pipeName, onListen), retryCount);
|
|
});
|
|
}
|
|
/**
|
|
* Process the initial request to establish a long lived connection via a streaming server.
|
|
*
|
|
* @param req The connection request.
|
|
* @param socket The raw socket connection between the bot (server) and channel/caller (client).
|
|
* @param head The first packet of the upgraded stream.
|
|
* @param logic The logic that handles incoming streaming requests for the lifetime of the WebSocket connection.
|
|
*/
|
|
useWebSocket(req, socket, head, logic) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
// Use the provided NodeWebSocketFactoryBase on BotFrameworkAdapter construction,
|
|
// otherwise create a new NodeWebSocketFactory.
|
|
const webSocketFactory = this.webSocketFactory || new botframework_streaming_1.NodeWebSocketFactory();
|
|
if (!logic) {
|
|
throw new Error('Streaming logic needs to be provided to `useWebSocket`');
|
|
}
|
|
this.logic = logic;
|
|
try {
|
|
yield this.authenticateConnection(req, this.settings.channelService);
|
|
}
|
|
catch (err) {
|
|
abortWebSocketUpgrade(socket, err);
|
|
throw err;
|
|
}
|
|
const nodeWebSocket = yield webSocketFactory.createWebSocket(req, socket, head);
|
|
yield this.startWebSocket(nodeWebSocket);
|
|
});
|
|
}
|
|
startNamedPipeServer(pipeName, onListen) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
this.namedPipeName = pipeName;
|
|
this.streamingServer = new botframework_streaming_1.NamedPipeServer(pipeName, this);
|
|
try {
|
|
yield this.streamingServer.start(onListen);
|
|
}
|
|
finally {
|
|
this.namedPipeName = undefined;
|
|
}
|
|
});
|
|
}
|
|
authenticateConnection(req, channelService) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (!this.credentials.appId) {
|
|
// auth is disabled
|
|
return;
|
|
}
|
|
const authHeader = req.headers.authorization || req.headers.Authorization || '';
|
|
const channelIdHeader = req.headers.channelid || req.headers.ChannelId || req.headers.ChannelID || '';
|
|
// Validate the received Upgrade request from the channel.
|
|
const claims = yield botframework_connector_1.JwtTokenValidation.validateAuthHeader(authHeader, this.credentialsProvider, channelService, channelIdHeader);
|
|
if (!claims.isAuthenticated) {
|
|
throw new botframework_connector_1.AuthenticationError('Unauthorized Access. Request is not authorized', botbuilder_core_1.StatusCodes.UNAUTHORIZED);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Connects the handler to a WebSocket server and begins listening for incoming requests.
|
|
*
|
|
* @param socket The socket to use when creating the server.
|
|
*/
|
|
startWebSocket(socket) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
this.streamingServer = new botframework_streaming_1.WebSocketServer(socket, this);
|
|
yield this.streamingServer.start();
|
|
});
|
|
}
|
|
readRequestBodyAsString(request) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const [activityStream, ...attachmentStreams] = request.streams;
|
|
const activity = yield activityStream.readAsJson();
|
|
activity.attachments = yield Promise.all(attachmentStreams.map((attachmentStream) => __awaiter(this, void 0, void 0, function* () {
|
|
const contentType = attachmentStream.contentType;
|
|
const content = contentType === 'application/json'
|
|
? yield attachmentStream.readAsJson()
|
|
: yield attachmentStream.readAsString();
|
|
return { contentType, content };
|
|
})));
|
|
return activity;
|
|
});
|
|
}
|
|
handleVersionRequest(request, response) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (request.verb.toLocaleUpperCase() === streaming_1.GET) {
|
|
response.statusCode = botbuilder_core_1.StatusCodes.OK;
|
|
if (!this.credentials.appId) {
|
|
response.setBody({ UserAgent: exports.USER_AGENT });
|
|
return response;
|
|
}
|
|
let token = '';
|
|
try {
|
|
token = yield this.credentials.getToken();
|
|
}
|
|
catch (err) {
|
|
/**
|
|
* In reality a missing BotToken will cause the channel to close the connection,
|
|
* but we still send the response and allow the channel to make that decision
|
|
* instead of proactively disconnecting. This allows the channel to know why
|
|
* the connection has been closed and make the choice not to make endless reconnection
|
|
* attempts that will end up right back here.
|
|
*/
|
|
console.error(err.message);
|
|
}
|
|
response.setBody({ UserAgent: exports.USER_AGENT, BotToken: token });
|
|
}
|
|
else {
|
|
response.statusCode = botbuilder_core_1.StatusCodes.METHOD_NOT_ALLOWED;
|
|
response.setBody(`Invalid verb received for path: ${request.path}. Only GET is accepted. Verb: ${request.verb}`);
|
|
}
|
|
return response;
|
|
});
|
|
}
|
|
/**
|
|
* Determine if the serviceUrl was sent via an Http/Https connection or Streaming
|
|
* This can be determined by looking at the ServiceUrl property:
|
|
* (1) All channels that send messages via http/https are not streaming
|
|
* (2) Channels that send messages via streaming have a ServiceUrl that does not begin with http/https.
|
|
*
|
|
* @param serviceUrl the serviceUrl provided in the resquest.
|
|
* @returns True if the serviceUrl is a streaming url, otherwise false.
|
|
*/
|
|
static isStreamingServiceUrl(serviceUrl) {
|
|
return serviceUrl && !serviceUrl.toLowerCase().startsWith('http');
|
|
}
|
|
}
|
|
exports.BotFrameworkAdapter = BotFrameworkAdapter;
|
|
/**
|
|
* Handles incoming webhooks from the botframework
|
|
* @private
|
|
* @param req incoming web request
|
|
*/
|
|
function parseRequest(req) {
|
|
return new Promise((resolve, reject) => {
|
|
if (req.body) {
|
|
try {
|
|
const activity = (0, activityValidator_1.validateAndFixActivity)(req.body);
|
|
resolve(activity);
|
|
}
|
|
catch (err) {
|
|
reject(err);
|
|
}
|
|
}
|
|
else {
|
|
let requestData = '';
|
|
req.on('data', (chunk) => {
|
|
requestData += chunk;
|
|
});
|
|
req.on('end', () => {
|
|
try {
|
|
req.body = JSON.parse(requestData);
|
|
const activity = (0, activityValidator_1.validateAndFixActivity)(req.body);
|
|
resolve(activity);
|
|
}
|
|
catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Creates an error message with status code to write to socket, then closes the connection.
|
|
*
|
|
* @param socket The raw socket connection between the bot (server) and channel/caller (client).
|
|
* @param err The error. If the error includes a status code, it will be included in the message written to the socket.
|
|
*/
|
|
function abortWebSocketUpgrade(socket, err) {
|
|
if (socket.writable) {
|
|
const connectionHeader = "Connection: 'close'\r\n";
|
|
let message = '';
|
|
if (botframework_connector_1.AuthenticationError.isStatusCodeError(err)) {
|
|
message = `HTTP/1.1 ${err.statusCode} ${botbuilder_core_1.StatusCodes[err.statusCode]}\r\n${err.message}\r\n${connectionHeader}\r\n`;
|
|
}
|
|
else {
|
|
message = botframework_connector_1.AuthenticationError.determineStatusCodeAndBuildMessage(err);
|
|
}
|
|
socket.write(message);
|
|
}
|
|
socket.destroy();
|
|
}
|
|
//# sourceMappingURL=botFrameworkAdapter.js.map
|