UNPKG

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