botbuilder-azure
Version:
Azure extensions for Microsoft BotBuilder.
309 lines • 14.8 kB
JavaScript
/**
* @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
;