botbuilder-core
Version:
Core components for Microsoft Bot Builder. Components in this library can run either in a browser or on the server.
224 lines (194 loc) • 7.57 kB
text/typescript
/**
* @module botbuilder
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Activity } from 'botframework-schema';
import { PagedResult, TranscriptInfo, TranscriptStore } from './transcriptLogger';
/**
* The memory transcript store stores transcripts in volatile memory in a Map.
*
* @remarks
* Because this uses an unbounded volatile dictionary this should only be used for unit tests or
* non-production environments.
*/
export class MemoryTranscriptStore implements TranscriptStore {
private static readonly pageSize: number = 20;
private channels: Map<string, Map<string, Activity[]>> = new Map<string, Map<string, Activity[]>>();
/**
* Log an activity to the transcript.
*
* @param activity Activity to log.
* @returns {Promise<void>} A promise representing the async operation.
*/
logActivity(activity: Activity): void | Promise<void> {
if (!activity) {
throw new Error('activity cannot be null for logActivity()');
}
// get channel
let channel: Map<string, Activity[]>;
if (!this.channels.has(activity.channelId)) {
channel = new Map<string, Activity[]>();
this.channels.set(activity.channelId, channel);
} else {
channel = this.channels.get(activity.channelId);
}
// get conversation transcript
let transcript: Activity[];
if (!channel.has(activity.conversation.id)) {
transcript = [];
channel.set(activity.conversation.id, transcript);
} else {
transcript = channel.get(activity.conversation.id);
}
transcript.push(activity);
return Promise.resolve();
}
/**
* Get activities from the memory transcript store.
*
* @param channelId Channel Id.
* @param conversationId Conversation Id.
* @param continuationToken Continuation token to page through results.
* @param startDate Earliest time to include.
* @returns {Promise<PagedResult<Activity>>} A page of matching activities.
*/
getTranscriptActivities(
channelId: string,
conversationId: string,
continuationToken?: string,
startDate?: Date,
): Promise<PagedResult<Activity>> {
if (!channelId) {
throw new Error('Missing channelId');
}
if (!conversationId) {
throw new Error('Missing conversationId');
}
const pagedResult: PagedResult<Activity> = { items: [], continuationToken: undefined };
if (this.channels.has(channelId)) {
const channel: Map<string, Activity[]> = this.channels.get(channelId);
if (channel.has(conversationId)) {
const transcript: Activity[] = channel.get(conversationId);
if (continuationToken) {
pagedResult.items = transcript
.sort(timestampSorter)
.filter((a: Activity) => !startDate || a.timestamp >= startDate)
.filter(skipWhileExpression((a: Activity) => a.id !== continuationToken))
.slice(1, MemoryTranscriptStore.pageSize + 1);
} else {
pagedResult.items = transcript
.sort(timestampSorter)
.filter((a: Activity) => !startDate || a.timestamp >= startDate)
.slice(0, MemoryTranscriptStore.pageSize);
}
if (pagedResult.items.length === MemoryTranscriptStore.pageSize) {
pagedResult.continuationToken = pagedResult.items[pagedResult.items.length - 1].id;
}
}
}
return Promise.resolve(pagedResult);
}
/**
* List conversations in the channelId.
*
* @param channelId Channel Id.
* @param continuationToken Continuation token to page through results.
* @returns {Promise<PagedResult<TranscriptInfo>>} A page of conversations for a channel from the store.
*/
listTranscripts(channelId: string, continuationToken?: string): Promise<PagedResult<TranscriptInfo>> {
if (!channelId) {
throw new Error('Missing channelId');
}
const pagedResult: PagedResult<TranscriptInfo> = { items: [], continuationToken: undefined };
if (this.channels.has(channelId)) {
const channel: Map<string, Activity[]> = this.channels.get(channelId);
if (continuationToken) {
pagedResult.items = Array.from(channel.entries())
.map((kv: [string, Activity[]]) => ({
channelId,
id: kv[0],
created: getDate(kv[1]),
}))
.sort(createdSorter)
.filter(skipWhileExpression((a: any) => a.id !== continuationToken))
.slice(1, MemoryTranscriptStore.pageSize + 1);
} else {
pagedResult.items = Array.from(channel.entries())
.map((kv: [string, Activity[]]) => ({
channelId,
id: kv[0],
created: getDate(kv[1]),
}))
.sort(createdSorter)
.slice(0, MemoryTranscriptStore.pageSize);
}
if (pagedResult.items.length === MemoryTranscriptStore.pageSize) {
pagedResult.continuationToken = pagedResult.items[pagedResult.items.length - 1].id;
}
}
return Promise.resolve(pagedResult);
}
/**
* Delete a specific conversation and all of it's activities.
*
* @param channelId Channel Id where conversation took place.
* @param conversationId Id of the conversation to delete.
* @returns {Promise<void>} A promise representing the async operation.
*/
deleteTranscript(channelId: string, conversationId: string): Promise<void> {
if (!channelId) {
throw new Error('Missing channelId');
}
if (!conversationId) {
throw new Error('Missing conversationId');
}
if (this.channels.has(channelId)) {
const channel: Map<string, Activity[]> = this.channels.get(channelId);
if (channel.has(conversationId)) {
channel.delete(conversationId);
}
}
return Promise.resolve();
}
}
/**
* @private
*/
const createdSorter: (a: TranscriptInfo, b: TranscriptInfo) => number = (
a: TranscriptInfo,
b: TranscriptInfo,
): number => a.created.getTime() - b.created.getTime();
/**
* @private
*/
const timestampSorter: (a: Activity, b: Activity) => number = (a: Activity, b: Activity): number =>
a.timestamp.getTime() - b.timestamp.getTime();
/**
* @private
*/
const skipWhileExpression: (expression: any) => (item: any) => boolean = (
expression: any,
): ((item: any) => boolean) => {
let skipping = true;
return (item: any): boolean => {
if (!skipping) {
return true;
}
if (!expression(item)) {
skipping = false;
}
return !skipping;
};
};
/**
* @private
*/
const getDate: (activities: Activity[]) => Date = (activities: Activity[]): Date => {
if (activities && activities.length > 0) {
return activities[0].timestamp || new Date(0);
}
return new Date(0);
};