type2docfx
Version:
A tool to convert json format output from TypeDoc to universal reference model for DocFx to consume.
335 lines (290 loc) • 15.8 kB
text/typescript
/**
* @module botbuilder-azure
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { TranscriptStore, Activity, PagedResult, Transcript } from 'botbuilder';
import { BlobStorageSettings } from "./blobStorage";
import * as azure from 'azure-storage';
import { escape } from 'querystring';
const ContainerNameCheck = new RegExp('^[a-z0-9](?!.*--)[a-z0-9-]{1,61}[a-z0-9]$');
/**
* Internal dictionary with the containers where entities will be stored.
*/
let checkedCollections: { [key: string]: Promise<azure.BlobService.ContainerResult>; } = {};
/**
* The blob transcript store stores transcripts in an Azure Blob container where each activity is stored as json blob in structure of
* container/{channelId]/{conversationId}/{Timestamp.ticks}-{activity.id}.json
*/
export class AzureBlobTranscriptStore implements TranscriptStore {
private settings: BlobStorageSettings
private client: BlobServiceAsync
private pageSize = 20;
/**
* Creates an instance of AzureBlobTranscriptStore
* @param settings Settings for configuring an instance of BlobStorage
*/
public constructor(settings: BlobStorageSettings) {
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.storageAccountOrConnectionString, this.settings.storageAccessKey, this.settings.host);
}
/**
* Log an activity to the transcript.
* @param activity Activity being logged.
*/
logActivity(activity: Activity): void | Promise<void> {
if (!activity) {
throw new Error('Missing activity.');
}
let blobName = this.getBlobName(activity);
let data = JSON.stringify(activity);
return this.ensureContainerExists().then((container) => {
let writeProperties = () => this.client.setBlobPropertiesAsync(container.name, blobName, {
contentType: 'application/json'
});
let writeMetadata = () => this.client.setBlobMetadataAsync(container.name, blobName, {
'fromid': activity.from.id,
'recipientid': activity.recipient.id,
'timestamp': activity.timestamp.getTime().toString()
});
let writeData = () => this.client.createBlockBlobFromTextAsync(container.name, blobName, data, null);
return writeData().then(writeProperties).then(writeMetadata).then(() => {});
});
}
/**
* Get activities for a conversation (Aka the transcript)
* @param channelId Channel Id.
* @param conversationId Conversation Id.
* @param continuationToken Continuatuation token to page through results.
* @param startDate Earliest time to include.
*/
getTranscriptActivities(channelId: string, conversationId: string, continuationToken?: string, startDate?: Date): Promise<PagedResult<Activity>> {
if (!channelId) {
throw new Error("Missing channelId");
}
if (!conversationId) {
throw new Error("Missing conversationId");
}
if (!startDate) {
startDate = new Date(0);
}
let prefix = this.getDirName(channelId, conversationId) + '/';
let token = null;
return this.ensureContainerExists()
.then(container => this.getActivityBlobs([], container.name, prefix, continuationToken, startDate, token))
.then((blobs) => {
return Promise.all(blobs.map(blob => this.blobToActivity(blob)))
.then((activities) => {
let pagedResult = new PagedResult<Activity>();
pagedResult.items = activities;
if (pagedResult.items.length == this.pageSize) {
pagedResult.continuationToken = blobs.slice(-1).pop().name;
}
return pagedResult;
});
});
}
private blobToActivity(blob: azure.BlobService.BlobResult): Promise<Activity> {
return this.client.getBlobToTextAsync(blob.container, blob.name)
.then((content) => {
let activity = JSON.parse(content as any) as Activity;
activity.timestamp = new Date(activity.timestamp);
return activity;
});
}
private getActivityBlobs(blobs: azure.BlobService.BlobResult[], container: string, prefix: string, continuationToken: string, startDate: Date, token: azure.common.ContinuationToken): Promise<azure.BlobService.BlobResult[]> {
return new Promise((resolve, reject) => {
this.client.listBlobsSegmentedWithPrefixAsync(container, prefix, token, { include: 'metadata' })
.then((result) => {
result.entries.some((blob) => {
let timestamp = Number.parseInt(blob.metadata['timestamp'], 10);
if (timestamp >= startDate.getTime()) {
if (continuationToken){
if (blob.name === continuationToken) {
continuationToken = null;
}
} else {
blob.container = container;
blobs.push(blob);
return (blobs.length === this.pageSize);
}
}
return false;
});
if (result.continuationToken && blobs.length < this.pageSize) {
resolve(this.getActivityBlobs(blobs, container, prefix, continuationToken, startDate, result.continuationToken));
}
resolve(blobs);
})
.catch(error => reject(error));
});
}
/**
* List conversations in the channelId.
* @param channelId Channel Id.
* @param continuationToken Continuatuation token to page through results.
*/
listTranscripts(channelId: string, continuationToken?: string): Promise<PagedResult<Transcript>> {
if (!channelId) {
throw new Error("Missing channelId");
}
let prefix = this.getDirName(channelId) + '/';
let token = null;
return this.ensureContainerExists()
.then(container => this.getTranscriptsFolders([], container.name, prefix, continuationToken, channelId, token))
.then(transcripts => {
let pagedResult = new PagedResult<Transcript>();
pagedResult.items = transcripts;
if (pagedResult.items.length == this.pageSize) {
pagedResult.continuationToken = transcripts.slice(-1).pop().id;
}
return pagedResult;
});
}
private getTranscriptsFolders(transcripts: Transcript[], container: string, prefix: string, continuationToken: string, channelId: string, token: azure.common.ContinuationToken): Promise<Transcript[]> {
return new Promise((resolve, reject) => {
this.client.listBlobDirectoriesSegmentedWithPrefixAsync(container, prefix, token).then((result) => {
result.entries.some((blob) => {
let conversation = new Transcript();
conversation.id = blob.name.split('/').filter(part => part).slice(-1).pop();
conversation.channelId = channelId;
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) {
resolve(this.getTranscriptsFolders(transcripts, container, prefix, continuationToken, channelId, result.continuationToken))
}
resolve(transcripts);
})
.catch(error => reject(error));
});
}
/**
* 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: string, conversationId: string): Promise<void> {
if (!channelId) {
throw new Error("Missing channelId");
}
if (!conversationId) {
throw new Error("Missing conversationId");
}
let prefix = this.getDirName(channelId, conversationId) + '/';
let token = null;
return this.ensureContainerExists()
.then(container => this.getConversationsBlobs([], container.name, prefix, token))
.then(blobs => Promise.all(blobs.map(blob => this.client.deleteBlobIfExistsAsync(blob.container, blob.name))))
.then(results => { });
}
private getConversationsBlobs(blobs: azure.BlobService.BlobResult[], container: string, prefix: string, token: azure.common.ContinuationToken): Promise<azure.BlobService.BlobResult[]> {
return new Promise((resolve, reject) => {
this.client.listBlobsSegmentedWithPrefixAsync(container, prefix, token, null)
.then((result) => {
blobs = blobs.concat(result.entries.map(blob => {
blob.container = container;
return blob;
}))
if (result.continuationToken) {
resolve(this.getConversationsBlobs(blobs, container, prefix, result.continuationToken))
}
resolve(blobs);
})
.catch(error => reject(error));
});
}
private checkContainerName(container: string): boolean {
return ContainerNameCheck.test(container);
}
private getBlobName(activity: Activity): string {
let channelId = this.sanitizeKey(activity.channelId);
let conversationId = this.sanitizeKey(activity.conversation.id);
let timestamp = this.sanitizeKey(this.getTicks(activity.timestamp));
let activityId = this.sanitizeKey(activity.id);
return `${channelId}/${conversationId}/${timestamp}-${activityId}.json`;
}
private getDirName(channelId: string, conversationId?: string): string {
if (!conversationId) {
return this.sanitizeKey(channelId);
}
return `${this.sanitizeKey(channelId)}/${this.sanitizeKey(conversationId)}`;
}
private sanitizeKey(key: string): string {
return escape(key);
}
private getTicks(timestamp: Date): string {
var epochTicks = 621355968000000000; // the number of .net ticks at the unix epoch
var ticksPerMillisecond = 10000; // there are 10000 .net ticks per millisecond
let ticks = epochTicks + (timestamp.getTime() * ticksPerMillisecond);
return ticks.toString(16);
}
private ensureContainerExists(): Promise<azure.BlobService.ContainerResult> {
let key = this.settings.containerName;
if (!checkedCollections[key]) {
checkedCollections[key] = this.client.createContainerIfNotExistsAsync(key);
}
return checkedCollections[key];
}
private createBlobService(storageAccountOrConnectionString: string, storageAccessKey: string, host: any): BlobServiceAsync {
if (!storageAccountOrConnectionString) {
throw new Error('The storageAccountOrConnectionString parameter is required.');
}
const blobService = azure.createBlobService(storageAccountOrConnectionString, storageAccessKey, host).withFilter(new azure.LinearRetryPolicyFilter(5, 500));
// create BlobServiceAsync by using denodeify to create promise wrappers around cb functions
return Object.assign(blobService as any, {
createContainerIfNotExistsAsync: this.denodeify(blobService, blobService.createContainerIfNotExists),
deleteContainerIfExistsAsync: this.denodeify(blobService, blobService.deleteContainerIfExists),
createBlockBlobFromTextAsync: this.denodeify(blobService, blobService.createBlockBlobFromText),
getBlobMetadataAsync: this.denodeify(blobService, blobService.getBlobMetadata),
setBlobMetadataAsync: this.denodeify(blobService, blobService.setBlobMetadata),
getBlobPropertiesAsync: this.denodeify(blobService, blobService.getBlobProperties),
setBlobPropertiesAsync: this.denodeify(blobService, blobService.setBlobProperties),
getBlobToTextAsync: this.denodeify(blobService, blobService.getBlobToText),
deleteBlobIfExistsAsync: this.denodeify(blobService, blobService.deleteBlobIfExists),
listBlobsSegmentedWithPrefixAsync: this.denodeify(blobService, blobService.listBlobsSegmentedWithPrefix),
listBlobDirectoriesSegmentedWithPrefixAsync: this.denodeify(blobService, blobService['listBlobDirectoriesSegmentedWithPrefix'])
});
}
// turn a cb based azure method into a Promisified one
private denodeify<T>(thisArg: any, fn: Function): (...args: any[]) => Promise<T> {
return (...args: any[]) => {
return new Promise<T>((resolve, reject) => {
args.push((error: Error, result: any) => (error) ? reject(error) : resolve(result));
fn.apply(thisArg, args);
});
};
}
}
// Promise based methods created using denodeify function
interface BlobServiceAsync extends azure.BlobService {
createContainerIfNotExistsAsync(container: string): Promise<azure.BlobService.ContainerResult>;
deleteContainerIfExistsAsync(container: string): Promise<boolean>;
createBlockBlobFromTextAsync(container: string, blob: string, text: string | Buffer, options: azure.BlobService.CreateBlobRequestOptions): Promise<azure.BlobService.BlobResult>;
getBlobMetadataAsync(container: string, blob: string): Promise<azure.BlobService.BlobResult>;
setBlobMetadataAsync(container: string, blob: string, metadata: { [index: string] : string }): Promise<azure.BlobService.BlobResult>;
getBlobPropertiesAsync(container: string, blob: string): Promise<azure.BlobService.BlobResult>;
setBlobPropertiesAsync(container: string, blob: string, propertiesAndOptions: azure.BlobService.SetBlobPropertiesRequestOptions): Promise<azure.BlobService.BlobResult>;
getBlobToTextAsync(container: string, blob: string): Promise<azure.BlobService.BlobToText>;
deleteBlobIfExistsAsync(container: string, blob: string): Promise<boolean>;
listBlobsSegmentedWithPrefixAsync(container: string, prefix: string, currentToken: azure.common.ContinuationToken, options: azure.BlobService.ListBlobsSegmentedRequestOptions,): Promise<azure.BlobService.ListBlobsResult>;
listBlobDirectoriesSegmentedWithPrefixAsync(container: string, prefix: string, currentToken: azure.common.ContinuationToken): Promise<azure.BlobService.ListBlobsResult>;
}