264 lines
No EOL
10 KiB
JavaScript
264 lines
No EOL
10 KiB
JavaScript
"use strict";
|
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
});
|
|
};
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.FileTranscriptStore = void 0;
|
|
/**
|
|
* @module botbuilder
|
|
*/
|
|
/**
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
const path_1 = require("path");
|
|
const fs_extra_1 = require("fs-extra");
|
|
const index_1 = __importDefault(require("../vendors/filenamify/index"));
|
|
/**
|
|
* @private
|
|
* The number of .net ticks at the unix epoch.
|
|
*/
|
|
const epochTicks = 621355968000000000;
|
|
/**
|
|
* @private
|
|
* There are 10000 .net ticks per millisecond.
|
|
*/
|
|
const ticksPerMillisecond = 10000;
|
|
/**
|
|
* @private
|
|
* @param timestamp A date used to calculate future ticks.
|
|
*/
|
|
function getTicks(timestamp) {
|
|
const ticks = epochTicks + timestamp.getTime() * ticksPerMillisecond;
|
|
return ticks.toString(16);
|
|
}
|
|
/**
|
|
* @private
|
|
* @param ticks A string containing ticks.
|
|
*/
|
|
function readDate(ticks) {
|
|
const t = Math.round((parseInt(ticks, 16) - epochTicks) / ticksPerMillisecond);
|
|
return new Date(t);
|
|
}
|
|
/**
|
|
* @private
|
|
* @param date A date used to create a filter.
|
|
* @param fileName The filename containing the timestamp string
|
|
*/
|
|
function withDateFilter(date, fileName) {
|
|
if (!date) {
|
|
return true;
|
|
}
|
|
const ticks = fileName.split('-')[0];
|
|
return readDate(ticks) >= date;
|
|
}
|
|
/**
|
|
* @private
|
|
* @param expression A function that will be used to test items.
|
|
*/
|
|
function includeWhen(expression) {
|
|
let shouldInclude = false;
|
|
return (item) => {
|
|
return shouldInclude || (shouldInclude = expression(item));
|
|
};
|
|
}
|
|
/**
|
|
* @private
|
|
* @param json A JSON string to be parsed into an activity.
|
|
*/
|
|
function parseActivity(json) {
|
|
const activity = JSON.parse(json);
|
|
activity.timestamp = new Date(activity.timestamp);
|
|
return activity;
|
|
}
|
|
/**
|
|
* The file transcript store stores transcripts in file system with each activity as a file.
|
|
*
|
|
* @remarks
|
|
* This class provides an interface to log all incoming and outgoing activities to the filesystem.
|
|
* It implements the features necessary to work alongside the TranscriptLoggerMiddleware plugin.
|
|
* When used in concert, your bot will automatically log all conversations.
|
|
*
|
|
* Below is the boilerplate code needed to use this in your app:
|
|
* ```javascript
|
|
* const { FileTranscriptStore, TranscriptLoggerMiddleware } = require('botbuilder');
|
|
*
|
|
* adapter.use(new TranscriptLoggerMiddleware(new FileTranscriptStore(__dirname + '/transcripts/')));
|
|
* ```
|
|
*/
|
|
class FileTranscriptStore {
|
|
/**
|
|
* Creates an instance of FileTranscriptStore.
|
|
*
|
|
* @param folder Root folder where transcript will be stored.
|
|
*/
|
|
constructor(folder) {
|
|
if (!folder) {
|
|
throw new Error('Missing folder.');
|
|
}
|
|
this.rootFolder = folder;
|
|
}
|
|
/**
|
|
* Log an activity to the transcript.
|
|
*
|
|
* @param activity Activity being logged.
|
|
* @returns {Promise<void>} a promise representing the asynchronous operation.
|
|
*/
|
|
logActivity(activity) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (!activity) {
|
|
throw new Error('activity cannot be null for logActivity()');
|
|
}
|
|
const conversationFolder = this.getTranscriptFolder(activity.channelId, activity.conversation.id);
|
|
const activityFileName = this.getActivityFilename(activity);
|
|
return this.saveActivity(activity, conversationFolder, activityFileName);
|
|
});
|
|
}
|
|
/**
|
|
* Get all activities associated with a conversation id (aka get the transcript).
|
|
*
|
|
* @param channelId Channel Id.
|
|
* @param conversationId Conversation Id.
|
|
* @param continuationToken (Optional) Continuation token to page through results.
|
|
* @param startDate (Optional) Earliest time to include.
|
|
* @returns {Promise<PagedResult<Activity>>} PagedResult of activities.
|
|
*/
|
|
getTranscriptActivities(channelId, conversationId, continuationToken, startDate) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (!channelId) {
|
|
throw new Error('Missing channelId');
|
|
}
|
|
if (!conversationId) {
|
|
throw new Error('Missing conversationId');
|
|
}
|
|
const pagedResult = { items: [], continuationToken: undefined };
|
|
const transcriptFolder = this.getTranscriptFolder(channelId, conversationId);
|
|
const exists = yield (0, fs_extra_1.pathExists)(transcriptFolder);
|
|
if (!exists) {
|
|
return pagedResult;
|
|
}
|
|
//eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
const transcriptFolderContents = yield (0, fs_extra_1.readdir)(transcriptFolder);
|
|
const include = includeWhen((fileName) => !continuationToken || (0, path_1.parse)(fileName).name === continuationToken);
|
|
const items = transcriptFolderContents.filter((transcript) => transcript.endsWith('.json') && withDateFilter(startDate, transcript) && include(transcript));
|
|
pagedResult.items = yield Promise.all(items
|
|
.slice(0, FileTranscriptStore.PageSize)
|
|
.sort()
|
|
.map((activityFilename) => __awaiter(this, void 0, void 0, function* () {
|
|
//eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
const json = yield (0, fs_extra_1.readFile)((0, path_1.join)(transcriptFolder, activityFilename), 'utf8');
|
|
return parseActivity(json);
|
|
})));
|
|
const { length } = pagedResult.items;
|
|
if (length === FileTranscriptStore.PageSize && items[length]) {
|
|
pagedResult.continuationToken = (0, path_1.parse)(items[length]).name;
|
|
}
|
|
return pagedResult;
|
|
});
|
|
}
|
|
/**
|
|
* List all the logged conversations for a given channelId.
|
|
*
|
|
* @param channelId Channel Id.
|
|
* @param continuationToken (Optional) Continuation token to page through results.
|
|
* @returns {Promise<PagedResult<TranscriptInfo>>} PagedResult of transcripts.
|
|
*/
|
|
listTranscripts(channelId, continuationToken) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (!channelId) {
|
|
throw new Error('Missing channelId');
|
|
}
|
|
const pagedResult = { items: [], continuationToken: undefined };
|
|
const channelFolder = this.getChannelFolder(channelId);
|
|
const exists = yield (0, fs_extra_1.pathExists)(channelFolder);
|
|
if (!exists) {
|
|
return pagedResult;
|
|
}
|
|
//eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
const channels = yield (0, fs_extra_1.readdir)(channelFolder);
|
|
const items = channels.filter(includeWhen((di) => !continuationToken || di === continuationToken));
|
|
pagedResult.items = items
|
|
.slice(0, FileTranscriptStore.PageSize)
|
|
.map((i) => ({ channelId: channelId, id: i, created: null }));
|
|
const { length } = pagedResult.items;
|
|
if (length === FileTranscriptStore.PageSize && items[length]) {
|
|
pagedResult.continuationToken = items[length];
|
|
}
|
|
return pagedResult;
|
|
});
|
|
}
|
|
/**
|
|
* Delete a 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 asynchronous operation.
|
|
*/
|
|
deleteTranscript(channelId, conversationId) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (!channelId) {
|
|
throw new Error('Missing channelId');
|
|
}
|
|
if (!conversationId) {
|
|
throw new Error('Missing conversationId');
|
|
}
|
|
const transcriptFolder = this.getTranscriptFolder(channelId, conversationId);
|
|
return (0, fs_extra_1.remove)(transcriptFolder);
|
|
});
|
|
}
|
|
/**
|
|
* Saves the [Activity](xref:botframework-schema.Activity) as a JSON file.
|
|
*
|
|
* @param activity The [Activity](xref:botframework-schema.Activity) to transcript.
|
|
* @param transcriptPath The path where the transcript will be saved.
|
|
* @param activityFilename The name for the file.
|
|
* @returns {Promise<void>} A promise representing the asynchronous operation.
|
|
*/
|
|
saveActivity(activity, transcriptPath, activityFilename) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const json = JSON.stringify(activity, null, '\t');
|
|
const exists = yield (0, fs_extra_1.pathExists)(transcriptPath);
|
|
if (!exists) {
|
|
yield (0, fs_extra_1.mkdirp)(transcriptPath);
|
|
}
|
|
//eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
return (0, fs_extra_1.writeFile)((0, path_1.join)(transcriptPath, activityFilename), json, 'utf8');
|
|
});
|
|
}
|
|
/**
|
|
* @private
|
|
*/
|
|
getActivityFilename(activity) {
|
|
return `${getTicks(activity.timestamp)}-${this.sanitizeKey(activity.id)}.json`;
|
|
}
|
|
/**
|
|
* @private
|
|
*/
|
|
getChannelFolder(channelId) {
|
|
return (0, path_1.join)(this.rootFolder, this.sanitizeKey(channelId));
|
|
}
|
|
/**
|
|
* @private
|
|
*/
|
|
getTranscriptFolder(channelId, conversationId) {
|
|
return (0, path_1.join)(this.rootFolder, this.sanitizeKey(channelId), this.sanitizeKey(conversationId));
|
|
}
|
|
/**
|
|
* @private
|
|
*/
|
|
sanitizeKey(key) {
|
|
return (0, index_1.default)(key);
|
|
}
|
|
}
|
|
exports.FileTranscriptStore = FileTranscriptStore;
|
|
FileTranscriptStore.PageSize = 20;
|
|
//# sourceMappingURL=fileTranscriptStore.js.map
|