diff --git a/libraries/botbuilder-azure-blobs/etc/botbuilder-azure-blobs.api.md b/libraries/botbuilder-azure-blobs/etc/botbuilder-azure-blobs.api.md index 2495331738..4da9c63f9a 100644 --- a/libraries/botbuilder-azure-blobs/etc/botbuilder-azure-blobs.api.md +++ b/libraries/botbuilder-azure-blobs/etc/botbuilder-azure-blobs.api.md @@ -31,11 +31,12 @@ export class BlobsTranscriptStore implements TranscriptStore { deleteTranscript(channelId: string, conversationId: string): Promise; getTranscriptActivities(channelId: string, conversationId: string, continuationToken?: string, startDate?: Date): Promise>; listTranscripts(channelId: string, continuationToken?: string): Promise>; - logActivity(activity: Activity): Promise; + logActivity(activity: Activity, options?: BlobsTranscriptStoreOptions): Promise; } // @public export interface BlobsTranscriptStoreOptions { + decodeTranscriptKey?: boolean; storagePipelineOptions?: StoragePipelineOptions; } diff --git a/libraries/botbuilder-azure-blobs/src/blobsTranscriptStore.ts b/libraries/botbuilder-azure-blobs/src/blobsTranscriptStore.ts index d9fd31065f..f44ff53660 100644 --- a/libraries/botbuilder-azure-blobs/src/blobsTranscriptStore.ts +++ b/libraries/botbuilder-azure-blobs/src/blobsTranscriptStore.ts @@ -33,14 +33,15 @@ function getConversationPrefix(channelId: string, conversationId: string): strin } // Formats an activity as a blob key -function getBlobKey(activity: Activity): string { +function getBlobKey(activity: Activity, options?: BlobsTranscriptStoreOptions): string { const { timestamp } = z .object({ timestamp: z.instanceof(Date) }) .nonstrict() .parse(activity); return sanitizeBlobKey( - [activity.channelId, activity.conversation.id, `${formatTicks(timestamp)}-${activity.id}.json`].join('/') + [activity.channelId, activity.conversation.id, `${formatTicks(timestamp)}-${activity.id}.json`].join('/'), + options ); } @@ -56,6 +57,12 @@ export interface BlobsTranscriptStoreOptions { * storage client */ storagePipelineOptions?: StoragePipelineOptions; + + /** + * Optional setting to return a new string representing the decoded version of the given encoded blob transcript key. + * This remains the default behavior to false, but can be overridden by setting decodeTranscriptKey to true. + */ + decodeTranscriptKey?: boolean; } /** @@ -70,6 +77,7 @@ export class BlobsTranscriptStore implements TranscriptStore { private readonly _containerClient: ContainerClient; private readonly _concurrency = Infinity; private _initializePromise?: Promise; + private _isDecodeTranscriptKey?: boolean = false; /** * Constructs a BlobsTranscriptStore instance. @@ -86,6 +94,8 @@ export class BlobsTranscriptStore implements TranscriptStore { this._containerClient = new ContainerClient(connectionString, containerName, options?.storagePipelineOptions); + this._isDecodeTranscriptKey = options?.decodeTranscriptKey; + // At most one promise at a time to be friendly to local emulator users if (connectionString.trim() === 'UseDevelopmentStorage=true;') { this._concurrency = 1; @@ -253,14 +263,15 @@ export class BlobsTranscriptStore implements TranscriptStore { * Log an activity to the transcript. * * @param {Activity} activity activity to log + * @param {BlobsTranscriptStoreOptions} options Optional settings for BlobsTranscriptStore * @returns {Promise} A promise representing the async operation. */ - async logActivity(activity: Activity): Promise { + async logActivity(activity: Activity, options?: BlobsTranscriptStoreOptions): Promise { z.object({ activity: z.record(z.unknown()) }).parse({ activity }); await this._initialize(); - const blob = this._containerClient.getBlockBlobClient(getBlobKey(activity)); + const blob = this._containerClient.getBlockBlobClient(getBlobKey(activity, options)); const serialized = JSON.stringify(activity); const metadata: Record = { diff --git a/libraries/botbuilder-azure-blobs/src/sanitizeBlobKey.ts b/libraries/botbuilder-azure-blobs/src/sanitizeBlobKey.ts index 4b3295f138..714982e1dd 100644 --- a/libraries/botbuilder-azure-blobs/src/sanitizeBlobKey.ts +++ b/libraries/botbuilder-azure-blobs/src/sanitizeBlobKey.ts @@ -1,18 +1,26 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { BlobsTranscriptStoreOptions } from './blobsTranscriptStore'; + /** * Ensures that `key` is a properly sanitized Azure Blob Storage key. It should be URI encoded, * no longer than 1024 characters, and contain no more than 254 slash ("/") chars. * * @param {string} key string blob key to sanitize + * @param {BlobsTranscriptStoreOptions} options Optional settings for BlobsTranscriptStore * @returns {string} sanitized blob key */ -export function sanitizeBlobKey(key: string): string { +export function sanitizeBlobKey(key: string, options?: BlobsTranscriptStoreOptions): string { if (!key || !key.length) { throw new Error('Please provide a non-empty key'); } const sanitized = key.split('/').reduce((acc, part, idx) => (part ? [acc, part].join(idx < 255 ? '/' : '') : acc)); + + // Options settings to decode key in order to support previous Blob + if (options?.decodeTranscriptKey) { + return decodeURIComponent(sanitized).substr(0, 1024); + } return encodeURIComponent(sanitized).substr(0, 1024); } diff --git a/libraries/botbuilder-azure-blobs/tests/blobsTranscriptStore.test.js b/libraries/botbuilder-azure-blobs/tests/blobsTranscriptStore.test.js index 5a0d1ad290..132439871c 100644 --- a/libraries/botbuilder-azure-blobs/tests/blobsTranscriptStore.test.js +++ b/libraries/botbuilder-azure-blobs/tests/blobsTranscriptStore.test.js @@ -24,6 +24,11 @@ describe('BlobsStorage', function () { timestamp: new Date(), }; + // Options for BlobsTranscriptStore. + const blobTranscriptOptions = { + decodeTranscriptKey: false, + }; + // Constructs a random channel ID and set of activities, and then handles preloading // and clearing them with beforeEach/afterEach IFF client is defined const maybePreload = () => { @@ -77,7 +82,7 @@ describe('BlobsStorage', function () { }); maybeIt('should log an activity', async () => { - await client.logActivity(activity); + await client.logActivity(activity, blobTranscriptOptions); }); }); diff --git a/libraries/botbuilder-dialogs-adaptive-runtime/src/index.ts b/libraries/botbuilder-dialogs-adaptive-runtime/src/index.ts index 7fecfb1bb4..baee6af898 100644 --- a/libraries/botbuilder-dialogs-adaptive-runtime/src/index.ts +++ b/libraries/botbuilder-dialogs-adaptive-runtime/src/index.ts @@ -96,6 +96,7 @@ function addFeatures(services: ServiceCollection, configuration: Configuration): .object({ connectionString: z.string(), containerName: z.string(), + decodeTranscriptKey: z.boolean().optional(), }) .nonstrict() ); diff --git a/libraries/botbuilder-repo-utils/package.json b/libraries/botbuilder-repo-utils/package.json index eea1d3f318..aa91a8f70d 100644 --- a/libraries/botbuilder-repo-utils/package.json +++ b/libraries/botbuilder-repo-utils/package.json @@ -16,7 +16,7 @@ "globby": "^11.0.1", "lodash": "^4.17.20", "minimatch": "^3.0.4", - "minimist": "^1.2.5", + "minimist": "^1.2.6", "p-map": "^4.0.0" }, "devDependencies": { diff --git a/libraries/botbuilder-repo-utils/src/package.ts b/libraries/botbuilder-repo-utils/src/package.ts index 16627f9d29..089f632cd1 100644 --- a/libraries/botbuilder-repo-utils/src/package.ts +++ b/libraries/botbuilder-repo-utils/src/package.ts @@ -15,6 +15,7 @@ export interface Package { dependencies?: Record; devDependencies?: Record; + peerDependencies?: Record; scripts?: Record; } diff --git a/libraries/botbuilder-repo-utils/src/updateVersions.ts b/libraries/botbuilder-repo-utils/src/updateVersions.ts index 92f2553397..fa3e877a68 100644 --- a/libraries/botbuilder-repo-utils/src/updateVersions.ts +++ b/libraries/botbuilder-repo-utils/src/updateVersions.ts @@ -94,14 +94,14 @@ export const command = (argv: string[], quiet = false) => async (): Promise>( (acc, { pkg }) => ({ ...acc, - [pkg.name]: getPackageVersion(pkg, newVersion, { + [pkg.name]: getPackageVersion(pkg, pkg.private ? pkg.version : newVersion, { buildLabel: flags.buildLabel, commitSha, date, @@ -122,7 +122,7 @@ export const command = (argv: string[], quiet = false) => async (): Promise( workspaces.map(async ({ absPath, pkg }) => { const newVersion = workspaceVersions[pkg.name]; @@ -142,6 +142,10 @@ export const command = (argv: string[], quiet = false) => async (): Promise