UNPKG

botbuilder-azure

Version:

Azure extensions for Microsoft BotBuilder.

309 lines 14.8 kB
"use strict"; /** * @module botbuilder-azure */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ 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.AzureBlobTranscriptStore = exports.checkedCollectionsKey = void 0; const storage_blob_1 = require("@azure/storage-blob"); const botbuilder_stdlib_1 = require("botbuilder-stdlib"); const querystring_1 = require("querystring"); const consumers_1 = __importDefault(require("stream/consumers")); const p_map_1 = __importDefault(require("../vendors/p-map")); const ContainerNameCheck = new RegExp('^[a-z0-9](?!.*--)[a-z0-9-]{1,61}[a-z0-9]$'); /** * @private * Unique ket used to access the static <code>checkedCollections</code> * property of the AzureBlobTranscriptStore. Accessing it is necessary for * proper testing and debugging. */ exports.checkedCollectionsKey = Symbol('checkedCollectionsKey'); /** * Stores transcripts in an Azure Blob container. * * @remarks * Each activity is stored as JSON blob with a structure of * `container/{channelId]/{conversationId}/{Timestamp.ticks}-{activity.id}.json`. * * @deprecated This class is deprecated in favor of [BlobsTranscriptStore](xref:botbuilder-azure-blobs.BlobsTranscriptStore) */ class AzureBlobTranscriptStore { /** * Creates a new AzureBlobTranscriptStore instance. * * @param settings Settings required for configuring an instance of BlobStorage */ constructor(settings) { this.pageSize = 20; this.concurrency = Infinity; if (!settings) { throw new Error('The settings parameter is required.'); } if (!settings.containerName) { throw new Error('The containerName is required.'); } if (!this.checkContainerName(settings.containerName)) { throw new Error('Invalid container name.'); } if (!settings.storageAccountOrConnectionString) { throw new Error('The storageAccountOrConnectionString is required.'); } this.settings = Object.assign({}, settings); const pipeline = (0, storage_blob_1.newPipeline)(new storage_blob_1.AnonymousCredential(), { retryOptions: { retryPolicyType: storage_blob_1.StorageRetryPolicyType.FIXED, maxTries: 5, retryDelayInMs: 500, }, // Retry options }); this.containerClient = new storage_blob_1.ContainerClient(this.settings.storageAccountOrConnectionString, this.settings.containerName, pipeline.options); } initialize() { if (!this.initializePromise) { this.initializePromise = this.containerClient.createIfNotExists(); } return this.initializePromise; } /** * Log an activity to the transcript. * * @param activity Activity being logged. */ logActivity(activity) { return __awaiter(this, void 0, void 0, function* () { if (!activity) { throw new Error('Missing activity.'); } yield this.initialize(); const blobName = this.getBlobName(activity); const data = JSON.stringify(activity); const metadata = { FromId: activity.from.id, RecipientId: activity.recipient.id, }; if (activity.timestamp) { metadata.timestamp = activity.timestamp.toJSON(); } const blockBlobClient = this.containerClient.getBlockBlobClient(blobName); const blobOptions = { blobHTTPHeaders: { blobContentType: 'application/json' }, metadata: metadata }; yield blockBlobClient.upload(data, data.length, blobOptions); }); } /** * 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. * @returns The PagedResult of activities. */ getTranscriptActivities(channelId, conversationId, continuationToken, startDate) { var _a, _b, _c, _d; return __awaiter(this, void 0, void 0, function* () { if (!channelId) { throw new Error('Missing channelId'); } if (!conversationId) { throw new Error('Missing conversationId'); } if (!startDate) { startDate = new Date(0); } yield this.initialize(); const prefix = this.getDirName(channelId, conversationId) + '/'; const listBlobResult = this.containerClient .listBlobsByHierarchy('/', { prefix: prefix, }) .byPage({ continuationToken, maxPageSize: this.pageSize }); let page = yield listBlobResult.next(); const result = []; let response; while (!page.done) { // Note: azure library does not properly type iterator result, hence the need to cast response = (0, botbuilder_stdlib_1.maybeCast)((_a = page === null || page === void 0 ? void 0 : page.value) !== null && _a !== void 0 ? _a : {}); const blobItems = (_c = (_b = response === null || response === void 0 ? void 0 : response.segment) === null || _b === void 0 ? void 0 : _b.blobItems) !== null && _c !== void 0 ? _c : []; // Locate first index of results to slice from. If we have a start date, we want to return // activities after that start date. Otherwise we can simply return all activities in this page. const fromIdx = startDate != null ? blobItems.findIndex((blobItem) => { var _a, _b; return ((_a = blobItem === null || blobItem === void 0 ? void 0 : blobItem.properties) === null || _a === void 0 ? void 0 : _a.createdOn) && ((_b = blobItem === null || blobItem === void 0 ? void 0 : blobItem.properties) === null || _b === void 0 ? void 0 : _b.createdOn) >= startDate; }) : 0; if (fromIdx !== -1) { const activities = yield (0, p_map_1.default)(blobItems.slice(fromIdx), (blobItem) => __awaiter(this, void 0, void 0, function* () { const blobClient = this.containerClient.getBlobClient(blobItem.name); const blob = yield blobClient.download(); const { readableStreamBody } = blob; if (!readableStreamBody) { return null; } const activity = (yield consumers_1.default.json(readableStreamBody)); return Object.assign(Object.assign({}, activity), { timestamp: new Date(activity.timestamp) }); }), { concurrency: this.concurrency }); activities.forEach((activity) => { if (activity) result.push(activity); }); } page = yield listBlobResult.next(); } return { continuationToken: (_d = response === null || response === void 0 ? void 0 : response.continuationToken) !== null && _d !== void 0 ? _d : '', items: result.reduce((acc, activity) => (activity ? acc.concat(activity) : acc), []), }; }); } /** * List conversations in the channelId. * * @param channelId Channel Id. * @param continuationToken ContinuationToken token to page through results. * @returns A promise representation of [PagedResult<TranscriptInfo>](xref:botbuilder-core.PagedResult) */ listTranscripts(channelId, continuationToken) { var _a, _b, _c, _d; return __awaiter(this, void 0, void 0, function* () { if (!channelId) { throw new Error('Missing channelId'); } yield this.initialize(); // tslint:disable-next-line:prefer-template const prefix = this.getDirName(channelId) + '/'; const iter = this.containerClient .listBlobsByHierarchy('/', { prefix: prefix, }) .byPage({ continuationToken, maxPageSize: this.pageSize }); let page = yield iter.next(); const result = []; let response; while (!page.done) { // Note: azure library does not properly type iterator result, hence the need to cast const response = (0, botbuilder_stdlib_1.maybeCast)((_a = page === null || page === void 0 ? void 0 : page.value) !== null && _a !== void 0 ? _a : {}); const blobItems = (_c = (_b = response === null || response === void 0 ? void 0 : response.segment) === null || _b === void 0 ? void 0 : _b.blobItems) !== null && _c !== void 0 ? _c : []; const items = blobItems.map((blobItem) => { var _a, _b; const [, id] = decodeURIComponent(blobItem.name).split('/'); const created = ((_a = blobItem.properties) === null || _a === void 0 ? void 0 : _a.createdOn) ? new Date((_b = blobItem.properties) === null || _b === void 0 ? void 0 : _b.createdOn) : new Date(); return { channelId, created, id }; }); items.forEach((transcript) => { if (transcript) result.push(transcript); }); page = yield iter.next(); } return { continuationToken: (_d = response === null || response === void 0 ? void 0 : response.continuationToken) !== null && _d !== void 0 ? _d : '', items: result !== null && result !== void 0 ? result : [], }; }); } /** * 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. */ deleteTranscript(channelId, conversationId) { var _a, _b, _c; return __awaiter(this, void 0, void 0, function* () { if (!channelId) { throw new Error('Missing channelId'); } if (!conversationId) { throw new Error('Missing conversationId'); } yield this.initialize(); // tslint:disable-next-line:prefer-template const prefix = this.getDirName(channelId, conversationId) + '/'; const iter = this.containerClient.listBlobsByHierarchy('/', { prefix: prefix, includeMetadata: true, }); const segment = iter.byPage({ maxPageSize: this.pageSize, }); let page = yield segment.next(); while (!page.done) { // Note: azure library does not properly type iterator result, hence the need to cast const response = (0, botbuilder_stdlib_1.maybeCast)((_a = page === null || page === void 0 ? void 0 : page.value) !== null && _a !== void 0 ? _a : {}); const blobItems = (_c = (_b = response === null || response === void 0 ? void 0 : response.segment) === null || _b === void 0 ? void 0 : _b.blobItems) !== null && _c !== void 0 ? _c : []; yield (0, p_map_1.default)(blobItems, (blobItem) => this.containerClient.deleteBlob(blobItem.name), { concurrency: this.concurrency, }); page = yield segment.next(); } }); } /** * Check if a container name is valid. * * @param container String representing the container name to validate. * @returns A boolean value that indicates whether or not the name is valid. */ checkContainerName(container) { return ContainerNameCheck.test(container); } /** * Get the blob name based on the [Activity](xref:botframework-schema.Activity). * * @param activity [Activity](xref:botframework-schema.Activity) to get the blob name from. * @returns The blob name. */ getBlobName(activity) { const channelId = this.sanitizeKey(activity.channelId); const conversationId = this.sanitizeKey(activity.conversation.id); const timestamp = this.sanitizeKey(this.getTicks(activity.timestamp)); const activityId = this.sanitizeKey(activity.id); return `${channelId}/${conversationId}/${timestamp}-${activityId}.json`; } /** * Get the directory name. * * @param channelId Channel Id. * @param conversationId Id of the conversation to get the directory name from. * @returns The sanitized directory name. */ getDirName(channelId, conversationId) { if (!conversationId) { return this.sanitizeKey(channelId); } return `${this.sanitizeKey(channelId)}/${this.sanitizeKey(conversationId)}`; } /** * Escape a given key to be compatible for use with BlobStorage. * * @param key Key to be sanitized(scaped). * @returns The sanitized key. */ sanitizeKey(key) { return (0, querystring_1.escape)(key); } /** * @private */ getTicks(timestamp) { const epochTicks = 621355968000000000; // the number of .net ticks at the unix epoch const ticksPerMillisecond = 10000; // there are 10000 .net ticks per millisecond const ticks = epochTicks + timestamp.getTime() * ticksPerMillisecond; return ticks.toString(16); } } exports.AzureBlobTranscriptStore = AzureBlobTranscriptStore; //# sourceMappingURL=azureBlobTranscriptStore.js.map