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
text/typescript
/**
* @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;
}