botbuilder-azure
Version:
Azure extensions for Microsoft BotBuilder.
363 lines • 15.4 kB
JavaScript
"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 _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AzureBlobTranscriptStore = exports.checkedCollectionsKey = void 0;
const azure = require("azure-storage");
const querystring_1 = require("querystring");
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;
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.');
}
this.settings = Object.assign({}, settings);
this.client = this.createBlobService(this.settings);
}
/**
* 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.');
}
const blobName = this.getBlobName(activity);
const data = JSON.stringify(activity);
const container = yield this.ensureContainerExists();
const block = yield this.client.createBlockBlobFromTextAsync(container.name, blobName, data, null);
const meta = this.client.setBlobMetadataAsync(container.name, blobName, {
fromid: activity.from.id,
recipientid: activity.recipient.id,
timestamp: activity.timestamp.toJSON(),
});
const props = this.client.setBlobPropertiesAsync(container.name, blobName, {
contentType: 'application/json',
});
yield Promise.all([block, meta, props]); // Concurrent
});
}
/**
* 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) {
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);
}
// tslint:disable-next-line:prefer-template
const prefix = this.getDirName(channelId, conversationId) + '/';
const token = null;
const container = yield this.ensureContainerExists();
const activityBlobs = yield this.getActivityBlobs([], container.name, prefix, continuationToken, startDate, token);
const activities = yield Promise.all(activityBlobs.map((blob) => this.blobToActivity(blob)));
const pagedResult = { items: activities, continuationToken: undefined };
if (pagedResult.items.length === this.pageSize) {
pagedResult.continuationToken = activityBlobs.slice(-1).pop().name;
}
return pagedResult;
});
}
/**
* 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) {
return __awaiter(this, void 0, void 0, function* () {
if (!channelId) {
throw new Error('Missing channelId');
}
// tslint:disable-next-line:prefer-template
const prefix = this.getDirName(channelId) + '/';
const token = null;
const container = yield this.ensureContainerExists();
const transcripts = yield this.getTranscriptsFolders([], container.name, prefix, continuationToken, channelId, token);
const pagedResult = { items: transcripts, continuationToken: undefined };
if (pagedResult.items.length === this.pageSize) {
pagedResult.continuationToken = transcripts.slice(-1).pop().id;
}
return pagedResult;
});
}
/**
* 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) {
return __awaiter(this, void 0, void 0, function* () {
if (!channelId) {
throw new Error('Missing channelId');
}
if (!conversationId) {
throw new Error('Missing conversationId');
}
// tslint:disable-next-line:prefer-template
const prefix = this.getDirName(channelId, conversationId) + '/';
const token = null;
const container = yield this.ensureContainerExists();
const conversationBlobs = yield this.getConversationsBlobs([], container.name, prefix, token);
yield Promise.all(conversationBlobs.map((blob) => this.client.deleteBlobIfExistsAsync(blob.container, blob.name)));
});
}
/**
* Parse a BlobResult as an [Activity](xref:botframework-schema.Activity).
*
* @param blob BlobResult to parse as an [Activity](xref:botframework-schema.Activity).
* @returns The parsed [Activity](xref:botframework-schema.Activity).
*/
blobToActivity(blob) {
return __awaiter(this, void 0, void 0, function* () {
const content = yield this.client.getBlobToTextAsync(blob.container, blob.name);
const activity = JSON.parse(content);
activity.timestamp = new Date(activity.timestamp);
return activity;
});
}
/**
* @private
*/
getActivityBlobs(blobs, container, prefix, continuationToken, startDate, token) {
return __awaiter(this, void 0, void 0, function* () {
const listBlobResult = yield this.client.listBlobsSegmentedWithPrefixAsync(container, prefix, token, {
include: 'metadata',
});
listBlobResult.entries.some((blob) => {
const timestamp = new Date(blob.metadata.timestamp);
if (timestamp >= startDate) {
if (continuationToken) {
if (blob.name === continuationToken) {
continuationToken = null;
}
}
else {
blob.container = container;
blobs.push(blob);
return blobs.length === this.pageSize;
}
}
return false;
});
if (listBlobResult.continuationToken && blobs.length < this.pageSize) {
yield this.getActivityBlobs(blobs, container, prefix, continuationToken, startDate, listBlobResult.continuationToken);
}
return blobs;
});
}
/**
* @private
*/
getTranscriptsFolders(transcripts, container, prefix, continuationToken, channelId, token) {
return __awaiter(this, void 0, void 0, function* () {
const result = yield this.client.listBlobDirectoriesSegmentedWithPrefixAsync(container, prefix, token);
result.entries.some((blob) => {
const conversation = {
channelId: channelId,
id: blob.name
.split('/')
.filter((part) => part)
.slice(-1)
.pop(),
created: undefined,
};
if (continuationToken) {
if (conversation.id === continuationToken) {
continuationToken = null;
}
}
else {
transcripts.push(conversation);
return transcripts.length === this.pageSize;
}
return false;
});
if (result.continuationToken && transcripts.length < this.pageSize) {
yield this.getTranscriptsFolders(transcripts, container, prefix, continuationToken, channelId, result.continuationToken);
}
return transcripts;
});
}
/**
* @private
*/
getConversationsBlobs(blobs, container, prefix, token) {
return __awaiter(this, void 0, void 0, function* () {
const result = yield this.client.listBlobsSegmentedWithPrefixAsync(container, prefix, token, null);
blobs = blobs.concat(result.entries.map((blob) => {
blob.container = container;
return blob;
}));
if (result.continuationToken) {
yield this.getConversationsBlobs(blobs, container, prefix, result.continuationToken);
}
return blobs;
});
}
/**
* 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 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);
}
/**
* Delay Container creation if it does not exist.
*
* @returns A promise representing the asynchronous operation.
*/
ensureContainerExists() {
const key = this.settings.containerName;
if (!AzureBlobTranscriptStore[exports.checkedCollectionsKey][key]) {
AzureBlobTranscriptStore[exports.checkedCollectionsKey][key] = this.client.createContainerIfNotExistsAsync(key);
}
return AzureBlobTranscriptStore[exports.checkedCollectionsKey][key];
}
/**
* Create a Blob Service.
*
* @param param0 Settings required for configuring the Blob Service.
* @param param0.storageAccountOrConnectionString Storage account or connection string.
* @param param0.storageAccessKey Storage access key.
* @param param0.host Blob Service host.
* @returns The BlobService created.
*/
createBlobService({ storageAccountOrConnectionString, storageAccessKey, host, }) {
if (!storageAccountOrConnectionString) {
throw new Error('The storageAccountOrConnectionString parameter is required.');
}
const blobService = azure
.createBlobService(storageAccountOrConnectionString, storageAccessKey, host)
.withFilter(new azure.LinearRetryPolicyFilter(5, 500));
// The perfect use case for a Proxy
return new Proxy({}, {
get(target, p) {
const prop = p.toString().endsWith('Async') ? p.toString().replace('Async', '') : p;
return target[p] || (target[p] = denodeify(blobService, blobService[prop]));
},
});
// eslint-disable-next-line @typescript-eslint/ban-types
function denodeify(thisArg, fn) {
return (...args) => {
return new Promise((resolve, reject) => {
args.push((error, result) => (error ? reject(error) : resolve(result)));
fn.apply(thisArg, args);
});
};
}
}
}
exports.AzureBlobTranscriptStore = AzureBlobTranscriptStore;
_a = exports.checkedCollectionsKey;
/**
* @private
* Internal dictionary with the containers where entities will be stored.
*/
AzureBlobTranscriptStore[_a] = {};
//# sourceMappingURL=azureBlobTranscriptStore.js.map