UNPKG

9.68 kBJavaScriptView Raw
1"use strict";
2var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 return new (P || (P = Promise))(function (resolve, reject) {
5 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 step((generator = generator.apply(thisArg, _arguments || [])).next());
9 });
10};
11Object.defineProperty(exports, "__esModule", { value: true });
12exports.FileTranscriptStore = void 0;
13/**
14 * @module botbuilder
15 */
16/**
17 * Copyright (c) Microsoft Corporation. All rights reserved.
18 * Licensed under the MIT License.
19 */
20const path_1 = require("path");
21const fs_extra_1 = require("fs-extra");
22// eslint-disable-next-line @typescript-eslint/no-var-requires
23const filenamify = require('filenamify');
24/**
25 * @private
26 * The number of .net ticks at the unix epoch.
27 */
28const epochTicks = 621355968000000000;
29/**
30 * @private
31 * There are 10000 .net ticks per millisecond.
32 */
33const ticksPerMillisecond = 10000;
34/**
35 * @private
36 * @param timestamp A date used to calculate future ticks.
37 */
38function getTicks(timestamp) {
39 const ticks = epochTicks + timestamp.getTime() * ticksPerMillisecond;
40 return ticks.toString(16);
41}
42/**
43 * @private
44 * @param ticks A string containing ticks.
45 */
46function readDate(ticks) {
47 const t = Math.round((parseInt(ticks, 16) - epochTicks) / ticksPerMillisecond);
48 return new Date(t);
49}
50/**
51 * @private
52 * @param date A date used to create a filter.
53 * @param fileName The filename containing the timestamp string
54 */
55function withDateFilter(date, fileName) {
56 if (!date) {
57 return true;
58 }
59 const ticks = fileName.split('-')[0];
60 return readDate(ticks) >= date;
61}
62/**
63 * @private
64 * @param expression A function that will be used to test items.
65 */
66function includeWhen(expression) {
67 let shouldInclude = false;
68 return (item) => {
69 return shouldInclude || (shouldInclude = expression(item));
70 };
71}
72/**
73 * @private
74 * @param json A JSON string to be parsed into an activity.
75 */
76function parseActivity(json) {
77 const activity = JSON.parse(json);
78 activity.timestamp = new Date(activity.timestamp);
79 return activity;
80}
81/**
82 * The file transcript store stores transcripts in file system with each activity as a file.
83 *
84 * @remarks
85 * This class provides an interface to log all incoming and outgoing activities to the filesystem.
86 * It implements the features necessary to work alongside the TranscriptLoggerMiddleware plugin.
87 * When used in concert, your bot will automatically log all conversations.
88 *
89 * Below is the boilerplate code needed to use this in your app:
90 * ```javascript
91 * const { FileTranscriptStore, TranscriptLoggerMiddleware } = require('botbuilder');
92 *
93 * adapter.use(new TranscriptLoggerMiddleware(new FileTranscriptStore(__dirname + '/transcripts/')));
94 * ```
95 */
96class FileTranscriptStore {
97 /**
98 * Creates an instance of FileTranscriptStore.
99 * @param folder Root folder where transcript will be stored.
100 */
101 constructor(folder) {
102 if (!folder) {
103 throw new Error('Missing folder.');
104 }
105 this.rootFolder = folder;
106 }
107 /**
108 * Log an activity to the transcript.
109 * @param activity Activity being logged.
110 */
111 logActivity(activity) {
112 return __awaiter(this, void 0, void 0, function* () {
113 if (!activity) {
114 throw new Error('activity cannot be null for logActivity()');
115 }
116 const conversationFolder = this.getTranscriptFolder(activity.channelId, activity.conversation.id);
117 const activityFileName = this.getActivityFilename(activity);
118 return this.saveActivity(activity, conversationFolder, activityFileName);
119 });
120 }
121 /**
122 * Get all activities associated with a conversation id (aka get the transcript).
123 * @param channelId Channel Id.
124 * @param conversationId Conversation Id.
125 * @param continuationToken (Optional) Continuation token to page through results.
126 * @param startDate (Optional) Earliest time to include.
127 */
128 getTranscriptActivities(channelId, conversationId, continuationToken, startDate) {
129 return __awaiter(this, void 0, void 0, function* () {
130 if (!channelId) {
131 throw new Error('Missing channelId');
132 }
133 if (!conversationId) {
134 throw new Error('Missing conversationId');
135 }
136 const pagedResult = { items: [], continuationToken: undefined };
137 const transcriptFolder = this.getTranscriptFolder(channelId, conversationId);
138 const exists = yield fs_extra_1.pathExists(transcriptFolder);
139 if (!exists) {
140 return pagedResult;
141 }
142 const transcriptFolderContents = yield fs_extra_1.readdir(transcriptFolder);
143 const include = includeWhen((fileName) => !continuationToken || path_1.parse(fileName).name === continuationToken);
144 const items = transcriptFolderContents.filter((transcript) => transcript.endsWith('.json') && withDateFilter(startDate, transcript) && include(transcript));
145 pagedResult.items = yield Promise.all(items
146 .slice(0, FileTranscriptStore.PageSize)
147 .sort()
148 .map((activityFilename) => __awaiter(this, void 0, void 0, function* () {
149 const json = yield fs_extra_1.readFile(path_1.join(transcriptFolder, activityFilename), 'utf8');
150 return parseActivity(json);
151 })));
152 const { length } = pagedResult.items;
153 if (length === FileTranscriptStore.PageSize && items[length]) {
154 pagedResult.continuationToken = path_1.parse(items[length]).name;
155 }
156 return pagedResult;
157 });
158 }
159 /**
160 * List all the logged conversations for a given channelId.
161 * @param channelId Channel Id.
162 * @param continuationToken (Optional) Continuation token to page through results.
163 */
164 listTranscripts(channelId, continuationToken) {
165 return __awaiter(this, void 0, void 0, function* () {
166 if (!channelId) {
167 throw new Error('Missing channelId');
168 }
169 const pagedResult = { items: [], continuationToken: undefined };
170 const channelFolder = this.getChannelFolder(channelId);
171 const exists = yield fs_extra_1.pathExists(channelFolder);
172 if (!exists) {
173 return pagedResult;
174 }
175 const channels = yield fs_extra_1.readdir(channelFolder);
176 const items = channels.filter(includeWhen((di) => !continuationToken || di === continuationToken));
177 pagedResult.items = items
178 .slice(0, FileTranscriptStore.PageSize)
179 .map((i) => ({ channelId: channelId, id: i, created: null }));
180 const { length } = pagedResult.items;
181 if (length === FileTranscriptStore.PageSize && items[length]) {
182 pagedResult.continuationToken = items[length];
183 }
184 return pagedResult;
185 });
186 }
187 /**
188 * Delete a conversation and all of it's activities.
189 * @param channelId Channel Id where conversation took place.
190 * @param conversationId Id of the conversation to delete.
191 */
192 deleteTranscript(channelId, conversationId) {
193 return __awaiter(this, void 0, void 0, function* () {
194 if (!channelId) {
195 throw new Error('Missing channelId');
196 }
197 if (!conversationId) {
198 throw new Error('Missing conversationId');
199 }
200 const transcriptFolder = this.getTranscriptFolder(channelId, conversationId);
201 return fs_extra_1.remove(transcriptFolder);
202 });
203 }
204 /**
205 * Saves the [Activity](xref:botframework-schema.Activity) as a JSON file.
206 * @param activity The [Activity](xref:botframework-schema.Activity) to transcript.
207 * @param transcriptPath The path where the transcript will be saved.
208 * @param activityFilename The name for the file.
209 */
210 saveActivity(activity, transcriptPath, activityFilename) {
211 return __awaiter(this, void 0, void 0, function* () {
212 const json = JSON.stringify(activity, null, '\t');
213 const exists = yield fs_extra_1.pathExists(transcriptPath);
214 if (!exists) {
215 yield fs_extra_1.mkdirp(transcriptPath);
216 }
217 return fs_extra_1.writeFile(path_1.join(transcriptPath, activityFilename), json, 'utf8');
218 });
219 }
220 /**
221 * @private
222 */
223 getActivityFilename(activity) {
224 return `${getTicks(activity.timestamp)}-${this.sanitizeKey(activity.id)}.json`;
225 }
226 /**
227 * @private
228 */
229 getChannelFolder(channelId) {
230 return path_1.join(this.rootFolder, this.sanitizeKey(channelId));
231 }
232 /**
233 * @private
234 */
235 getTranscriptFolder(channelId, conversationId) {
236 return path_1.join(this.rootFolder, this.sanitizeKey(channelId), this.sanitizeKey(conversationId));
237 }
238 /**
239 * @private
240 */
241 sanitizeKey(key) {
242 return filenamify(key);
243 }
244}
245exports.FileTranscriptStore = FileTranscriptStore;
246FileTranscriptStore.PageSize = 20;
247//# sourceMappingURL=fileTranscriptStore.js.map
\No newline at end of file