UNPKG

type2docfx

Version:

A tool to convert json format output from TypeDoc to universal reference model for DocFx to consume.

575 lines (541 loc) 29.9 kB
/** * @module botbuilder */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { BotAdapter, TurnContext, Promiseable, ActivityTypes, Activity, ConversationReference, ResourceResponse, ConversationResourceResponse, ConversationParameters, ConversationAccount, TokenResponse, ConversationsResult, ChannelAccount } from 'botbuilder-core'; import { ConnectorClient, SimpleCredentialProvider, MicrosoftAppCredentials, JwtTokenValidation, OAuthApiClient } from 'botframework-connector'; /** * Express or Restify Request object. */ export interface WebRequest { body?: any; headers: any; on(event: string, ...args: any[]): any; } /** * Express or Restify Response object. */ export interface WebResponse { end(...args: any[]): any; send(status: number, body?: any): any; } /** * Bot Framework Adapter Settings. */ export interface BotFrameworkAdapterSettings { appId: string; appPassword: string; } /** * Response object expected to be sent in response to an `invoke` activity. */ export interface InvokeResponse { status: number; body?: any; } const pjson: any = require('../package.json'); const USER_AGENT = "Microsoft-BotFramework/3.1 (BotBuilder JS/" + pjson.version + ")"; const INVOKE_RESPONSE_KEY = Symbol('invokeResponse'); /** * ActivityAdapter class needed to communicate with a Bot Framework channel or the Emulator. * * @remarks * The following example shows the typical adapter setup: * * ```JavaScript * const { BotFrameworkAdapter } = require('botbuilder'); * * const adapter = new BotFrameworkAdapter({ * appId: process.env.MICROSOFT_APP_ID, * appPassword: process.env.MICROSOFT_APP_PASSWORD * }); * ``` */ export class BotFrameworkAdapter extends BotAdapter { protected readonly credentials: MicrosoftAppCredentials; protected readonly credentialsProvider: SimpleCredentialProvider; protected readonly settings: BotFrameworkAdapterSettings; /** * Creates a new BotFrameworkAdapter instance. * @param settings (optional) configuration settings for the adapter. */ constructor(settings?: Partial<BotFrameworkAdapterSettings>) { super(); this.settings = Object.assign({ appId: '', appPassword: '' }, settings); this.credentials = new MicrosoftAppCredentials(this.settings.appId, this.settings.appPassword || ''); this.credentialsProvider = new SimpleCredentialProvider(this.credentials.appId, this.credentials.appPassword); } /** * Continues a conversation with a user. This is often referred to as the bots "Proactive Messaging" * flow as its lets the bot proactively send messages to a conversation or user that its already * communicated with. Scenarios like sending notifications or coupons to a user are enabled by this * method. * * @remarks * The processing steps for this method are very similar to [processActivity()](#processactivity) * in that a `TurnContext` will be created which is then routed through the adapters middleware * before calling the passed in logic handler. The key difference being that since an activity * wasn't actually received it has to be created. The created activity will have its address * related fields populated but will have a `context.activity.type === undefined`. * * ```JavaScript * server.post('/api/notifyUser', async (req, res) => { * // Lookup previously saved conversation reference * const reference = await findReference(req.body.refId); * * // Proactively notify the user * if (reference) { * await adapter.continueConversation(reference, async (context) => { * await context.sendActivity(req.body.message); * }); * res.send(200); * } else { * res.send(404); * } * }); * ``` * @param reference A `ConversationReference` saved during a previous message from a user. This can be calculated for any incoming activity using `TurnContext.getConversationReference(context.activity)`. * @param logic A function handler that will be called to perform the bots logic after the the adapters middleware has been run. */ public continueConversation(reference: Partial<ConversationReference>, logic: (context: TurnContext) => Promiseable<void>): Promise<void> { const request = TurnContext.applyConversationReference({}, reference, true); const context = this.createContext(request); return this.runMiddleware(context, logic as any); } /** * Starts a new conversation with a user. This is typically used to Direct Message (DM) a member * of a group. * * @remarks * The processing steps for this method are very similar to [processActivity()](#processactivity) * in that a `TurnContext` will be created which is then routed through the adapters middleware * before calling the passed in logic handler. The key difference being that since an activity * wasn't actually received it has to be created. The created activity will have its address * related fields populated but will have a `context.activity.type === undefined`. * * ```JavaScript * // Get group members conversation reference * const reference = TurnContext.getConversationReference(context.activity); * * // Start a new conversation with the user * await adapter.createConversation(reference, async (ctx) => { * await ctx.sendActivity(`Hi (in private)`); * }); * ``` * @param reference A `ConversationReference` of the user to start a new conversation with. This can be calculated for any incoming activity using `TurnContext.getConversationReference(context.activity)`. * @param logic A function handler that will be called to perform the bots logic after the the adapters middleware has been run. */ public createConversation(reference: Partial<ConversationReference>, logic: (context: TurnContext) => Promiseable<void>): Promise<void> { try { if (!reference.serviceUrl) { throw new Error(`BotFrameworkAdapter.createConversation(): missing serviceUrl.`) } // Create conversation const parameters = { bot: reference.bot } as ConversationParameters; const client = this.createConnectorClient(reference.serviceUrl); return client.conversations.createConversation(parameters).then((response) => { // Initialize request and copy over new conversation ID and updated serviceUrl. const request = TurnContext.applyConversationReference({}, reference, true); request.conversation = { id: response.id } as ConversationAccount; if (response.serviceUrl) { request.serviceUrl = response.serviceUrl } // Create context and run middleware const context = this.createContext(request); return this.runMiddleware(context, logic as any); }); } catch (err) { return Promise.reject(err); } } /** * Deletes an activity that was previously sent to a channel. It should be noted that not all * channels support this feature. * * @remarks * Calling `TurnContext.deleteActivity()` is the preferred way of deleting activities as that * will ensure that any interested middleware has been notified. * @param context Context for the current turn of conversation with the user. * @param reference Conversation reference information for the activity being deleted. */ public deleteActivity(context: TurnContext, reference: Partial<ConversationReference>): Promise<void> { try { 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.createConnectorClient(reference.serviceUrl); return client.conversations.deleteActivity(reference.conversation.id, reference.activityId); } catch (err) { return Promise.reject(err); } } /** * Deletes a member from the current conversation. * @param context Context for the current turn of conversation with the user. * @param memberId ID of the member to delete from the conversation. */ public deleteConversationMember(context: TurnContext, memberId: string): Promise<void> { try { 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.createConnectorClient(serviceUrl); return client.conversations.deleteConversationMember(conversationId, memberId); } catch (err) { return Promise.reject(err); } } /** * Lists the members of a given activity. * @param context Context for the current turn of conversation with the user. * @param activityId (Optional) activity ID to enumerate. If not specified the current activities ID will be used. */ public getActivityMembers(context: TurnContext, activityId?: string): Promise<ChannelAccount[]> { try { 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.createConnectorClient(serviceUrl); return client.conversations.getActivityMembers(conversationId, activityId); } catch (err) { return Promise.reject(err); } } /** * Lists the members of the current conversation. * @param context Context for the current turn of conversation with the user. */ public getConversationMembers(context: TurnContext): Promise<ChannelAccount[]> { try { 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.createConnectorClient(serviceUrl); return client.conversations.getConversationMembers(conversationId); } catch (err) { return Promise.reject(err); } } /** * Lists the Conversations in which this bot has participated for a given channel server. The * channel server returns results in pages and each page will include a `continuationToken` * that can be used to fetch the next page of results from the server. * @param contextOrServiceUrl The URL of the channel server to query or a TurnContext. This can be retrieved from `context.activity.serviceUrl`. * @param continuationToken (Optional) token used to fetch the next page of results from the channel server. This should be left as `undefined` to retrieve the first page of results. */ public getConversations(contextOrServiceUrl: TurnContext|string, continuationToken?: string): Promise<ConversationsResult> { const url = typeof contextOrServiceUrl === 'object' ? contextOrServiceUrl.activity.serviceUrl : contextOrServiceUrl; const client = this.createConnectorClient(url); return client.conversations.getConversations(continuationToken ? { continuationToken: continuationToken } : undefined); } /** * Attempts to retrieve the token for a user that's in a signin flow. * @param context Context for the current turn of conversation with the user. * @param connectionName Name of the auth connection to use. * @param magicCode (Optional) Optional user entered code to validate. */ public getUserToken(context: TurnContext, connectionName: string, magicCode?: string): Promise<TokenResponse> { try { if (!context.activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.getUserToken(): missing serviceUrl`) } if (!context.activity.from || !context.activity.from.id) { throw new Error(`BotFrameworkAdapter.getUserToken(): missing from or from.id`) } const serviceUrl = context.activity.serviceUrl; const userId = context.activity.from.id; const client = this.createOAuthApiClient(serviceUrl); return client.getUserToken(userId, connectionName, magicCode); } catch (err) { return Promise.reject(err); } } /** * Signs the user out with the token server. * @param context Context for the current turn of conversation with the user. * @param connectionName Name of the auth connection to use. */ public signOutUser(context: TurnContext, connectionName: string): Promise<void> { try { if (!context.activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.signOutUser(): missing serviceUrl`) } if (!context.activity.from || !context.activity.from.id) { throw new Error(`BotFrameworkAdapter.signOutUser(): missing from or from.id`) } const serviceUrl = context.activity.serviceUrl; const userId = context.activity.from.id; const client = this.createOAuthApiClient(serviceUrl); return client.signOutUser(userId, connectionName); } catch (err) { return Promise.reject(err); } } /** * Gets a signin link from the token server that can be sent as part of a SigninCard. * @param context Context for the current turn of conversation with the user. * @param connectionName Name of the auth connection to use. */ public getSignInLink(context: TurnContext, connectionName: string): Promise<string> { try { if (!context.activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.getSignInLink(): missing serviceUrl`) } const conversation = TurnContext.getConversationReference(context.activity); const serviceUrl = context.activity.serviceUrl; const client = this.createOAuthApiClient(serviceUrl); return client.getSignInLink(conversation as ConversationReference, connectionName); } catch (err) { return Promise.reject(err); } } /** * Tells the token service to emulate the sending of OAuthCards for a channel. * @param contextOrServiceUrl The URL of the channel server to query or a TurnContext. This can be retrieved from `context.activity.serviceUrl`. * @param emulate If `true` the token service will emulate the sending of OAuthCards. */ public emulateOAuthCards(contextOrServiceUrl: TurnContext|string, emulate: boolean): Promise<void> { const url = typeof contextOrServiceUrl === 'object' ? contextOrServiceUrl.activity.serviceUrl : contextOrServiceUrl; const client = this.createOAuthApiClient(url); return client.emulateOAuthCards(emulate); } /** * Processes an activity received by the bots web server. This includes any messages sent from a * user and is the method that drives what's often referred to as the bots "Reactive Messaging" * flow. * * @remarks * The following steps will be taken to process the activity: * * - The identity of the sender will be verified to be either the Emulator or a valid Microsoft * server. The bots `appId` and `appPassword` will be used during this process and the request * will be rejected if the senders identity can't be verified. * - The activity will be parsed from the body of the incoming request. An error will be returned * if the activity can't be parsed. * - A `TurnContext` instance will be created for the received activity and wrapped with a * [Revocable Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/revocable). * - The context will be routed through any middleware registered with the adapter using * [use()](#use). Middleware is executed in the order in which it's added and any middleware * can intercept or prevent further routing of the context by simply not calling the passed * in `next()` function. This is called the "Leading Edge" of the request and middleware will * get a second chance to run on the "Trailing Edge" of the request after the bots logic has run. * - Assuming the context hasn't been intercepted by a piece of middleware, the context will be * passed to the logic handler passed in. The bot may perform an 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. * - Once the bots logic completes the promise chain setup by the middleware stack will be resolved * giving middleware a second chance to run on the "Trailing Edge" of the request. * - After the middleware stacks promise chain has been fully resolved the context object will be * `revoked()` and any future calls to the context will result in a `TypeError: Cannot perform * 'set' on a proxy that has been revoked` being thrown. * * ```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 === 'message') { * await context.sendActivity(`Hello World`); * } * }); * }); * ``` * @param req An Express or Restify style Request object. * @param res An Express or Restify style Response object. * @param logic A function handler that will be called to perform the bots logic after the received activity has been pre-processed by the adapter and routed through any middleware for processing. */ public processActivity(req: WebRequest, res: WebResponse, logic: (context: TurnContext) => Promiseable<any>): Promise<void> { // Parse body of request let errorCode = 500; return parseRequest(req).then((request) => { // Authenticate the incoming request errorCode = 401; const authHeader = req.headers["authorization"] || ''; return this.authenticateRequest(request, authHeader).then(() => { // Process received activity errorCode = 500; const context = this.createContext(request); return this.runMiddleware(context, logic as any) .then(() => { if (request.type === ActivityTypes.Invoke) { // Retrieve cached invoke response. const invokeResponse = context.services.get(INVOKE_RESPONSE_KEY); if (invokeResponse && invokeResponse.value) { const value = invokeResponse.value as InvokeResponse; res.send(value.status, value.body); res.end(); } else { throw new Error(`Bot failed to return a valid 'invokeResponse' activity.`); } } else { res.send(202); res.end(); } }); }); }).catch((err) => { // Reject response with error code console.warn(`BotFrameworkAdapter.processActivity(): ${errorCode} ERROR - ${err.toString()}`); res.send(errorCode, err.toString()); res.end(); throw err; }); } /** * Sends a set of activities to a channels server(s). 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. * * @remarks * Calling `TurnContext.sendActivities()` or `TurnContext.sendActivity()` is the preferred way of * sending activities as that will ensure that outgoing activities have been properly addressed * and that any interested middleware has been notified. * * The primary scenario for calling this method directly is when you want to explicitly bypass * going through any middleware. For instance, periodically sending a `typing` activity might * be a good reason to call this method directly as it would avoid any false signals from being * logged. * @param context Context for the current turn of conversation with the user. * @param activities List of activities to send. */ public sendActivities(context: TurnContext, activities: Partial<Activity>[]): Promise<ResourceResponse[]> { return new Promise((resolve, reject) => { const responses: ResourceResponse[] = []; const that = this; function next(i: number) { if (i < activities.length) { try { const activity = activities[i]; switch (activity.type) { case 'delay': setTimeout(() => { responses.push({} as ResourceResponse); next(i + 1); }, typeof activity.value === 'number' ? activity.value : 1000); break; case 'invokeResponse': // Cache response to context object. This will be retrieved when turn completes. context.services.set(INVOKE_RESPONSE_KEY, activity); responses.push({} as ResourceResponse); next(i + 1); 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.`) } let p: Promise<ResourceResponse>; const client = that.createConnectorClient(activity.serviceUrl); if (activity.type === 'trace' && activity.channelId !== 'emulator') { // Just eat activity p = Promise.resolve({} as ResourceResponse); } else if (activity.replyToId) { p = client.conversations.replyToActivity( activity.conversation.id, activity.replyToId, activity as Activity ); } else { p = client.conversations.sendToConversation( activity.conversation.id, activity as Activity ); } p.then((response) => { responses.push(response); next(i + 1); }, (err) => reject(err)); break; } } catch (err) { reject(err); } } else { resolve(responses); } } next(0); }); } /** * Replaces an activity that was previously sent to a channel. It should be noted that not all * channels support this feature. * * @remarks * Calling `TurnContext.updateActivity()` is the preferred way of updating activities as that * will ensure that any interested middleware has been notified. * @param context Context for the current turn of conversation with the user. * @param activity New activity to replace a current activity with. */ public updateActivity(context: TurnContext, activity: Partial<Activity>): Promise<void> { try { 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.createConnectorClient(activity.serviceUrl); return client.conversations.updateActivity( activity.conversation.id, activity.id, activity as Activity ).then(() => {}); } catch (err) { return Promise.reject(err); } } /** * Allows for the overriding of authentication in unit tests. * @param request Received request. * @param authHeader Received authentication header. */ protected authenticateRequest(request: Partial<Activity>, authHeader: string): Promise<void> { return JwtTokenValidation.assertValidActivity(request as Activity, authHeader, this.credentialsProvider); } /** * Allows for mocking of the connector client in unit tests. * @param serviceUrl Clients service url. */ protected createConnectorClient(serviceUrl: string): ConnectorClient { const client = new ConnectorClient(this.credentials, serviceUrl); client.addUserAgentInfo(USER_AGENT); return client; } /** * Allows for mocking of the OAuth API Client in unit tests. * @param serviceUrl Clients service url. */ protected createOAuthApiClient(serviceUrl: string): OAuthApiClient { return new OAuthApiClient(this.createConnectorClient(serviceUrl)); } /** * Allows for the overriding of the context object in unit tests and derived adapters. * @param request Received request. */ protected createContext(request: Partial<Activity>): TurnContext { return new TurnContext(this as any, request); } } function parseRequest(req: WebRequest): Promise<Activity> { return new Promise((resolve, reject) => { function returnActivity(activity: Activity) { if (typeof activity !== 'object') { throw new Error(`BotFrameworkAdapter.parseRequest(): invalid request body.`) } if (typeof activity.type !== 'string') { throw new Error(`BotFrameworkAdapter.parseRequest(): missing activity type.`) } resolve(activity); } if (req.body) { try { returnActivity(req.body); } catch (err) { reject(err); } } else { let requestData = ''; req.on('data', (chunk: string) => { requestData += chunk }); req.on('end', () => { try { req.body = JSON.parse(requestData); returnActivity(req.body); } catch (err) { reject(err); } }); } }); }