botbuilder-dialogs
Version:
A dialog stack based conversation manager for Microsoft BotBuilder.
255 lines (222 loc) • 8.53 kB
text/typescript
/**
* @module botbuilder-dialogs
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { DialogContext } from '../../dialogContext';
import { DialogTurnStateConstants } from '../../dialogTurnStateConstants';
import { MemoryScope } from './memoryScope';
import { ScopePath } from '../scopePath';
/**
* The setting node.
*/
class Node {
/**
* Initializes a new instance of `Node`.
*
* @param {string} value Value of the node. If the node is not leaf, value represents the current path.
*/
constructor(public value?: string) {}
/**
* The child nodes of the node.
*/
children: Node[] = [];
/**
* Indicates if the node is leaf node.
*
* @returns {boolean} If the node is leaf node or not.
*/
isLeaf(): boolean {
return this.children.length === 0;
}
}
/**
* SettingsMemoryScope maps "settings" -> dc.context.turnState['settings']
*/
export class SettingsMemoryScope extends MemoryScope {
private static readonly blockingList = [
'MicrosoftAppPassword',
'cosmosDb:authKey',
'blobStorage:connectionString',
'BlobsStorage:connectionString',
'CosmosDbPartitionedStorage:authKey',
'applicationInsights:connectionString',
'applicationInsights:InstrumentationKey',
'runtimeSettings:telemetry:options:connectionString',
'runtimeSettings:telemetry:options:instrumentationKey',
'runtimeSettings:features:blobTranscript:connectionString',
];
/**
* Initializes a new instance of the [SettingsMemoryScope](xref:botbuilder-dialogs.SettingsMemoryScope) class.
*
* @param initialSettings initial set of settings to supply
*/
constructor(private readonly initialSettings?: Record<string, unknown>) {
super(ScopePath.settings, false);
}
/**
* Gets the backing memory for this scope.
*
* @param {DialogContext} dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) object for this turn.
* @returns {Record<string, ?>} The memory for the scope.
*/
getMemory(dc: DialogContext): Record<string, unknown> {
if (dc.context.turnState.has(ScopePath.settings)) {
return dc.context.turnState.get(ScopePath.settings) ?? {};
}
const configuration = dc.context.turnState.get(DialogTurnStateConstants.configuration) ?? {};
Object.entries(process.env).reduce((result, [key, value]) => {
result[`${key}`] = value;
return result;
}, configuration);
const settings = SettingsMemoryScope.loadSettings(configuration);
dc.context.turnState.set(ScopePath.settings, settings);
return settings;
}
/**
* @param dc Current dialog context.
*/
async load(dc: DialogContext): Promise<void> {
if (this.initialSettings) {
// filter initialSettings
const filteredSettings = SettingsMemoryScope.filterSettings(this.initialSettings);
dc.context.turnState.set(ScopePath.settings, filteredSettings);
}
await super.load(dc);
}
/**
* Build a dictionary view of configuration providers.
*
* @param {Record<string, string>} configuration The configuration that we are running with.
* @returns {Record<string, ?>} Projected dictionary for settings.
*/
protected static loadSettings(configuration: Record<string, string>): Record<string, unknown> {
let settings = {};
if (configuration) {
// load configuration into settings
const root = this.convertFlattenSettingToNode(Object.entries(configuration));
settings = root.children.reduce(
(acc, child) => ({ ...acc, [child.value]: this.convertNodeToObject(child) }),
settings
);
}
// filter env configuration settings
return this.filterSettings(settings);
}
/**
* Generate a node tree with the flatten settings.
* For example:
* {
* "array":["item1", "item2"],
* "object":{"array":["item1"], "2":"numberkey"}
* }
*
* Would generate a flatten settings like:
* array:0 item1
* array:1 item2
* object:array:0 item1
* object:2 numberkey
*
* After Converting it from flatten settings into node tree, would get:
*
* null
* | |
* array object
* | | | |
* 0 1 array 2
* | | | |
* item1 item2 0 numberkey
* |
* item1
* The result is a Tree.
*
* @param {Array<[string, string]>} kvs Configurations with key value pairs.
* @returns {Node} The root node of the tree.
*/
private static convertFlattenSettingToNode(kvs: Array<[key: string, value: string]>): Node {
const root = new Node();
kvs.forEach(([key, value]) => {
const keyChain = key.split(':');
let currentNode = root;
keyChain.forEach((item) => {
const matchItem = currentNode.children.find((u) => u?.value === item);
if (!matchItem) {
// Remove all the leaf children
currentNode.children = currentNode.children.filter((u) => u.children.length !== 0);
// Append new child into current node
const node = new Node(item);
currentNode.children.push(node);
currentNode = node;
} else {
currentNode = matchItem;
}
});
currentNode.children.push(new Node(value));
});
return root;
}
private static convertNodeToObject(node: Node): unknown {
if (!node.children.length) {
return {};
}
// If the child is leaf node, return its value directly.
if (node.children.length === 1 && node.children[0].isLeaf()) {
return node.children[0].value;
}
// check if all the children are number format.
let pureNumberIndex = true;
const indexArray: number[] = [];
let indexMax = -1;
for (let i = 0; i < node.children.length; i++) {
const child = node.children[Number(i)];
if (/^-?\d+$/.test(child.value)) {
const num = parseInt(child.value, 10);
if (!isNaN(num) && num >= 0) {
indexArray.push(num);
if (num > indexMax) {
indexMax = num;
}
continue;
}
}
pureNumberIndex = false;
break;
}
if (pureNumberIndex) {
// all children are int numbers, treat it as array.
const listResult = new Array(indexMax + 1);
node.children.forEach((child, index) => {
listResult[indexArray[Number(index)]] = this.convertNodeToObject(child);
});
return listResult;
}
// Convert all child into dictionary
return node.children.reduce((result, child) => {
result[child.value] = this.convertNodeToObject(child);
return result;
}, {});
}
private static filterSettings(settings: Record<string, unknown>): Record<string, unknown> {
const result = Object.assign({}, settings);
this.blockingList.forEach((path) => this.deletePropertyPath(result, path));
return result;
}
private static deletePropertyPath(obj, path: string): void {
if (!obj || !path?.length) {
return;
}
const pathArray = path.split(':');
for (let i = 0; i < pathArray.length - 1; i++) {
const realKey = Object.keys(obj).find((key) => key.toLowerCase() === pathArray[i].toLowerCase());
obj = obj[realKey];
if (typeof obj === 'undefined') {
return;
}
}
const lastPath = pathArray.pop().toLowerCase();
const lastKey = Object.keys(obj).find((key) => key.toLowerCase() === lastPath);
delete obj[lastKey];
}
}