UNPKG

botbuilder-core

Version:

Core components for Microsoft Bot Builder. Components in this library can run either in a browser or on the server.

296 lines (259 loc) 9.65 kB
/** * @module botbuilder */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { Activity, ActivityEventNames, ActivityTypes, ConversationReference, ResourceResponse, RoleTypes, } from 'botframework-schema'; import { Middleware } from './middlewareSet'; import { TurnContext } from './turnContext'; /** * Logs incoming and outgoing activities to a TranscriptStore. */ export class TranscriptLoggerMiddleware implements Middleware { private logger: TranscriptLogger; /** * Middleware for logging incoming and outgoing activities to a transcript store. * * @param logger Transcript logger */ constructor(logger: TranscriptLogger) { if (!logger) { throw new Error('TranscriptLoggerMiddleware requires a TranscriptLogger instance.'); } this.logger = logger; } /** * Initialization for middleware turn. * * @param context Context for the current turn of conversation with the user. * @param next Function to call at the end of the middleware chain. */ async onTurn(context: TurnContext, next: () => Promise<void>): Promise<void> { const transcript: Activity[] = []; // log incoming activity at beginning of turn if (context.activity) { if (!context.activity.from.role) { context.activity.from.role = RoleTypes.User; } this.logActivity(transcript, this.cloneActivity(context.activity)); } // hook up onSend pipeline context.onSendActivities( async (ctx: TurnContext, activities: Partial<Activity>[], next: () => Promise<ResourceResponse[]>) => { // Run full pipeline. const responses = await next(); activities.forEach((activity, index) => { const clonedActivity = this.cloneActivity(activity); clonedActivity.id = responses && responses[index] ? responses[index].id : clonedActivity.id; // For certain channels, a ResourceResponse with an id is not always sent to the bot. // This fix uses the timestamp on the activity to populate its id for logging the transcript. // If there is no outgoing timestamp, the current time for the bot is used for the activity.id. // See https://github.com/microsoft/botbuilder-js/issues/1122 if (!clonedActivity.id) { const prefix = `g_${Math.random().toString(36).slice(2, 8)}`; if (clonedActivity.timestamp) { clonedActivity.id = `${prefix}${new Date(clonedActivity.timestamp).getTime().toString()}`; } else { clonedActivity.id = `${prefix}${new Date().getTime().toString()}`; } } this.logActivity(transcript, clonedActivity); }); return responses; }, ); // hook up update activity pipeline context.onUpdateActivity(async (ctx: TurnContext, activity: Partial<Activity>, next: () => Promise<void>) => { // run full pipeline const response: void = await next(); // add Message Update activity const updateActivity = this.cloneActivity(activity); updateActivity.type = ActivityTypes.MessageUpdate; this.logActivity(transcript, updateActivity); return response; }); // hook up delete activity pipeline context.onDeleteActivity( async (ctx: TurnContext, reference: Partial<ConversationReference>, next: () => Promise<void>) => { // run full pipeline await next(); // add MessageDelete activity // log as MessageDelete activity const deleteActivity = TurnContext.applyConversationReference( { type: ActivityTypes.MessageDelete, id: reference.activityId, }, reference, false, ); this.logActivity(transcript, this.cloneActivity(deleteActivity)); }, ); // process bot logic await next(); // flush transcript at end of turn while (transcript.length) { try { // If the implementation of this.logger.logActivity() is asynchronous, we don't // await it as to not block processing of activities. // Because TranscriptLogger.logActivity() returns void or Promise<void>, we capture // the result and see if it is a Promise. const maybePromise = this.logger.logActivity(transcript.shift()); // If this.logger.logActivity() returns a Promise, a catch is added in case there // is no innate error handling in the method. This catch prevents // UnhandledPromiseRejectionWarnings from being thrown and prints the error to the // console. if (maybePromise instanceof Promise) { maybePromise.catch((err) => { this.transcriptLoggerErrorHandler(err); }); } } catch (err) { this.transcriptLoggerErrorHandler(err); } } } /** * Logs the Activity. * * @param transcript Array where the activity will be pushed. * @param activity Activity to log. */ private logActivity(transcript: Activity[], activity: Activity): void { if (!activity.timestamp) { activity.timestamp = new Date(); } // We should not log ContinueConversation events used by skills to initialize the middleware. if (!(activity.type === ActivityTypes.Event && activity.name === ActivityEventNames.ContinueConversation)) { transcript.push(activity); } } /** * Clones the Activity entity. * * @param activity Activity to clone. * @returns The cloned activity. */ private cloneActivity(activity: Partial<Activity>): Activity { return Object.assign(<Activity>{}, activity); } /** * Error logging helper function. * * @param err Error or object to console.error out. */ private transcriptLoggerErrorHandler(err: Error | any): void { // tslint:disable:no-console if (err instanceof Error) { console.error(`TranscriptLoggerMiddleware logActivity failed: "${err.message}"`); console.error(err.stack); } else { console.error(`TranscriptLoggerMiddleware logActivity failed: "${JSON.stringify(err)}"`); } // tslint:enable:no-console } } /** * ConsoleTranscriptLogger , writes activities to Console output. */ export class ConsoleTranscriptLogger implements TranscriptLogger { /** * Log an activity to the transcript. * * @param activity Activity being logged. */ logActivity(activity: Activity): void | Promise<void> { if (!activity) { throw new Error('Activity is required.'); } // tslint:disable-next-line:no-console console.log('Activity Log:', activity); } } /** * Transcript logger stores activities for conversations for recall. */ export interface TranscriptLogger { /** * Log an activity to the transcript. * * @param activity Activity being logged. */ logActivity(activity: Activity): void | Promise<void>; } /** * Transcript logger stores activities for conversations for recall. */ export interface TranscriptStore extends TranscriptLogger { /** * Get activities for a conversation (Aka the transcript) * * @param channelId Channel Id. * @param conversationId Conversation Id. * @param continuationToken Continuation token to page through results. * @param startDate Earliest time to include. */ getTranscriptActivities( channelId: string, conversationId: string, continuationToken?: string, startDate?: Date, ): Promise<PagedResult<Activity>>; /** * List conversations in the channelId. * * @param channelId Channel Id. * @param continuationToken Continuation token to page through results. */ listTranscripts(channelId: string, continuationToken?: string): Promise<PagedResult<TranscriptInfo>>; /** * Delete a specific conversation and all of its activities. * * @param channelId Channel Id where conversation took place. * @param conversationId Id of the conversation to delete. */ deleteTranscript(channelId: string, conversationId: string): Promise<void>; } /** * Metadata for a stored transcript. */ export interface TranscriptInfo { /** * ChannelId that the transcript was taken from. */ channelId: string; /** * Conversation Id. */ id: string; /** * Date conversation was started. */ created: Date; } /** * Page of results. * * @param T type of items being paged in. */ // tslint:disable-next-line:max-classes-per-file export interface PagedResult<T> { /** * Page of items. */ items: T[]; /** * Token used to page through multiple pages. */ continuationToken: string; }