diff --git a/.gitignore b/.gitignore index be951fa297..997ecc98bf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,11 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +# Blanket ignore zip files +# Related to Teams Scenarios work +*.zip +*.vscode + # User-specific files *.suo *.user @@ -287,7 +292,12 @@ __pycache__/ *.odx.cs *.xsd.cs /**/node_modules -/**/.vscode + +# Exception for including the ESLint extension in vscode +!.vscode/ +.vscode/* +!.vscode/extensions.json + /**/*.VC.db #coverage /**/coverage @@ -295,4 +305,7 @@ __pycache__/ #derived /test-runner/out +package-lock.json +*.tsbuildinfo +*.js.map diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..7d0473afa7 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + ] +} \ No newline at end of file diff --git a/lerna.json b/lerna.json index 9d285a77ed..d175add360 100644 --- a/lerna.json +++ b/lerna.json @@ -10,12 +10,13 @@ "libraries/botbuilder-dialogs", "libraries/botbuilder-dialogs-adaptive", "libraries/botbuilder-dialogs-declarative", - "libraries/botframework-expressions", "libraries/botbuilder-lg", "libraries/botbuilder-testing", "libraries/botframework-config", "libraries/botframework-connector", + "libraries/botframework-expressions", "libraries/botframework-schema", + "libraries/botframework-streaming", "libraries/functional-tests", "libraries/testbot", "transcripts" diff --git a/libraries/botbuilder-ai/src/luisRecognizer.ts b/libraries/botbuilder-ai/src/luisRecognizer.ts index 61b4e9e53f..4169a10c1b 100644 --- a/libraries/botbuilder-ai/src/luisRecognizer.ts +++ b/libraries/botbuilder-ai/src/luisRecognizer.ts @@ -142,7 +142,7 @@ export interface LuisRecognizerTelemetryClient { * * @remarks * This class is used to recognize intents and extract entities from incoming messages. - * See this class in action [in this sample application](https://github.com/Microsoft/BotBuilder-Samples/tree/master/samples/javascript_nodejs/12.nlp-with-luis). + * See this class in action [in this sample application](https://github.com/microsoft/BotBuilder-Samples/tree/master/samples/javascript_nodejs/14.nlp-with-dispatch). * * This component can be used within your bots logic by calling [recognize()](#recognize). */ diff --git a/libraries/botbuilder-ai/src/qnaMaker.ts b/libraries/botbuilder-ai/src/qnaMaker.ts index dfc2034948..e3ea997f47 100644 --- a/libraries/botbuilder-ai/src/qnaMaker.ts +++ b/libraries/botbuilder-ai/src/qnaMaker.ts @@ -5,9 +5,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { Activity, TurnContext, BotTelemetryClient, NullTelemetryClient } from 'botbuilder-core'; +import { TurnContext, BotTelemetryClient, NullTelemetryClient } from 'botbuilder-core'; -import { constants } from 'http2'; import { QnATelemetryConstants } from './qnaTelemetryConstants'; import { QnAMakerEndpoint } from './qnamaker-interfaces/qnamakerEndpoint'; import { QnAMakerMetadata } from './qnamaker-interfaces/qnamakerMetadata'; @@ -18,6 +17,7 @@ import { FeedbackRecords } from './qnamaker-interfaces/feedbackRecords'; import { GenerateAnswerUtils } from './qnamaker-utils/generateAnswerUtils'; import { ActiveLearningUtils } from './qnamaker-utils/activeLearningUtils'; import { TrainUtils } from './qnamaker-utils/trainUtils'; +import { QnAMakerResults } from './qnamaker-interfaces/qnamakerResults'; export const QNAMAKER_TRACE_TYPE = 'https://www.qnamaker.ai/schemas/trace'; export const QNAMAKER_TRACE_NAME = 'QnAMaker'; @@ -132,25 +132,50 @@ export class QnAMaker implements QnAMakerTelemetryClient { throw new TypeError('QnAMaker.getAnswers() requires a TurnContext.'); } + var response = await this.getAnswersRaw(context, options, telemetryProperties, telemetryMetrics); + + if (!response) { + return []; + } + + return response.answers; + } + + public async getAnswersRaw(context: TurnContext, options?: QnAMakerOptions, telemetryProperties?: {[key: string]:string}, telemetryMetrics?: {[key: string]:number} ): Promise { + if (!context) { + throw new TypeError('QnAMaker.getAnswers() requires a TurnContext.'); + } + const queryResult: QnAMakerResult[] = [] as QnAMakerResult[]; const question: string = this.getTrimmedMessageText(context); const queryOptions: QnAMakerOptions = { ...this._options, ...options } as QnAMakerOptions; this.generateAnswerUtils.validateOptions(queryOptions); - if (question.length > 0) { - const answers: QnAMakerResult[] = await this.generateAnswerUtils.queryQnaService(this.endpoint, question, queryOptions); - const sortedQnaAnswers: QnAMakerResult[] = GenerateAnswerUtils.sortAnswersWithinThreshold(answers, queryOptions); + var result: QnAMakerResults; + if (question.length > 0) { + result = await this.generateAnswerUtils.queryQnaServiceRaw(this.endpoint, question, queryOptions); + + const sortedQnaAnswers: QnAMakerResult[] = GenerateAnswerUtils.sortAnswersWithinThreshold(result.answers, queryOptions); queryResult.push(...sortedQnaAnswers); } + if (!result) { + return result; + } + // Log telemetry this.onQnaResults(queryResult, context, telemetryProperties, telemetryMetrics); await this.generateAnswerUtils.emitTraceInfo(context, queryResult, queryOptions); - return queryResult; + const qnaResponse: QnAMakerResults = { + activeLearningEnabled: result.activeLearningEnabled, + answers: queryResult + } + + return qnaResponse; } /** @@ -198,10 +223,10 @@ export class QnAMaker implements QnAMakerTelemetryClient { const trimmedAnswer: string = question ? question.trim() : ''; if (trimmedAnswer.length > 0) { - const answers: QnAMakerResult[] = await this.callService(this.endpoint, question, typeof top === 'number' ? top : 1); + const result: QnAMakerResults = await this.callService(this.endpoint, question, typeof top === 'number' ? top : 1); const minScore: number = typeof scoreThreshold === 'number' ? scoreThreshold : 0.001; - return answers.filter( + return result.answers.filter( (ans: QnAMakerResult) => ans.score >= minScore) .sort((a: QnAMakerResult, b: QnAMakerResult) => b.score - a.score); } @@ -236,8 +261,8 @@ export class QnAMaker implements QnAMakerTelemetryClient { * @remarks * This is exposed to enable better unit testing of the service. */ - protected async callService(endpoint: QnAMakerEndpoint, question: string, top: number): Promise { - return this.generateAnswerUtils.queryQnaService(endpoint, question, { top } as QnAMakerOptions); + protected async callService(endpoint: QnAMakerEndpoint, question: string, top: number): Promise { + return this.generateAnswerUtils.queryQnaServiceRaw(endpoint, question, { top } as QnAMakerOptions); } /** diff --git a/libraries/botbuilder-ai/src/qnamaker-interfaces/index.ts b/libraries/botbuilder-ai/src/qnamaker-interfaces/index.ts index 519f9babdc..173f18688d 100644 --- a/libraries/botbuilder-ai/src/qnamaker-interfaces/index.ts +++ b/libraries/botbuilder-ai/src/qnamaker-interfaces/index.ts @@ -10,6 +10,6 @@ export * from './qnamakerEndpoint'; export * from './qnamakerMetadata'; export * from './qnamakerOptions'; export * from './qnamakerResult'; -export * from './qnamakerTraceinfo'; +export * from './qnamakerTraceInfo'; export * from './feedbackRecord'; export * from './feedbackRecords'; \ No newline at end of file diff --git a/libraries/botbuilder-ai/src/qnamaker-interfaces/qnaRequestContext.ts b/libraries/botbuilder-ai/src/qnamaker-interfaces/qnaRequestContext.ts new file mode 100644 index 0000000000..670ab3d867 --- /dev/null +++ b/libraries/botbuilder-ai/src/qnamaker-interfaces/qnaRequestContext.ts @@ -0,0 +1,22 @@ +/** + * @module botbuilder-ai + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + /** + * The context associated with QnA. Used to mark if the current prompt is relevant with a previous question or not. + */ +export interface QnARequestContext { + + /** + * The previous QnA Id that was returned. + */ + previousQnAId: number; + + /** + * The previous user query/question. + */ + previousUserQuery: string; +} \ No newline at end of file diff --git a/libraries/botbuilder-ai/src/qnamaker-interfaces/qnaResponseContext.ts b/libraries/botbuilder-ai/src/qnamaker-interfaces/qnaResponseContext.ts new file mode 100644 index 0000000000..46c8796254 --- /dev/null +++ b/libraries/botbuilder-ai/src/qnamaker-interfaces/qnaResponseContext.ts @@ -0,0 +1,20 @@ +/** + * @module botbuilder-ai + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { QnAMakerPrompt } from './qnamakerPrompt'; + + /** + * The context associated with QnA. Used to mark if the qna response has related prompts. + */ +export interface QnAResponseContext { + + /** + * The prompts collection of related prompts. + */ + prompts: QnAMakerPrompt[]; +} \ No newline at end of file diff --git a/libraries/botbuilder-ai/src/qnamaker-interfaces/qnamakerOptions.ts b/libraries/botbuilder-ai/src/qnamaker-interfaces/qnamakerOptions.ts index d5c8789d1d..2d80a4f5df 100644 --- a/libraries/botbuilder-ai/src/qnamaker-interfaces/qnamakerOptions.ts +++ b/libraries/botbuilder-ai/src/qnamaker-interfaces/qnamakerOptions.ts @@ -7,6 +7,7 @@ */ import { QnAMakerMetadata } from './qnamakerMetadata'; +import { QnARequestContext } from './qnaRequestContext'; /** * Additional settings used to configure a `QnAMaker` instance. @@ -42,5 +43,15 @@ export interface QnAMakerOptions { * * @remarks Defaults to "100000" milliseconds. */ - timeout?: number; + timeout?: number; + + /** + * The context of the previous turn. + */ + context?: QnARequestContext; + + /** + * Id of the current question asked. + */ + qnaId?: number; } \ No newline at end of file diff --git a/libraries/botbuilder-ai/src/qnamaker-interfaces/qnamakerPrompt.ts b/libraries/botbuilder-ai/src/qnamaker-interfaces/qnamakerPrompt.ts new file mode 100644 index 0000000000..cfe91e27aa --- /dev/null +++ b/libraries/botbuilder-ai/src/qnamaker-interfaces/qnamakerPrompt.ts @@ -0,0 +1,32 @@ +/** + * @module botbuilder-ai + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + /** + * QnAMaker Prompt Object. + */ +export interface QnAMakerPrompt { + + /** + * Display Order - index of the prompt - used in ordering of the prompts. + */ + displayOrder: number; + + /** + * Qna id corresponding to the prompt - if QnaId is present, QnADTO object is ignored. + */ + qnaId: number; + + /** + * The QnA object returned from the API (Optional parameter). + */ + qna?: object; + + /** + * Display Text - Text displayed to represent a follow up question prompt. + */ + displayText: string; +} \ No newline at end of file diff --git a/libraries/botbuilder-ai/src/qnamaker-interfaces/qnamakerResult.ts b/libraries/botbuilder-ai/src/qnamaker-interfaces/qnamakerResult.ts index c440a658e0..c8545571a0 100644 --- a/libraries/botbuilder-ai/src/qnamaker-interfaces/qnamakerResult.ts +++ b/libraries/botbuilder-ai/src/qnamaker-interfaces/qnamakerResult.ts @@ -6,6 +6,8 @@ * Licensed under the MIT License. */ +import { QnAResponseContext } from './qnaResponseContext'; + /** * An individual answer returned by a call to the QnA Maker Service. */ @@ -39,4 +41,9 @@ export interface QnAMakerResult { * The index of the answer in the knowledge base. V3 uses 'qnaId', V4 uses 'id'. (If any) */ id?: number; + + /** + * Context for multi-turn responses. + */ + context?: QnAResponseContext; } \ No newline at end of file diff --git a/libraries/botbuilder-ai/src/qnamaker-interfaces/qnamakerResults.ts b/libraries/botbuilder-ai/src/qnamaker-interfaces/qnamakerResults.ts new file mode 100644 index 0000000000..a2ee0c7382 --- /dev/null +++ b/libraries/botbuilder-ai/src/qnamaker-interfaces/qnamakerResults.ts @@ -0,0 +1,24 @@ +/** + * @module botbuilder-ai + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { QnAMakerResult } from './qnamakerResult'; + + /** + * An object returned by a call to the QnA Maker Service. + */ +export interface QnAMakerResults { + /** + * The answers for a user query, sorted in decreasing order of ranking score. + */ + answers?: QnAMakerResult[]; + + /** + * The active learning enable flag. + */ + activeLearningEnabled?: boolean; +} \ No newline at end of file diff --git a/libraries/botbuilder-ai/src/qnamaker-interfaces/qnamakerTraceInfo.ts b/libraries/botbuilder-ai/src/qnamaker-interfaces/qnamakerTraceInfo.ts index 4626db5da6..3b84522fd3 100644 --- a/libraries/botbuilder-ai/src/qnamaker-interfaces/qnamakerTraceInfo.ts +++ b/libraries/botbuilder-ai/src/qnamaker-interfaces/qnamakerTraceInfo.ts @@ -8,6 +8,7 @@ import { Activity } from 'botbuilder-core'; import { QnAMakerResult } from './qnamakerResult'; +import { QnARequestContext } from './qnaRequestContext'; /** * Trace info that we collect and emit from a QnA Maker query @@ -47,4 +48,14 @@ export interface QnAMakerTraceInfo { * Metadata related to query. Not used in JavaScript SDK v4 yet. */ metadataBoost: any[]; + + /** + * The context for multi-turn responses. + */ + context?: QnARequestContext; + + /** + * Id of the current question asked. + */ + qnaId?: number; } \ No newline at end of file diff --git a/libraries/botbuilder-ai/src/qnamaker-utils/activeLearningUtils.ts b/libraries/botbuilder-ai/src/qnamaker-utils/activeLearningUtils.ts index f685c25795..d89d06bfc4 100644 --- a/libraries/botbuilder-ai/src/qnamaker-utils/activeLearningUtils.ts +++ b/libraries/botbuilder-ai/src/qnamaker-utils/activeLearningUtils.ts @@ -12,10 +12,10 @@ import { QnAMakerResult } from '../qnamaker-interfaces/qnamakerResult'; const MinimumScoreForLowScoreVariation = 20; // Previous Low Score Variation Multiplier -const PreviousLowScoreVariationMultiplier = 1.4; +const PreviousLowScoreVariationMultiplier = 0.7; // Max Low Score Variation Multiplier -const MaxLowScoreVariationMultiplier = 2.0; +const MaxLowScoreVariationMultiplier = 1.0; // Maximum Score For Low Score Variation const MaximumScoreForLowScoreVariation = 95.0; diff --git a/libraries/botbuilder-ai/src/qnamaker-utils/generateAnswerUtils.ts b/libraries/botbuilder-ai/src/qnamaker-utils/generateAnswerUtils.ts index 68731bb99e..c97967a934 100644 --- a/libraries/botbuilder-ai/src/qnamaker-utils/generateAnswerUtils.ts +++ b/libraries/botbuilder-ai/src/qnamaker-utils/generateAnswerUtils.ts @@ -9,6 +9,7 @@ import { TurnContext } from 'botbuilder-core'; import { QnAMakerResult } from '../qnamaker-interfaces/qnamakerResult'; +import { QnAMakerResults } from '../qnamaker-interfaces/qnamakerResults'; import { QnAMakerEndpoint } from '../qnamaker-interfaces/qnamakerEndpoint'; import { QnAMakerOptions } from '../qnamaker-interfaces/qnamakerOptions'; import { QnAMakerTraceInfo } from '../qnamaker-interfaces/qnamakerTraceInfo'; @@ -43,6 +44,18 @@ export class GenerateAnswerUtils { * @param options (Optional) The options for the QnA Maker knowledge base. If null, constructor option is used for this instance. */ public async queryQnaService(endpoint: QnAMakerEndpoint, question: string, options?: QnAMakerOptions): Promise { + var result = await this.queryQnaServiceRaw(endpoint, question, options); + + return result.answers; + } + + /** + * Called internally to query the QnA Maker service. + * @param endpoint The endpoint of the knowledge base to query. + * @param question Question which need to be queried. + * @param options (Optional) The options for the QnA Maker knowledge base. If null, constructor option is used for this instance. + */ + public async queryQnaServiceRaw(endpoint: QnAMakerEndpoint, question: string, options?: QnAMakerOptions): Promise { const url: string = `${ endpoint.host }/knowledgebases/${ endpoint.knowledgeBaseId }/generateanswer`; const queryOptions: QnAMakerOptions = { ...this._options, ...options } as QnAMakerOptions; @@ -61,25 +74,27 @@ export class GenerateAnswerUtils { /** * Emits a trace event detailing a QnA Maker call and its results. * - * @param context Context for the current turn of conversation with the user. + * @param turnContext Turn Context for the current turn of conversation with the user. * @param answers Answers returned by QnA Maker. * @param options (Optional) The options for the QnA Maker knowledge base. If null, constructor option is used for this instance. */ - public async emitTraceInfo(context: TurnContext, answers: QnAMakerResult[], queryOptions?: QnAMakerOptions): Promise { + public async emitTraceInfo(turnContext: TurnContext, answers: QnAMakerResult[], queryOptions?: QnAMakerOptions): Promise { const requestOptions: QnAMakerOptions = { ...this._options, ...queryOptions }; - const { scoreThreshold, top, strictFilters, metadataBoost } = requestOptions; + const { scoreThreshold, top, strictFilters, metadataBoost, context, qnaId } = requestOptions; const traceInfo: QnAMakerTraceInfo = { - message: context.activity, + message: turnContext.activity, queryResults: answers, knowledgeBaseId: this.endpoint.knowledgeBaseId, scoreThreshold, top, strictFilters, - metadataBoost + metadataBoost, + context, + qnaId, }; - return context.sendActivity({ + return turnContext.sendActivity({ type: 'trace', valueType: QNAMAKER_TRACE_TYPE, name: QNAMAKER_TRACE_NAME, @@ -119,8 +134,8 @@ export class GenerateAnswerUtils { .sort((a: QnAMakerResult, b: QnAMakerResult) => b.score - a.score); } - private formatQnaResult(qnaResult: any): QnAMakerResult[] { - return qnaResult.answers.map((ans: any) => { + private formatQnaResult(qnaResult: any): QnAMakerResults { + qnaResult.answers = qnaResult.answers.map((ans: any) => { ans.score = ans.score / 100; if (ans.qnaId) { @@ -130,6 +145,10 @@ export class GenerateAnswerUtils { return ans as QnAMakerResult; }); + + qnaResult.activeLearningEnabled = (qnaResult.activeLearningEnabled != null) ? qnaResult.activeLearningEnabled : true; + + return qnaResult; } private validateScoreThreshold(scoreThreshold: number): void { diff --git a/libraries/botbuilder-ai/tests/TestData/QnAMaker/should_return_answer_and_active_learning_flag.json b/libraries/botbuilder-ai/tests/TestData/QnAMaker/should_return_answer_and_active_learning_flag.json new file mode 100644 index 0000000000..c66774675d --- /dev/null +++ b/libraries/botbuilder-ai/tests/TestData/QnAMaker/should_return_answer_and_active_learning_flag.json @@ -0,0 +1,26 @@ +{ + "activeLearningEnabled": false, + "answers": [ + { + "questions": [ + "how do I clean the stove?" + ], + "answer": "BaseCamp: You can use a damp rag to clean around the Power Pack", + "score": 100, + "id": 5, + "source": "Editorial", + "metadata": [], + "context": { + "isContextOnly": true, + "prompts": [ + { + "displayOrder": 0, + "qnaId": 55, + "qna": null, + "displayText": "Where can I buy?" + } + ] + } + } + ] + } \ No newline at end of file diff --git a/libraries/botbuilder-ai/tests/TestData/QnAMaker/should_return_answer_with_high_score_provided_context.json b/libraries/botbuilder-ai/tests/TestData/QnAMaker/should_return_answer_with_high_score_provided_context.json new file mode 100644 index 0000000000..e72863e108 --- /dev/null +++ b/libraries/botbuilder-ai/tests/TestData/QnAMaker/should_return_answer_with_high_score_provided_context.json @@ -0,0 +1,18 @@ +{ + "answers": [ + { + "questions": [ + "Where can I buy cleaning products?" + ], + "answer": "Any DIY store", + "score": 100, + "id": 55, + "source": "Editorial", + "metadata": [], + "context": { + "isContextOnly": true, + "prompts": [] + } + } + ] + } \ No newline at end of file diff --git a/libraries/botbuilder-ai/tests/TestData/QnAMaker/should_return_answer_with_high_score_provided_id.json b/libraries/botbuilder-ai/tests/TestData/QnAMaker/should_return_answer_with_high_score_provided_id.json new file mode 100644 index 0000000000..e72863e108 --- /dev/null +++ b/libraries/botbuilder-ai/tests/TestData/QnAMaker/should_return_answer_with_high_score_provided_id.json @@ -0,0 +1,18 @@ +{ + "answers": [ + { + "questions": [ + "Where can I buy cleaning products?" + ], + "answer": "Any DIY store", + "score": 100, + "id": 55, + "source": "Editorial", + "metadata": [], + "context": { + "isContextOnly": true, + "prompts": [] + } + } + ] + } \ No newline at end of file diff --git a/libraries/botbuilder-ai/tests/TestData/QnAMaker/should_return_answer_with_low_score_not_provided_context.json b/libraries/botbuilder-ai/tests/TestData/QnAMaker/should_return_answer_with_low_score_not_provided_context.json new file mode 100644 index 0000000000..1790ea662a --- /dev/null +++ b/libraries/botbuilder-ai/tests/TestData/QnAMaker/should_return_answer_with_low_score_not_provided_context.json @@ -0,0 +1,32 @@ +{ + "answers": [ + { + "questions": [ + "Where can I buy home appliances?" + ], + "answer": "Any Walmart store", + "score": 68, + "id": 56, + "source": "Editorial", + "metadata": [], + "context": { + "isContextOnly": false, + "prompts": [] + } + }, + { + "questions": [ + "Where can I buy cleaning products?" + ], + "answer": "Any DIY store", + "score": 56, + "id": 55, + "source": "Editorial", + "metadata": [], + "context": { + "isContextOnly": false, + "prompts": [] + } + } + ] + } \ No newline at end of file diff --git a/libraries/botbuilder-ai/tests/TestData/QnAMaker/should_return_answer_with_prompts.json b/libraries/botbuilder-ai/tests/TestData/QnAMaker/should_return_answer_with_prompts.json new file mode 100644 index 0000000000..da246deea4 --- /dev/null +++ b/libraries/botbuilder-ai/tests/TestData/QnAMaker/should_return_answer_with_prompts.json @@ -0,0 +1,25 @@ +{ + "answers": [ + { + "questions": [ + "how do I clean the stove?" + ], + "answer": "BaseCamp: You can use a damp rag to clean around the Power Pack", + "score": 100, + "id": 5, + "source": "Editorial", + "metadata": [], + "context": { + "isContextOnly": true, + "prompts": [ + { + "displayOrder": 0, + "qnaId": 55, + "qna": null, + "displayText": "Where can I buy?" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/libraries/botbuilder-ai/tests/qnaMaker.test.js b/libraries/botbuilder-ai/tests/qnaMaker.test.js index 13faa68ea4..bc85e95db3 100644 --- a/libraries/botbuilder-ai/tests/qnaMaker.test.js +++ b/libraries/botbuilder-ai/tests/qnaMaker.test.js @@ -181,6 +181,18 @@ describe('QnAMaker', function () { }); + it('should return answer and active learning flag', async function() { + const noOptionsQnA = new QnAMaker(endpoint); + const noOptionsContext = new TestContext({ text: 'where are the unicorns?' }) + const defaultNumberOfAnswers = 1; + + const results = await noOptionsQnA.getAnswersRaw(noOptionsContext); + const numberOfResults = results.answers.length; + + assert.strictEqual(numberOfResults, defaultNumberOfAnswers, 'Should return only 1 answer with default settings (i.e. no options specified) for question with answer.'); + assert.strictEqual(results.activeLearningEnabled, false, 'Should return false for active learning flag.'); + }); + it('should sort the answers in the qna results from highest to lowest score', async function() { const qna = new QnAMaker(endpoint); const context = new TestContext({ text: "what's your favorite animal?" }); @@ -192,6 +204,54 @@ describe('QnAMaker', function () { assert.strictEqual(qnaResults, descendingQnaResults, 'answers should be sorted from greatest to least score'); }); + it('should return answer with prompts', async function() { + const qna = new QnAMaker(endpoint); + const context = new TestContext({ text: "how do I clean the stove?" }); + const options = { top: 2 }; + + const qnaResults = await qna.getAnswers(context, options); + + assert.strictEqual(qnaResults.length, 1, 'one answer should be returned'); + assert.strictEqual(qnaResults[0].context.prompts.length > 0, true, 'One or more prompts should be present'); + }); + + it('should return answer with high score provided context', async function() { + const qna = new QnAMaker(endpoint); + const turnContext = new TestContext({ text: "where can I buy?" }); + + const context = { previousQnAId: 5, previousUserQuery: "how do I clean the stove?"}; + const options = { top: 2, context: context }; + + const qnaResults = await qna.getAnswers(turnContext, options); + + assert.strictEqual(qnaResults.length, 1, 'one answer should be returned'); + assert.strictEqual(qnaResults[0].score, 1, 'score should be high'); + }); + + it('should return answer with high score provided id', async function() { + const qna = new QnAMaker(endpoint); + const turnContext = new TestContext({ text: "where can I buy?" }); + + const options = { top: 2, qnaId: 55 }; + + const qnaResults = await qna.getAnswers(turnContext, options); + + assert.strictEqual(qnaResults.length, 1, 'one answer should be returned'); + assert.strictEqual(qnaResults[0].score, 1, 'score should be high'); + }); + + it('should return answer with low score not provided context', async function() { + const qna = new QnAMaker(endpoint); + const turnContext = new TestContext({ text: "where can I buy?" }); + + const options = { top: 2, context: null }; + + const qnaResults = await qna.getAnswers(turnContext, options); + + assert.strictEqual(qnaResults.length, 2, 'one answer should be returned'); + assert.strictEqual(qnaResults[0].score < 1, true, 'score should be low'); + }); + it('should return answer with timeout option specified', async function() { const timeoutOption = { timeout: 500000 }; const qna = new QnAMaker(endpoint, timeoutOption); diff --git a/libraries/botbuilder-ai/tsconfig.json b/libraries/botbuilder-ai/tsconfig.json index 016cc03344..f7ed49127c 100644 --- a/libraries/botbuilder-ai/tsconfig.json +++ b/libraries/botbuilder-ai/tsconfig.json @@ -1,17 +1,15 @@ -{ - "compilerOptions": { - "plugins": [ - { "name": "typescript-tslint-plugin" } - ], - "target": "ESNext", - "module": "commonjs", - "declaration": true, - "sourceMap": true, - "outDir": "./lib", - "rootDir": "./src", - "types" : ["node"] - }, - "include": [ - "src/**/*" - ] +{ + "compilerOptions": { + "target": "es6", + "lib": ["es2015"], + "module": "commonjs", + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + "types" : ["node"] + }, + "include": [ + "src/**/*" + ] } \ No newline at end of file diff --git a/libraries/botbuilder-applicationinsights/package.json b/libraries/botbuilder-applicationinsights/package.json index 243568a3d6..9b73683a6d 100644 --- a/libraries/botbuilder-applicationinsights/package.json +++ b/libraries/botbuilder-applicationinsights/package.json @@ -21,9 +21,7 @@ "main": "./lib/index.js", "typings": "./lib/index.d.ts", "dependencies": { - "appinsights-usage": "1.0.2", "applicationinsights": "1.2.0", - "applicationinsights-js": "1.0.20", "botbuilder-core": "4.1.6", "cls-hooked": "^4.2.2" }, diff --git a/libraries/botbuilder-applicationinsights/src/index.ts b/libraries/botbuilder-applicationinsights/src/index.ts index b461c34586..e9f4f4f786 100644 --- a/libraries/botbuilder-applicationinsights/src/index.ts +++ b/libraries/botbuilder-applicationinsights/src/index.ts @@ -6,3 +6,4 @@ * Licensed under the MIT License. */ export * from './applicationInsightsTelemetryClient'; +export * from './telemetryInitializerMiddleware'; diff --git a/libraries/botbuilder-applicationinsights/src/telemetryInitializerMiddleware.ts b/libraries/botbuilder-applicationinsights/src/telemetryInitializerMiddleware.ts new file mode 100644 index 0000000000..9d19499258 --- /dev/null +++ b/libraries/botbuilder-applicationinsights/src/telemetryInitializerMiddleware.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { TelemetryLoggerMiddleware } from 'botbuilder-core'; +import { Middleware } from 'botbuilder-core'; +import { TurnContext } from 'botbuilder-core'; +import { CorrelationContext } from 'applicationinsights/out/AutoCollection/CorrelationContextManager'; +import * as appInsights from 'applicationinsights'; + +/** + * Middleware for storing the incoming activity to be made available to Application Insights and optionally run the TelemetryLoggerMiddleware. + * Uses the botTelemetryClient interface. + */ +export class TelemetryInitializerMiddleware implements Middleware { + + // tslint:disable:variable-name + private readonly _logActivityTelemetry: boolean; + private readonly _telemetryLoggerMiddleware: TelemetryLoggerMiddleware; + private _correlationContext: CorrelationContext; + // tslint:enable:variable-name + + /** + * Initializes a new instance of the TelemetryInitializerMiddleware class. + * @param telemetryLoggerMiddleware The TelemetryLoggerMiddleware used for logging activity telemetry. + * * @param logActivityTelemetry (Optional) Enable/Disable logging of activity telemetry. + */ + constructor(telemetryLoggerMiddleware: TelemetryLoggerMiddleware, logActivityTelemetry: boolean = false) { + this._telemetryLoggerMiddleware = telemetryLoggerMiddleware; + this._logActivityTelemetry = logActivityTelemetry; + } + + /** + * Gets a value indicating whether determines whether to call the telemetry logging middleware to log activity events. + */ + public get logActivityTelemetry(): boolean { return this._logActivityTelemetry; } + + /** + * Gets the currently configured TelemetryLoggerMiddleware that logs activity events. + */ + public get telemetryClient(): TelemetryLoggerMiddleware { return this._telemetryLoggerMiddleware; } + + /** + * Sets the correlation context so that a mock context can be passed in for testing purposes. + */ + protected set appInsightsCorrelationContext(value: CorrelationContext) { this._correlationContext = value; } + + /** + * Gets the correlation context that can be used for testing purposes. + */ + protected get appInsightsCorrelationContext() { return this._correlationContext; } + + /** + * Store the incoming activity on the App Insights Correlation Context and optionally calls the TelemetryLoggerMiddleware + * @param context The context object for this turn. + * @param next The delegate to call to continue the bot middleware pipeline + */ + public async onTurn(context: TurnContext, next: () => Promise): Promise { + if (context === null) { + throw new Error('context is null'); + } + + if (context.activity && context.activity.id) { + const correlationContext: CorrelationContext = this._correlationContext || appInsights.getCorrelationContext(); + if(correlationContext) + { + correlationContext['activity'] = context.activity; + } + } + + if(this._logActivityTelemetry && this._telemetryLoggerMiddleware) + { + await this._telemetryLoggerMiddleware.onTurn(context, next); + } + else if (next !== null) { + await next(); + } + } +} \ No newline at end of file diff --git a/libraries/botbuilder-applicationinsights/tests/telemetryInitializer.test.js b/libraries/botbuilder-applicationinsights/tests/telemetryInitializer.test.js new file mode 100644 index 0000000000..8a755f29a5 --- /dev/null +++ b/libraries/botbuilder-applicationinsights/tests/telemetryInitializer.test.js @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +const assert = require('assert'); +const { TestAdapter, ActivityTypes, TelemetryLoggerMiddleware } = require('botbuilder-core'); +const { TelemetryInitializerMiddleware } = require('../'); + +class TestInitializerMiddleware extends TelemetryInitializerMiddleware { + constructor(botTelemetryClient, logActivities, mockCorrelationContext) { + super(botTelemetryClient, logActivities); + + this.appInsightsCorrelationContext = mockCorrelationContext; + } +} + +describe(`TelemetryInitializerMiddleware`, function() { + this.timeout(5000); + + it(`telemetry initializer stores activity`, function(done) { + + var telemetryClient = { + trackEvent: (telemetry) => { + } + }; + + var telemetryLoggerMiddleware = new TelemetryLoggerMiddleware(telemetryClient, true); + var initializerMiddleware = new TestInitializerMiddleware(telemetryLoggerMiddleware, true, []); + + var adapter = new TestAdapter(async (context) => { + conversationId = context.activity.conversation.id; + var typingActivity = { + type: ActivityTypes.Typing, + relatesTo: context.activity.relatesTo + }; + await context.sendActivity(typingActivity); + await context.sendActivity(`echo:${ context.activity.text }`); + }).use(initializerMiddleware); + + adapter + .send('foo') + .then(res => { assert(initializerMiddleware.appInsightsCorrelationContext.activity.text == 'foo'); }) + .assertReply(activity => assert.equal(activity.type, ActivityTypes.Typing)) + .assertReply('echo:foo') + .send('bar') + .then(res => { assert(initializerMiddleware.appInsightsCorrelationContext.activity.text == 'bar'); }) + .assertReply(activity => assert.equal(activity.type, ActivityTypes.Typing)) + .assertReply('echo:bar') + .then(done); + }); + + it(`calls logging middleware (when logActivityTelemetry is true)`, function(done) { + + var callCount = 0; + + var telemetryClient = { + trackEvent: (telemetry) => { + assert(telemetry, 'telemetry is null'); + ++callCount; + assert(callCount < 7 && callCount > 0); + } + }; + + var telemetryLoggerMiddleware = new TelemetryLoggerMiddleware(telemetryClient, true); + var initializerMiddleware = new TestInitializerMiddleware(telemetryLoggerMiddleware, true, []); + + var adapter = new TestAdapter(async (context) => { + conversationId = context.activity.conversation.id; + var typingActivity = { + type: ActivityTypes.Typing, + relatesTo: context.activity.relatesTo + }; + await context.sendActivity(typingActivity); + await context.sendActivity(`echo:${ context.activity.text }`); + }).use(initializerMiddleware); + + adapter + .send('foo') + .assertReply(activity => assert.equal(activity.type, ActivityTypes.Typing)) + .assertReply('echo:foo') + .send('bar') + .assertReply(activity => assert.equal(activity.type, ActivityTypes.Typing)) + .assertReply('echo:bar') + .then(done); + }); + + it(`does not call logging middleware (when logActivityTelemetry is false)`, function(done) { + + var telemetryClient = { + trackEvent: (telemetry) => { + assert.fail('logging middleware was called'); + } + }; + + var telemetryLoggerMiddleware = new TelemetryLoggerMiddleware(telemetryClient, true); + var initializerMiddleware = new TestInitializerMiddleware(telemetryLoggerMiddleware, false, []); + + var adapter = new TestAdapter(async (context) => { + conversationId = context.activity.conversation.id; + var typingActivity = { + type: ActivityTypes.Typing, + relatesTo: context.activity.relatesTo + }; + await context.sendActivity(typingActivity); + await context.sendActivity(`echo:${ context.activity.text }`); + }).use(initializerMiddleware); + + adapter + .send('foo') + .assertReply(activity => assert.equal(activity.type, ActivityTypes.Typing)) + .assertReply('echo:foo') + .send('bar') + .assertReply(activity => assert.equal(activity.type, ActivityTypes.Typing)) + .assertReply('echo:bar') + .then(done); + }); + + it(`correlation context is null (disabled) middleware does not fail`, function(done) { + + var callCount = 0; + + var telemetryClient = { + trackEvent: (telemetry) => { + assert(telemetry, 'telemetry is null'); + ++callCount; + assert(callCount < 7 && callCount > 0); + } + }; + + var telemetryLoggerMiddleware = new TelemetryLoggerMiddleware(telemetryClient, true); + var initializerMiddleware = new TestInitializerMiddleware(telemetryLoggerMiddleware, true, []); + + var adapter = new TestAdapter(async (context) => { + conversationId = context.activity.conversation.id; + var typingActivity = { + type: ActivityTypes.Typing, + relatesTo: context.activity.relatesTo + }; + await context.sendActivity(typingActivity); + await context.sendActivity(`echo:${ context.activity.text }`); + }).use(initializerMiddleware); + + adapter + .send('foo') + .assertReply(activity => assert.equal(activity.type, ActivityTypes.Typing)) + .assertReply('echo:foo') + .send('bar') + .assertReply(activity => assert.equal(activity.type, ActivityTypes.Typing)) + .assertReply('echo:bar') + .then(done); + }); +}); \ No newline at end of file diff --git a/libraries/botbuilder-applicationinsights/tsconfig.json b/libraries/botbuilder-applicationinsights/tsconfig.json index 3788218c0b..f7ed49127c 100644 --- a/libraries/botbuilder-applicationinsights/tsconfig.json +++ b/libraries/botbuilder-applicationinsights/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "ESNext", + "target": "es6", + "lib": ["es2015"], "module": "commonjs", "declaration": true, "sourceMap": true, diff --git a/libraries/botbuilder-azure/package.json b/libraries/botbuilder-azure/package.json index 5ad4ff1311..5eb875ccbc 100644 --- a/libraries/botbuilder-azure/package.json +++ b/libraries/botbuilder-azure/package.json @@ -21,6 +21,7 @@ "main": "./lib/index.js", "typings": "./lib/index.d.ts", "dependencies": { + "@azure/cosmos": "^3.3.1", "@types/documentdb": "^1.10.5", "@types/node": "^10.12.18", "azure-storage": "2.10.2", diff --git a/libraries/botbuilder-azure/src/azureBlobTranscriptStore.ts b/libraries/botbuilder-azure/src/azureBlobTranscriptStore.ts index 1d3f079c81..5e4ff908c5 100644 --- a/libraries/botbuilder-azure/src/azureBlobTranscriptStore.ts +++ b/libraries/botbuilder-azure/src/azureBlobTranscriptStore.ts @@ -79,7 +79,7 @@ export class AzureBlobTranscriptStore implements TranscriptStore { { fromid: activity.from.id, recipientid: activity.recipient.id, - timestamp: activity.timestamp.getTime().toString() + timestamp: activity.timestamp.toJSON() } ); @@ -205,8 +205,8 @@ export class AzureBlobTranscriptStore implements TranscriptStore { ): Promise { const listBlobResult = await this.client.listBlobsSegmentedWithPrefixAsync(container, prefix, token, { include: 'metadata' }); listBlobResult.entries.some(blob => { - const timestamp: number = parseInt(blob.metadata.timestamp, 10); - if (timestamp >= startDate.getTime()) { + const timestamp: Date = new Date(blob.metadata.timestamp); + if (timestamp >= startDate) { if (continuationToken) { if (blob.name === continuationToken) { continuationToken = null; @@ -335,7 +335,7 @@ export class AzureBlobTranscriptStore implements TranscriptStore { ).withFilter(new azure.LinearRetryPolicyFilter(5, 500)); // The perfect use case for a Proxy - return new Proxy({}, { + return new Proxy({}, { get(target: azure.services.blob.blobservice.BlobService, p: PropertyKey): Promise { const prop = p.toString().endsWith('Async') ? p.toString().replace('Async', '') :p; return target[p] || (target[p] = denodeify(blobService, blobService[prop])); diff --git a/libraries/botbuilder-azure/src/cosmosDbPartitionedStorage.ts b/libraries/botbuilder-azure/src/cosmosDbPartitionedStorage.ts new file mode 100644 index 0000000000..ef17d7dbbd --- /dev/null +++ b/libraries/botbuilder-azure/src/cosmosDbPartitionedStorage.ts @@ -0,0 +1,243 @@ +/** + * @module botbuilder-azure + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Storage, StoreItems } from 'botbuilder'; +import { Container, CosmosClient, CosmosClientOptions } from '@azure/cosmos'; +import { CosmosDbKeyEscape } from './cosmosDbKeyEscape'; +import * as semaphore from 'semaphore'; + +const _semaphore: semaphore.Semaphore = semaphore(1); + +/** + * Cosmos DB Partitioned Storage Options. + */ +export interface CosmosDbPartitionedStorageOptions { + /** + * The CosmosDB endpoint. + */ + cosmosDbEndpoint: string; + /** + * The authentication key for Cosmos DB. + */ + authKey: string; + /** + * The database identifier for Cosmos DB instance. + */ + databaseId: string; + /** + * The container identifier. + */ + containerId: string; + /** + * The options for the CosmosClient. + */ + cosmosClientOptions?: CosmosClientOptions; + /** + * The throughput set when creating the Container. Defaults to 400. + */ + containerThroughput?: number; +} + +/** + * @private + * Internal data structure for storing items in a CosmosDB Collection. + */ +class DocumentStoreItem { + /** + * Gets the PartitionKey path to be used for this document type. + */ + public static get partitionKeyPath(): string { + return '/id'; + } + /** + * Gets or sets the sanitized Id/Key used as PrimaryKey. + */ + public id: string + /** + * Gets or sets the un-sanitized Id/Key. + * + */ + public realId: string + /** + * Gets or sets the persisted object. + */ + public document: object + /** + * Gets or sets the ETag information for handling optimistic concurrency updates. + */ + public eTag: string + /** + * Gets the PartitionKey value for the document. + */ + public get partitionKey(): string { + return this.id; + } + + // We can't make the partitionKey optional AND have it auto-get this.realId, so we'll use a constructor + public constructor(storeItem: { id: string; realId: string; document: object; eTag?: string}) { + for (let prop in storeItem) { + this[prop] = storeItem[prop]; + } + } +} + +/** + * Implements an CosmosDB based storage provider using partitioning for a bot. + */ +export class CosmosDbPartitionedStorage implements Storage { + private container: Container; + private readonly cosmosDbStorageOptions: CosmosDbPartitionedStorageOptions; + private client: CosmosClient; + + /** + * Initializes a new instance of the class. + * using the provided CosmosDB credentials, database ID, and container ID. + * + * @param cosmosDbStorageOptions Cosmos DB partitioned storage configuration options. + */ + public constructor(cosmosDbStorageOptions: CosmosDbPartitionedStorageOptions) { + if (!cosmosDbStorageOptions) { throw new ReferenceError('CosmosDbPartitionedStorageOptions is required.'); } + if (!cosmosDbStorageOptions.cosmosDbEndpoint) { throw new ReferenceError('cosmosDbEndpoint for CosmosDB is required.'); } + if (!cosmosDbStorageOptions.authKey) { throw new ReferenceError('authKey for CosmosDB is required.'); } + if (!cosmosDbStorageOptions.databaseId) { throw new ReferenceError('databaseId is for CosmosDB required.'); } + if (!cosmosDbStorageOptions.containerId) { throw new ReferenceError('containerId for CosmosDB is required.'); } + + this.cosmosDbStorageOptions = cosmosDbStorageOptions; + } + + public async read(keys: string[]): Promise { + if (!keys) { throw new ReferenceError(`Keys are required when reading.`); } + else if (keys.length === 0) { return {}; } + + await this.initialize(); + + const storeItems: StoreItems = {}; + + await Promise.all(keys.map(async (k: string): Promise => { + try { + const escapedKey = CosmosDbKeyEscape.escapeKey(k); + + const readItemResponse = await this.container.item(escapedKey, escapedKey).read(); + const documentStoreItem = readItemResponse.resource; + if (documentStoreItem) { + storeItems[documentStoreItem.realId] = documentStoreItem.document; + storeItems[documentStoreItem.realId].eTag = documentStoreItem._etag; + } + } catch (err) { + // When an item is not found a CosmosException is thrown, but we want to + // return an empty collection so in this instance we catch and do not rethrow. + // Throw for any other exception. + if (err.code === 404) { } + // Throw unique error for 400s + else if (err.code === 400) { + this.throwInformativeError(`Error reading from container. You might be attempting to read from a non-partitioned + container or a container that does not use '/id' as the partitionKeyPath`, err); + } else { + this.throwInformativeError('Error reading from container', err); + } + } + })); + + return storeItems; + } + + public async write(changes: StoreItems): Promise { + if (!changes) { throw new ReferenceError(`Changes are required when writing.`); } + else if (changes.length === 0) { return; } + + await this.initialize(); + + await Promise.all(Object.keys(changes).map(async (k: string): Promise => { + const changesCopy: any = {...changes[k]}; + + // Remove eTag from JSON object that was copied from IStoreItem. + // The ETag information is updated as an _etag attribute in the document metadata. + delete changesCopy.eTag; + const documentChange = new DocumentStoreItem({ + id: CosmosDbKeyEscape.escapeKey(k), + realId: k, + document: changesCopy + }); + + const eTag: string = changes[k].eTag; + const ac = eTag !== '*' && eTag != null && eTag.length > 0 ? { accessCondition: { type: 'IfMatch', condition: eTag } } : undefined; + try { + await this.container.items + .upsert(documentChange, ac); + } catch (err) { + this.throwInformativeError('Error upserting document', err); + } + })); + } + + public async delete(keys: string[]): Promise { + + await this.initialize(); + + await Promise.all(keys.map(async (k: string): Promise => { + const escapedKey = CosmosDbKeyEscape.escapeKey(k); + try { + await this.container + .item(escapedKey, escapedKey) + .delete(); + } catch (err) { + // If trying to delete a document that doesn't exist, do nothing. Otherwise, throw + if (err.code === 404) { } + else { + this.throwInformativeError('Unable to delete document', err); + } + } + })); + } + + /** + * Connects to the CosmosDB database and creates / gets the container. + */ + public async initialize(): Promise { + if (!this.container) { + + if (!this.client) { + this.client = new CosmosClient({ + endpoint: this.cosmosDbStorageOptions.cosmosDbEndpoint, + key: this.cosmosDbStorageOptions.authKey, + ...this.cosmosDbStorageOptions.cosmosClientOptions, + }); + } + + if (!this.container) { + this.container = await new Promise((resolve: Function): void => { + _semaphore.take(async (): Promise => { + const result = await this.client + .database(this.cosmosDbStorageOptions.databaseId) + .containers.createIfNotExists({ + id: this.cosmosDbStorageOptions.containerId, + partitionKey: { + paths: [DocumentStoreItem.partitionKeyPath] + }, + throughput: this.cosmosDbStorageOptions.containerThroughput + }); + _semaphore.leave(); + resolve(result.container); + }); + }); + } + } + } + + /** + * The Cosmos JS SDK doesn't return very descriptive errors and not all errors contain a body. + * This provides more detailed errors and err['message'] prevents ReferenceError + */ + private throwInformativeError(prependedMessage: string, err: Error|object|string): void { + if (typeof err === 'string') { + err = new Error(err); + } + err['message'] = `[${ prependedMessage }] ${ err['message'] }`; + throw err; + } +} \ No newline at end of file diff --git a/libraries/botbuilder-azure/src/index.ts b/libraries/botbuilder-azure/src/index.ts index 81b90e39ca..b3fea9ecdf 100644 --- a/libraries/botbuilder-azure/src/index.ts +++ b/libraries/botbuilder-azure/src/index.ts @@ -6,6 +6,7 @@ * Licensed under the MIT License. */ export * from './cosmosDbStorage'; +export * from './cosmosDbPartitionedStorage'; export * from './blobStorage'; export * from './azureBlobTranscriptStore'; export * from './cosmosDbKeyEscape'; diff --git a/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_correctly_proceed_through_a_waterfall_dialog.json b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_correctly_proceed_through_a_waterfall_dialog.json new file mode 100644 index 0000000000..dc595ca68f --- /dev/null +++ b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_correctly_proceed_through_a_waterfall_dialog.json @@ -0,0 +1,1415 @@ +[ + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/test*2fconversations*2fConvo1*2f", + "body": "", + "status": 404, + "response": { + "code": "NotFound", + "message": "Message: {\"Errors\":[\"Resource Not Found\"]}\r\nActivityId: 0ffb7eaf-ef3a-4f72-aea1-2b6a51721bd3, Request URI: /apps/DocDbApp/services/DocDbServer15/partitions/a4cb495b-38c8-11e6-8106-8cdcd42c33be/replicas/1p/, RequestStats: \r\nRequestStartTime: 2019-10-25T22:08:23.1702878Z, RequestEndTime: 2019-10-25T22:08:23.1712889Z, Number of regions attempted: 1\r\nResponseTime: 2019-10-25T22:08:23.1712889Z, StoreResult: StorePhysicalAddress: rntbd://127.0.0.1:10253/apps/DocDbApp/services/DocDbServer15/partitions/a4cb495b-38c8-11e6-8106-8cdcd42c33be/replicas/1p/, LSN: 2134, GlobalCommittedLsn: -1, PartitionKeyRangeId: 0, IsValid: True, StatusCode: 404, SubStatusCode: 0, RequestCharge: 1, ItemLSN: -1, SessionToken: -1#2134, UsingLocalLSN: True, TransportException: null, ResourceType: Document, OperationType: Read\r\n, SDK: Microsoft.Azure.Documents.Common/2.4.0.0" + }, + "rawHeaders": [ + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/test*2fconversations*2fConvo1*2f", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "lsn", + "2134", + "x-ms-schemaversion", + "1.8", + "x-ms-quorum-acked-lsn", + "2134", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2697", + "x-ms-cosmos-llsn", + "2134", + "x-ms-cosmos-quorum-acked-llsn", + "2134", + "x-ms-session-token", + "0:-1#2134", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "0ffb7eaf-ef3a-4f72-aea1-2b6a51721bd3", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "POST", + "path": "/dbs/test-db/colls/bot-storage/docs", + "body": { + "id": "test*2fconversations*2fConvo1*2f", + "realId": "test/conversations/Convo1/", + "document": { + "dialogState": { + "dialogStack": [ + { + "id": "waterfallDialog", + "state": { + "options": {}, + "values": { + "instanceId": "fakeInstanceId" + }, + "stepIndex": 0 + } + } + ] + } + } + }, + "status": 201, + "response": { + "id": "test*2fconversations*2fConvo1*2f", + "realId": "test/conversations/Convo1/", + "document": { + "dialogState": { + "dialogStack": [ + { + "id": "waterfallDialog", + "state": { + "options": {}, + "values": { + "instanceId": "a7c67aaf-20cd-a70b-8521-08053510b9c9" + }, + "stepIndex": 0 + } + } + ] + } + }, + "_rid": "OuRPAMPSwdkHAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkHAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7ba634c01d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7ba634c01d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=5;collectionSize=6;", + "lsn", + "2135", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2134", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2698", + "x-ms-cosmos-llsn", + "2135", + "x-ms-cosmos-quorum-acked-llsn", + "2134", + "x-ms-session-token", + "0:-1#2135", + "x-ms-request-charge", + "8.38", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "5e1edc02-9925-4b3a-950b-48a4ec928955", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/test*2fconversations*2fConvo1*2f", + "body": "", + "status": 200, + "response": { + "id": "test*2fconversations*2fConvo1*2f", + "realId": "test/conversations/Convo1/", + "document": { + "dialogState": { + "dialogStack": [ + { + "id": "waterfallDialog", + "state": { + "options": {}, + "values": { + "instanceId": "a7c67aaf-20cd-a70b-8521-08053510b9c9" + }, + "stepIndex": 0 + } + } + ] + } + }, + "_rid": "OuRPAMPSwdkHAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkHAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7ba634c01d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/test*2fconversations*2fConvo1*2f", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7ba634c01d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=6;collectionSize=6;", + "lsn", + "2135", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2135", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2135", + "x-ms-transport-request-id", + "2699", + "x-ms-cosmos-llsn", + "2135", + "x-ms-cosmos-quorum-acked-llsn", + "2135", + "x-ms-cosmos-item-llsn", + "2135", + "x-ms-session-token", + "0:-1#2135", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "3374ea81-14aa-47c0-ae27-f3f6b0e14c63", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "POST", + "path": "/dbs/test-db/colls/bot-storage/docs", + "body": { + "id": "test*2fconversations*2fConvo1*2f", + "realId": "test/conversations/Convo1/", + "document": { + "dialogState": { + "dialogStack": [ + { + "id": "waterfallDialog", + "state": { + "options": {}, + "values": { + "instanceId": "fakeInstanceId" + }, + "stepIndex": 1 + } + }, + { + "id": "textPrompt", + "state": { + "options": { + "prompt": { + "type": "message", + "text": "Please type your name.", + "inputHint": "acceptingInput" + } + }, + "state": {} + } + } + ] + } + } + }, + "status": 200, + "response": { + "id": "test*2fconversations*2fConvo1*2f", + "realId": "test/conversations/Convo1/", + "document": { + "dialogState": { + "dialogStack": [ + { + "id": "waterfallDialog", + "state": { + "options": {}, + "values": { + "instanceId": "a7c67aaf-20cd-a70b-8521-08053510b9c9" + }, + "stepIndex": 1 + } + }, + { + "id": "textPrompt", + "state": { + "options": { + "prompt": { + "type": "message", + "text": "Please type your name.", + "inputHint": "acceptingInput" + } + }, + "state": {} + } + } + ] + } + }, + "_rid": "OuRPAMPSwdkHAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkHAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7bd3f6001d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7bd3f6001d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=6;collectionSize=6;", + "lsn", + "2136", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2135", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2700", + "x-ms-cosmos-llsn", + "2136", + "x-ms-cosmos-quorum-acked-llsn", + "2135", + "x-ms-session-token", + "0:-1#2136", + "x-ms-request-charge", + "12.38", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "561e04d8-d60a-4294-a27e-7971d2585030", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/test*2fconversations*2fConvo1*2f", + "body": "", + "status": 200, + "response": { + "id": "test*2fconversations*2fConvo1*2f", + "realId": "test/conversations/Convo1/", + "document": { + "dialogState": { + "dialogStack": [ + { + "id": "waterfallDialog", + "state": { + "options": {}, + "values": { + "instanceId": "a7c67aaf-20cd-a70b-8521-08053510b9c9" + }, + "stepIndex": 1 + } + }, + { + "id": "textPrompt", + "state": { + "options": { + "prompt": { + "type": "message", + "text": "Please type your name.", + "inputHint": "acceptingInput" + } + }, + "state": {} + } + } + ] + } + }, + "_rid": "OuRPAMPSwdkHAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkHAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7bd3f6001d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/test*2fconversations*2fConvo1*2f", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7bd3f6001d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=6;collectionSize=6;", + "lsn", + "2136", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2136", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2136", + "x-ms-transport-request-id", + "2701", + "x-ms-cosmos-llsn", + "2136", + "x-ms-cosmos-quorum-acked-llsn", + "2136", + "x-ms-cosmos-item-llsn", + "2136", + "x-ms-session-token", + "0:-1#2136", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "366ee057-205d-4e4f-81a9-e8d363c3fd7d", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "POST", + "path": "/dbs/test-db/colls/bot-storage/docs", + "body": { + "id": "test*2fconversations*2fConvo1*2f", + "realId": "test/conversations/Convo1/", + "document": { + "dialogState": { + "dialogStack": [ + { + "id": "waterfallDialog", + "state": { + "options": {}, + "values": { + "instanceId": "fakeInstanceId" + }, + "stepIndex": 1 + } + }, + { + "id": "textPrompt", + "state": { + "options": { + "prompt": { + "type": "message", + "text": "Please type your name.", + "inputHint": "acceptingInput" + } + }, + "state": { + "attemptCount": 2 + } + } + } + ] + } + } + }, + "status": 200, + "response": { + "id": "test*2fconversations*2fConvo1*2f", + "realId": "test/conversations/Convo1/", + "document": { + "dialogState": { + "dialogStack": [ + { + "id": "waterfallDialog", + "state": { + "options": {}, + "values": { + "instanceId": "a7c67aaf-20cd-a70b-8521-08053510b9c9" + }, + "stepIndex": 1 + } + }, + { + "id": "textPrompt", + "state": { + "options": { + "prompt": { + "type": "message", + "text": "Please type your name.", + "inputHint": "acceptingInput" + } + }, + "state": { + "attemptCount": 2 + } + } + } + ] + } + }, + "_rid": "OuRPAMPSwdkHAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkHAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7c0617701d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7c0617701d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=6;collectionSize=6;", + "lsn", + "2137", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2136", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2702", + "x-ms-cosmos-llsn", + "2137", + "x-ms-cosmos-quorum-acked-llsn", + "2136", + "x-ms-session-token", + "0:-1#2137", + "x-ms-request-charge", + "10.67", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "f7506c9c-f1d2-4930-9323-41b8e82e24b7", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/test*2fconversations*2fConvo1*2f", + "body": "", + "status": 200, + "response": { + "id": "test*2fconversations*2fConvo1*2f", + "realId": "test/conversations/Convo1/", + "document": { + "dialogState": { + "dialogStack": [ + { + "id": "waterfallDialog", + "state": { + "options": {}, + "values": { + "instanceId": "a7c67aaf-20cd-a70b-8521-08053510b9c9" + }, + "stepIndex": 1 + } + }, + { + "id": "textPrompt", + "state": { + "options": { + "prompt": { + "type": "message", + "text": "Please type your name.", + "inputHint": "acceptingInput" + } + }, + "state": { + "attemptCount": 2 + } + } + } + ] + } + }, + "_rid": "OuRPAMPSwdkHAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkHAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7c0617701d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/test*2fconversations*2fConvo1*2f", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7c0617701d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=6;collectionSize=6;", + "lsn", + "2137", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2137", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2137", + "x-ms-transport-request-id", + "2703", + "x-ms-cosmos-llsn", + "2137", + "x-ms-cosmos-quorum-acked-llsn", + "2137", + "x-ms-cosmos-item-llsn", + "2137", + "x-ms-session-token", + "0:-1#2137", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "7224c659-ec54-4980-86d9-7406733528f2", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "POST", + "path": "/dbs/test-db/colls/bot-storage/docs", + "body": { + "id": "test*2fconversations*2fConvo1*2f", + "realId": "test/conversations/Convo1/", + "document": { + "dialogState": { + "dialogStack": [ + { + "id": "waterfallDialog", + "state": { + "options": {}, + "values": { + "instanceId": "fakeInstanceId" + }, + "stepIndex": 1 + } + }, + { + "id": "textPrompt", + "state": { + "options": { + "prompt": { + "type": "message", + "text": "Please type your name.", + "inputHint": "acceptingInput" + } + }, + "state": { + "attemptCount": 3 + } + } + } + ] + } + } + }, + "status": 200, + "response": { + "id": "test*2fconversations*2fConvo1*2f", + "realId": "test/conversations/Convo1/", + "document": { + "dialogState": { + "dialogStack": [ + { + "id": "waterfallDialog", + "state": { + "options": {}, + "values": { + "instanceId": "a7c67aaf-20cd-a70b-8521-08053510b9c9" + }, + "stepIndex": 1 + } + }, + { + "id": "textPrompt", + "state": { + "options": { + "prompt": { + "type": "message", + "text": "Please type your name.", + "inputHint": "acceptingInput" + } + }, + "state": { + "attemptCount": 3 + } + } + } + ] + } + }, + "_rid": "OuRPAMPSwdkHAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkHAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7c324e201d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7c324e201d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=6;collectionSize=6;", + "lsn", + "2138", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2137", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2704", + "x-ms-cosmos-llsn", + "2138", + "x-ms-cosmos-quorum-acked-llsn", + "2137", + "x-ms-session-token", + "0:-1#2138", + "x-ms-request-charge", + "10.67", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "96e2a1d3-a09b-407f-bcf4-0c85c889aca9", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:23 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/test*2fconversations*2fConvo1*2f", + "body": "", + "status": 200, + "response": { + "id": "test*2fconversations*2fConvo1*2f", + "realId": "test/conversations/Convo1/", + "document": { + "dialogState": { + "dialogStack": [ + { + "id": "waterfallDialog", + "state": { + "options": {}, + "values": { + "instanceId": "a7c67aaf-20cd-a70b-8521-08053510b9c9" + }, + "stepIndex": 1 + } + }, + { + "id": "textPrompt", + "state": { + "options": { + "prompt": { + "type": "message", + "text": "Please type your name.", + "inputHint": "acceptingInput" + } + }, + "state": { + "attemptCount": 3 + } + } + } + ] + } + }, + "_rid": "OuRPAMPSwdkHAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkHAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7c324e201d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/test*2fconversations*2fConvo1*2f", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7c324e201d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=6;collectionSize=6;", + "lsn", + "2138", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2138", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2138", + "x-ms-transport-request-id", + "2705", + "x-ms-cosmos-llsn", + "2138", + "x-ms-cosmos-quorum-acked-llsn", + "2138", + "x-ms-cosmos-item-llsn", + "2138", + "x-ms-session-token", + "0:-1#2138", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "1ba7f578-ee04-4f01-9b9b-8ec27b04d3d3", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:23 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "POST", + "path": "/dbs/test-db/colls/bot-storage/docs", + "body": { + "id": "test*2fconversations*2fConvo1*2f", + "realId": "test/conversations/Convo1/", + "document": { + "dialogState": { + "dialogStack": [ + { + "id": "waterfallDialog", + "state": { + "options": {}, + "values": { + "instanceId": "fakeInstanceId" + }, + "stepIndex": 1 + } + }, + { + "id": "textPrompt", + "state": { + "options": { + "prompt": { + "type": "message", + "text": "Please type your name.", + "inputHint": "acceptingInput" + } + }, + "state": { + "attemptCount": 4 + } + } + } + ] + } + } + }, + "status": 200, + "response": { + "id": "test*2fconversations*2fConvo1*2f", + "realId": "test/conversations/Convo1/", + "document": { + "dialogState": { + "dialogStack": [ + { + "id": "waterfallDialog", + "state": { + "options": {}, + "values": { + "instanceId": "a7c67aaf-20cd-a70b-8521-08053510b9c9" + }, + "stepIndex": 1 + } + }, + { + "id": "textPrompt", + "state": { + "options": { + "prompt": { + "type": "message", + "text": "Please type your name.", + "inputHint": "acceptingInput" + } + }, + "state": { + "attemptCount": 4 + } + } + } + ] + } + }, + "_rid": "OuRPAMPSwdkHAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkHAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7c6316d01d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7c6316d01d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=6;collectionSize=6;", + "lsn", + "2139", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2138", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2706", + "x-ms-cosmos-llsn", + "2139", + "x-ms-cosmos-quorum-acked-llsn", + "2138", + "x-ms-session-token", + "0:-1#2139", + "x-ms-request-charge", + "10.67", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "1aac7e06-0751-4c7f-8f7d-538dc7fcfc5d", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:23 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/test*2fconversations*2fConvo1*2f", + "body": "", + "status": 200, + "response": { + "id": "test*2fconversations*2fConvo1*2f", + "realId": "test/conversations/Convo1/", + "document": { + "dialogState": { + "dialogStack": [ + { + "id": "waterfallDialog", + "state": { + "options": {}, + "values": { + "instanceId": "a7c67aaf-20cd-a70b-8521-08053510b9c9" + }, + "stepIndex": 1 + } + }, + { + "id": "textPrompt", + "state": { + "options": { + "prompt": { + "type": "message", + "text": "Please type your name.", + "inputHint": "acceptingInput" + } + }, + "state": { + "attemptCount": 4 + } + } + } + ] + } + }, + "_rid": "OuRPAMPSwdkHAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkHAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7c6316d01d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/test*2fconversations*2fConvo1*2f", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7c6316d01d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=6;collectionSize=6;", + "lsn", + "2139", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2139", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2139", + "x-ms-transport-request-id", + "2707", + "x-ms-cosmos-llsn", + "2139", + "x-ms-cosmos-quorum-acked-llsn", + "2139", + "x-ms-cosmos-item-llsn", + "2139", + "x-ms-session-token", + "0:-1#2139", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "a8a74d74-4a50-42f0-92dc-4c97cb27b366", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:23 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "POST", + "path": "/dbs/test-db/colls/bot-storage/docs", + "body": { + "id": "test*2fconversations*2fConvo1*2f", + "realId": "test/conversations/Convo1/", + "document": { + "dialogState": { + "dialogStack": [ + { + "id": "waterfallDialog", + "state": { + "options": {}, + "values": { + "instanceId": "fakeInstanceId" + }, + "stepIndex": 2 + } + } + ] + } + } + }, + "status": 200, + "response": { + "id": "test*2fconversations*2fConvo1*2f", + "realId": "test/conversations/Convo1/", + "document": { + "dialogState": { + "dialogStack": [ + { + "id": "waterfallDialog", + "state": { + "options": {}, + "values": { + "instanceId": "a7c67aaf-20cd-a70b-8521-08053510b9c9" + }, + "stepIndex": 2 + } + } + ] + } + }, + "_rid": "OuRPAMPSwdkHAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkHAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7cb11d701d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7cb11d701d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=6;collectionSize=6;", + "lsn", + "2140", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2139", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2708", + "x-ms-cosmos-llsn", + "2140", + "x-ms-cosmos-quorum-acked-llsn", + "2139", + "x-ms-session-token", + "0:-1#2140", + "x-ms-request-charge", + "12.76", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "9218004b-1ed7-40c7-88ea-5fb787a249c5", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:23 GMT" + ] + } +] \ No newline at end of file diff --git a/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_create_an_object.json b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_create_an_object.json new file mode 100644 index 0000000000..9770e58c00 --- /dev/null +++ b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_create_an_object.json @@ -0,0 +1,538 @@ +[ + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/", + "body": "", + "status": 200, + "response": { + "_self": "", + "id": "localhost", + "_rid": "localhost", + "media": "//media/", + "addresses": "//addresses/", + "_dbs": "//dbs/", + "writableLocations": [ + { + "name": "South Central US", + "databaseAccountEndpoint": "https://127.0.0.1:8081/" + } + ], + "readableLocations": [ + { + "name": "South Central US", + "databaseAccountEndpoint": "https://127.0.0.1:8081/" + } + ], + "enableMultipleWriteLocations": false, + "userReplicationPolicy": { + "asyncReplication": false, + "minReplicaSetSize": 1, + "maxReplicasetSize": 4 + }, + "userConsistencyPolicy": { + "defaultConsistencyLevel": "Session" + }, + "systemReplicationPolicy": { + "minReplicaSetSize": 1, + "maxReplicasetSize": 4 + }, + "readPolicy": { + "primaryReadCoefficient": 1, + "secondaryReadCoefficient": 1 + }, + "queryEngineConfiguration": "{\"maxSqlQueryInputLength\":262144,\"maxJoinsPerSqlQuery\":5,\"maxLogicalAndPerSqlQuery\":500,\"maxLogicalOrPerSqlQuery\":500,\"maxUdfRefPerSqlQuery\":10,\"maxInExpressionItemsCount\":16000,\"queryMaxInMemorySortDocumentCount\":500,\"maxQueryRequestTimeoutFraction\":0.9,\"sqlAllowNonFiniteNumbers\":false,\"sqlAllowAggregateFunctions\":true,\"sqlAllowSubQuery\":true,\"sqlAllowScalarSubQuery\":true,\"allowNewKeywords\":true,\"sqlAllowLike\":false,\"sqlAllowGroupByClause\":false,\"maxSpatialQueryCells\":12,\"spatialMaxGeometryPointCount\":256,\"sqlAllowTop\":true,\"enableSpatialIndexing\":true}" + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-max-media-storage-usage-mb", + "10240", + "x-ms-media-storage-usage-mb", + "0", + "x-ms-databaseaccount-consumed-mb", + "0", + "x-ms-databaseaccount-reserved-mb", + "0", + "x-ms-databaseaccount-provisioned-mb", + "0", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage", + "body": "", + "status": 200, + "response": { + "id": "bot-storage", + "indexingPolicy": { + "indexingMode": "consistent", + "automatic": true, + "includedPaths": [ + { + "path": "/*", + "indexes": [] + } + ], + "excludedPaths": [ + { + "path": "/\"_etag\"/?" + } + ] + }, + "partitionKey": { + "paths": [ + "/id" + ], + "kind": "Hash" + }, + "geospatialConfig": { + "type": "Geography" + }, + "_rid": "OuRPAMPSwdk=", + "_ts": 1572041302, + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/", + "_etag": "\"00000000-0000-0000-8b80-b75d4d7b01d5\"", + "_docs": "docs/", + "_sprocs": "sprocs/", + "_triggers": "triggers/", + "_udfs": "udfs/", + "_conflicts": "conflicts/" + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b75d4d7b01d5\"", + "collection-partition-index", + "0", + "collection-service-index", + "0", + "lsn", + "2121", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db", + "x-ms-content-path", + "OuRPAA==", + "x-ms-quorum-acked-lsn", + "2121", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2121", + "x-ms-transport-request-id", + "2666", + "x-ms-cosmos-llsn", + "2121", + "x-ms-cosmos-quorum-acked-llsn", + "2121", + "x-ms-cosmos-item-llsn", + "2121", + "x-ms-session-token", + "0:-1#2121", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "4fb6d0fe-0ccc-41c8-aa71-90b8207458cb", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "POST", + "path": "/dbs/test-db/colls/bot-storage/docs", + "body": { + "id": "createPoco", + "realId": "createPoco", + "document": { + "id": 1 + } + }, + "status": 201, + "response": { + "id": "createPoco", + "realId": "createPoco", + "document": { + "id": 1 + }, + "_rid": "OuRPAMPSwdkBAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkBAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b78ea9fb01d5\"", + "_attachments": "attachments/", + "_ts": 1572041302 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b78ea9fb01d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=0;collectionSize=1;", + "lsn", + "2122", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2121", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2667", + "x-ms-cosmos-llsn", + "2122", + "x-ms-cosmos-quorum-acked-llsn", + "2121", + "x-ms-session-token", + "0:-1#2122", + "x-ms-request-charge", + "6.48", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "7bf3ddcd-9358-416f-bc76-f3f7712e4bc7", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "POST", + "path": "/dbs/test-db/colls/bot-storage/docs", + "body": { + "id": "createPocoStoreItem", + "realId": "createPocoStoreItem", + "document": { + "id": 2 + } + }, + "status": 201, + "response": { + "id": "createPocoStoreItem", + "realId": "createPocoStoreItem", + "document": { + "id": 2 + }, + "_rid": "OuRPAMPSwdkCAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkCAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b78fef8901d5\"", + "_attachments": "attachments/", + "_ts": 1572041302 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b78fef8901d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=1;collectionSize=2;", + "lsn", + "2123", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2122", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2668", + "x-ms-cosmos-llsn", + "2123", + "x-ms-cosmos-quorum-acked-llsn", + "2122", + "x-ms-session-token", + "0:-1#2123", + "x-ms-request-charge", + "6.48", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "d65a0baf-4c8f-486d-b877-3b3f175be315", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/createPoco", + "body": "", + "status": 200, + "response": { + "id": "createPoco", + "realId": "createPoco", + "document": { + "id": 1 + }, + "_rid": "OuRPAMPSwdkBAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkBAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b78ea9fb01d5\"", + "_attachments": "attachments/", + "_ts": 1572041302 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/createPoco", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b78ea9fb01d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=2;collectionSize=2;", + "lsn", + "2123", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2123", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2122", + "x-ms-transport-request-id", + "2669", + "x-ms-cosmos-llsn", + "2123", + "x-ms-cosmos-quorum-acked-llsn", + "2123", + "x-ms-cosmos-item-llsn", + "2122", + "x-ms-session-token", + "0:-1#2123", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "3b7434c5-23f2-4811-b089-bc0aa53ebbce", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/createPocoStoreItem", + "body": "", + "status": 200, + "response": { + "id": "createPocoStoreItem", + "realId": "createPocoStoreItem", + "document": { + "id": 2 + }, + "_rid": "OuRPAMPSwdkCAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkCAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b78fef8901d5\"", + "_attachments": "attachments/", + "_ts": 1572041302 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/createPocoStoreItem", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b78fef8901d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=2;collectionSize=2;", + "lsn", + "2123", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2123", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2123", + "x-ms-transport-request-id", + "2670", + "x-ms-cosmos-llsn", + "2123", + "x-ms-cosmos-quorum-acked-llsn", + "2123", + "x-ms-cosmos-item-llsn", + "2123", + "x-ms-session-token", + "0:-1#2123", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "2111f1e9-2f87-4125-b3ff-f7cebe18ddbc", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + } +] \ No newline at end of file diff --git a/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_delete_an_object.json b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_delete_an_object.json new file mode 100644 index 0000000000..8d261c6a4e --- /dev/null +++ b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_delete_an_object.json @@ -0,0 +1,310 @@ +[ + { + "scope": "https://localhost:8081", + "method": "POST", + "path": "/dbs/test-db/colls/bot-storage/docs", + "body": { + "id": "delete1", + "realId": "delete1", + "document": { + "id": 1, + "count": 1 + } + }, + "status": 201, + "response": { + "id": "delete1", + "realId": "delete1", + "document": { + "id": 1, + "count": 1 + }, + "_rid": "OuRPAMPSwdkGAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkGAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7af83f101d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7af83f101d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=5;collectionSize=6;", + "lsn", + "2133", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2132", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2692", + "x-ms-cosmos-llsn", + "2133", + "x-ms-cosmos-quorum-acked-llsn", + "2132", + "x-ms-session-token", + "0:-1#2133", + "x-ms-request-charge", + "6.86", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "cc64c158-1793-4742-96e7-81da608048ac", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/delete1", + "body": "", + "status": 200, + "response": { + "id": "delete1", + "realId": "delete1", + "document": { + "id": 1, + "count": 1 + }, + "_rid": "OuRPAMPSwdkGAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkGAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7af83f101d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/delete1", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7af83f101d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=6;collectionSize=6;", + "lsn", + "2133", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2133", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2133", + "x-ms-transport-request-id", + "2693", + "x-ms-cosmos-llsn", + "2133", + "x-ms-cosmos-quorum-acked-llsn", + "2133", + "x-ms-cosmos-item-llsn", + "2133", + "x-ms-session-token", + "0:-1#2133", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "82125f55-b900-4477-9696-ff6dadab15b6", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "DELETE", + "path": "/dbs/test-db/colls/bot-storage/docs/delete1", + "body": "", + "status": 204, + "response": "", + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Content-Length", + "0", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/delete1", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=6;collectionSize=5;", + "lsn", + "2134", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2133", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2694", + "x-ms-cosmos-llsn", + "2134", + "x-ms-cosmos-quorum-acked-llsn", + "2133", + "x-ms-session-token", + "0:-1#2134", + "x-ms-request-charge", + "6.86", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "07036d44-f5c8-465b-be35-3d6ee1d0bab2", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/delete1", + "body": "", + "status": 404, + "response": { + "code": "NotFound", + "message": "Message: {\"Errors\":[\"Resource Not Found\"]}\r\nActivityId: fb711a49-6eb0-40d6-a038-92e73203b353, Request URI: /apps/DocDbApp/services/DocDbServer15/partitions/a4cb495b-38c8-11e6-8106-8cdcd42c33be/replicas/1p/, RequestStats: \r\nRequestStartTime: 2019-10-25T22:08:23.1432899Z, RequestEndTime: 2019-10-25T22:08:23.1442883Z, Number of regions attempted: 1\r\nResponseTime: 2019-10-25T22:08:23.1442883Z, StoreResult: StorePhysicalAddress: rntbd://127.0.0.1:10253/apps/DocDbApp/services/DocDbServer15/partitions/a4cb495b-38c8-11e6-8106-8cdcd42c33be/replicas/1p/, LSN: 2134, GlobalCommittedLsn: -1, PartitionKeyRangeId: 0, IsValid: True, StatusCode: 404, SubStatusCode: 0, RequestCharge: 1, ItemLSN: -1, SessionToken: -1#2134, UsingLocalLSN: True, TransportException: null, ResourceType: Document, OperationType: Read\r\n, SDK: Microsoft.Azure.Documents.Common/2.4.0.0" + }, + "rawHeaders": [ + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/delete1", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "lsn", + "2134", + "x-ms-schemaversion", + "1.8", + "x-ms-quorum-acked-lsn", + "2134", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2695", + "x-ms-cosmos-llsn", + "2134", + "x-ms-cosmos-quorum-acked-llsn", + "2134", + "x-ms-session-token", + "0:-1#2134", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "fb711a49-6eb0-40d6-a038-92e73203b353", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + } +] \ No newline at end of file diff --git a/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_handle_crazy_keys.json b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_handle_crazy_keys.json new file mode 100644 index 0000000000..bf3806ff98 --- /dev/null +++ b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_handle_crazy_keys.json @@ -0,0 +1,176 @@ +[ + { + "scope": "https://localhost:8081", + "method": "POST", + "path": "/dbs/test-db/colls/bot-storage/docs", + "body": { + "id": "!@*23$%^&*2a()~*2f*5c><,.*3f';\"`~", + "realId": "!@#$%^&*()~/\\><,.?';\"`~", + "document": { + "id": 1 + } + }, + "status": 201, + "response": { + "id": "!@*23$%^&*2a()~*2f*5c><,.*3f';\"`~", + "realId": "!@#$%^&*()~/\\><,.?';\"`~", + "document": { + "id": 1 + }, + "_rid": "OuRPAMPSwdkDAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkDAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7947bb301d5\"", + "_attachments": "attachments/", + "_ts": 1572041302 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7947bb301d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=2;collectionSize=3;", + "lsn", + "2124", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2123", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2671", + "x-ms-cosmos-llsn", + "2124", + "x-ms-cosmos-quorum-acked-llsn", + "2123", + "x-ms-session-token", + "0:-1#2124", + "x-ms-request-charge", + "6.48", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "64871a99-e921-408a-a7d0-f240d6326694", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/!@*23$%25%5E&*2a()~*2f*5c%3E%3C,.*3f%27;%22%60~", + "body": "", + "status": 200, + "response": { + "id": "!@*23$%^&*2a()~*2f*5c><,.*3f';\"`~", + "realId": "!@#$%^&*()~/\\><,.?';\"`~", + "document": { + "id": 1 + }, + "_rid": "OuRPAMPSwdkDAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkDAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7947bb301d5\"", + "_attachments": "attachments/", + "_ts": 1572041302 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/!@*23$%25%5E&*2a()~*2f*5c%3E%3C,.*3f';%22%60~", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7947bb301d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=3;collectionSize=3;", + "lsn", + "2124", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2124", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2124", + "x-ms-transport-request-id", + "2672", + "x-ms-cosmos-llsn", + "2124", + "x-ms-cosmos-quorum-acked-llsn", + "2124", + "x-ms-cosmos-item-llsn", + "2124", + "x-ms-session-token", + "0:-1#2124", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "6e32485a-5668-48ba-b9e6-827a20ea4d0e", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + } +] \ No newline at end of file diff --git a/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_not_throw_when_deleting_unknown_object.json b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_not_throw_when_deleting_unknown_object.json new file mode 100644 index 0000000000..002c7239c7 --- /dev/null +++ b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_not_throw_when_deleting_unknown_object.json @@ -0,0 +1,63 @@ +[ + { + "scope": "https://localhost:8081", + "method": "DELETE", + "path": "/dbs/test-db/colls/bot-storage/docs/unknown_key", + "body": "", + "status": 404, + "response": { + "code": "NotFound", + "message": "Message: {\"Errors\":[\"Resource Not Found\"]}\r\nActivityId: b84f4a40-b33e-4893-b6ca-447dc5362ed0, Request URI: /apps/DocDbApp/services/DocDbServer15/partitions/a4cb495b-38c8-11e6-8106-8cdcd42c33be/replicas/1p/, RequestStats: \r\nRequestStartTime: 2019-10-25T22:08:23.1572996Z, RequestEndTime: 2019-10-25T22:08:23.1582861Z, Number of regions attempted: 1\r\nResponseTime: 2019-10-25T22:08:23.1582861Z, StoreResult: StorePhysicalAddress: rntbd://127.0.0.1:10253/apps/DocDbApp/services/DocDbServer15/partitions/a4cb495b-38c8-11e6-8106-8cdcd42c33be/replicas/1p/, LSN: 2134, GlobalCommittedLsn: -1, PartitionKeyRangeId: 0, IsValid: True, StatusCode: 404, SubStatusCode: 0, RequestCharge: 1.24, ItemLSN: -1, SessionToken: -1#2134, UsingLocalLSN: False, TransportException: null, ResourceType: Document, OperationType: Delete\r\n, SDK: Microsoft.Azure.Documents.Common/2.4.0.0" + }, + "rawHeaders": [ + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/unknown_key", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "lsn", + "2134", + "x-ms-schemaversion", + "1.8", + "x-ms-quorum-acked-lsn", + "2134", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2696", + "x-ms-cosmos-llsn", + "2134", + "x-ms-cosmos-quorum-acked-llsn", + "2134", + "x-ms-session-token", + "0:-1#2134", + "x-ms-request-charge", + "1.24", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "b84f4a40-b33e-4893-b6ca-447dc5362ed0", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + } +] \ No newline at end of file diff --git a/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_not_throw_when_writing_no_items.json b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_not_throw_when_writing_no_items.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_not_throw_when_writing_no_items.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_return_empty_dictionary_when_reading_empty_keys.json b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_return_empty_dictionary_when_reading_empty_keys.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_return_empty_dictionary_when_reading_empty_keys.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_throw_on_invalid_options.json b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_throw_on_invalid_options.json new file mode 100644 index 0000000000..aebfea0f87 --- /dev/null +++ b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_throw_on_invalid_options.json @@ -0,0 +1,260 @@ +[ + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/", + "body": "", + "status": 200, + "response": { + "_self": "", + "id": "localhost", + "_rid": "localhost", + "media": "//media/", + "addresses": "//addresses/", + "_dbs": "//dbs/", + "writableLocations": [ + { + "name": "South Central US", + "databaseAccountEndpoint": "https://127.0.0.1:8081/" + } + ], + "readableLocations": [ + { + "name": "South Central US", + "databaseAccountEndpoint": "https://127.0.0.1:8081/" + } + ], + "enableMultipleWriteLocations": false, + "userReplicationPolicy": { + "asyncReplication": false, + "minReplicaSetSize": 1, + "maxReplicasetSize": 4 + }, + "userConsistencyPolicy": { + "defaultConsistencyLevel": "Session" + }, + "systemReplicationPolicy": { + "minReplicaSetSize": 1, + "maxReplicasetSize": 4 + }, + "readPolicy": { + "primaryReadCoefficient": 1, + "secondaryReadCoefficient": 1 + }, + "queryEngineConfiguration": "{\"maxSqlQueryInputLength\":262144,\"maxJoinsPerSqlQuery\":5,\"maxLogicalAndPerSqlQuery\":500,\"maxLogicalOrPerSqlQuery\":500,\"maxUdfRefPerSqlQuery\":10,\"maxInExpressionItemsCount\":16000,\"queryMaxInMemorySortDocumentCount\":500,\"maxQueryRequestTimeoutFraction\":0.9,\"sqlAllowNonFiniteNumbers\":false,\"sqlAllowAggregateFunctions\":true,\"sqlAllowSubQuery\":true,\"sqlAllowScalarSubQuery\":true,\"allowNewKeywords\":true,\"sqlAllowLike\":false,\"sqlAllowGroupByClause\":false,\"maxSpatialQueryCells\":12,\"spatialMaxGeometryPointCount\":256,\"sqlAllowTop\":true,\"enableSpatialIndexing\":true}" + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-max-media-storage-usage-mb", + "10240", + "x-ms-media-storage-usage-mb", + "0", + "x-ms-databaseaccount-consumed-mb", + "0", + "x-ms-databaseaccount-reserved-mb", + "0", + "x-ms-databaseaccount-provisioned-mb", + "0", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT", + "Connection", + "close" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage", + "body": "", + "status": 404, + "response": { + "code": "NotFound", + "message": "Message: {\"Errors\":[\"Resource Not Found\"]}\r\nActivityId: 9fd78fd9-86a0-4337-95d5-c8d93dae9cb8, Request URI: /apps/DocDbApp/services/DocDbMaster0/partitions/780e44f4-38c8-11e6-8106-8cdcd42c33be/replicas/1p/, RequestStats: \r\nRequestStartTime: 2019-10-25T22:08:22.5122878Z, RequestEndTime: 2019-10-25T22:08:22.5142857Z, Number of regions attempted: 1\r\nResponseTime: 2019-10-25T22:08:22.5142857Z, StoreResult: StorePhysicalAddress: rntbd://127.0.0.1:10251/apps/DocDbApp/services/DocDbMaster0/partitions/780e44f4-38c8-11e6-8106-8cdcd42c33be/replicas/1p/, LSN: 1460, GlobalCommittedLsn: -1, PartitionKeyRangeId: , IsValid: True, StatusCode: 404, SubStatusCode: 0, RequestCharge: 1, ItemLSN: -1, SessionToken: -1#1460, UsingLocalLSN: False, TransportException: null, ResourceType: Collection, OperationType: Read\r\nResponseTime: 2019-10-25T22:08:22.5142857Z, StoreResult: StorePhysicalAddress: rntbd://127.0.0.1:10251/apps/DocDbApp/services/DocDbMaster0/partitions/780e44f4-38c8-11e6-8106-8cdcd42c33be/replicas/1p/, LSN: 1460, GlobalCommittedLsn: -1, PartitionKeyRangeId: , IsValid: True, StatusCode: 404, SubStatusCode: 0, RequestCharge: 1, ItemLSN: -1, SessionToken: -1#1460, UsingLocalLSN: False, TransportException: null, ResourceType: Collection, OperationType: Read\r\n, SDK: Microsoft.Azure.Documents.Common/2.4.0.0" + }, + "rawHeaders": [ + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:26.197 GMT", + "lsn", + "1460", + "x-ms-schemaversion", + "1.8", + "x-ms-quorum-acked-lsn", + "1460", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "3892", + "x-ms-cosmos-llsn", + "1460", + "x-ms-cosmos-quorum-acked-llsn", + "1460", + "x-ms-session-token", + "0:-1#1460", + "x-ms-request-charge", + "2", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "9fd78fd9-86a0-4337-95d5-c8d93dae9cb8", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT", + "Connection", + "close" + ] + }, + { + "scope": "https://localhost:8081", + "method": "POST", + "path": "/dbs/test-db/colls", + "body": { + "id": "bot-storage", + "partitionKey": { + "paths": [ + "/id" + ] + } + }, + "status": 201, + "response": { + "id": "bot-storage", + "indexingPolicy": { + "indexingMode": "consistent", + "automatic": true, + "includedPaths": [ + { + "path": "/*", + "indexes": [] + } + ], + "excludedPaths": [ + { + "path": "/\"_etag\"/?" + } + ] + }, + "partitionKey": { + "paths": [ + "/id" + ], + "kind": "Hash" + }, + "geospatialConfig": { + "type": "Geography" + }, + "_rid": "OuRPAMPSwdk=", + "_ts": 1572041302, + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/", + "_etag": "\"00000000-0000-0000-8b80-b75d4d7b01d5\"", + "_docs": "docs/", + "_sprocs": "sprocs/", + "_triggers": "triggers/", + "_udfs": "udfs/", + "_conflicts": "conflicts/" + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b75d4d7b01d5\"", + "collection-partition-index", + "0", + "collection-service-index", + "0", + "lsn", + "2121", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db", + "x-ms-quorum-acked-lsn", + "2121", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2121", + "x-ms-transport-request-id", + "2665", + "x-ms-cosmos-llsn", + "2121", + "x-ms-cosmos-quorum-acked-llsn", + "2121", + "x-ms-cosmos-item-llsn", + "2121", + "x-ms-session-token", + "0:-1#2121", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "3b6bca8c-eed8-47ee-8747-0f3fafbdfa1f", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT", + "Connection", + "close" + ] + } +] \ No newline at end of file diff --git a/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_throw_when_reading_null_keys.json b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_throw_when_reading_null_keys.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_throw_when_reading_null_keys.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_throw_when_writing_null_keys.json b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_throw_when_writing_null_keys.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_throw_when_writing_null_keys.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_update_an_object.json b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_update_an_object.json new file mode 100644 index 0000000000..8aece41c73 --- /dev/null +++ b/libraries/botbuilder-azure/tests/TestData/CosmosDbPartitionedStorage/should_update_an_object.json @@ -0,0 +1,1682 @@ +[ + { + "scope": "https://localhost:8081", + "method": "POST", + "path": "/dbs/test-db/colls/bot-storage/docs", + "body": { + "id": "pocoItem", + "realId": "pocoItem", + "document": { + "id": 1, + "count": 1 + } + }, + "status": 201, + "response": { + "id": "pocoItem", + "realId": "pocoItem", + "document": { + "id": 1, + "count": 1 + }, + "_rid": "OuRPAMPSwdkEAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkEAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b79ab25601d5\"", + "_attachments": "attachments/", + "_ts": 1572041302 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b79ab25601d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=3;collectionSize=4;", + "lsn", + "2125", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2124", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2673", + "x-ms-cosmos-llsn", + "2125", + "x-ms-cosmos-quorum-acked-llsn", + "2124", + "x-ms-session-token", + "0:-1#2125", + "x-ms-request-charge", + "6.86", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "010182fe-861a-4a13-afc5-e121e7f50540", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "POST", + "path": "/dbs/test-db/colls/bot-storage/docs", + "body": { + "id": "pocoStoreItem", + "realId": "pocoStoreItem", + "document": { + "id": 1, + "count": 1 + } + }, + "status": 201, + "response": { + "id": "pocoStoreItem", + "realId": "pocoStoreItem", + "document": { + "id": 1, + "count": 1 + }, + "_rid": "OuRPAMPSwdkFAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkFAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b79bfa4a01d5\"", + "_attachments": "attachments/", + "_ts": 1572041302 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b79bfa4a01d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=4;collectionSize=5;", + "lsn", + "2126", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2125", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2674", + "x-ms-cosmos-llsn", + "2126", + "x-ms-cosmos-quorum-acked-llsn", + "2125", + "x-ms-session-token", + "0:-1#2126", + "x-ms-request-charge", + "6.86", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "63d1323d-ee13-4213-b808-853ed54f27a0", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/pocoItem", + "body": "", + "status": 200, + "response": { + "id": "pocoItem", + "realId": "pocoItem", + "document": { + "id": 1, + "count": 1 + }, + "_rid": "OuRPAMPSwdkEAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkEAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b79ab25601d5\"", + "_attachments": "attachments/", + "_ts": 1572041302 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/pocoItem", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b79ab25601d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=5;collectionSize=5;", + "lsn", + "2126", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2126", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2125", + "x-ms-transport-request-id", + "2675", + "x-ms-cosmos-llsn", + "2126", + "x-ms-cosmos-quorum-acked-llsn", + "2126", + "x-ms-cosmos-item-llsn", + "2125", + "x-ms-session-token", + "0:-1#2126", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "67f1ae18-fcf8-4533-8d4a-b8411f92b62a", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/pocoStoreItem", + "body": "", + "status": 200, + "response": { + "id": "pocoStoreItem", + "realId": "pocoStoreItem", + "document": { + "id": 1, + "count": 1 + }, + "_rid": "OuRPAMPSwdkFAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkFAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b79bfa4a01d5\"", + "_attachments": "attachments/", + "_ts": 1572041302 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/pocoStoreItem", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b79bfa4a01d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=5;collectionSize=5;", + "lsn", + "2126", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2126", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2126", + "x-ms-transport-request-id", + "2676", + "x-ms-cosmos-llsn", + "2126", + "x-ms-cosmos-quorum-acked-llsn", + "2126", + "x-ms-cosmos-item-llsn", + "2126", + "x-ms-session-token", + "0:-1#2126", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "bda1286c-be8c-49c7-9939-e92c72ea42e8", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "POST", + "path": "/dbs/test-db/colls/bot-storage/docs", + "body": { + "id": "pocoItem", + "realId": "pocoItem", + "document": { + "id": 1, + "count": 2 + } + }, + "status": 200, + "response": { + "id": "pocoItem", + "realId": "pocoItem", + "document": { + "id": 1, + "count": 2 + }, + "_rid": "OuRPAMPSwdkEAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkEAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b79f6ac101d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b79f6ac101d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=5;collectionSize=5;", + "lsn", + "2127", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2126", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2677", + "x-ms-cosmos-llsn", + "2127", + "x-ms-cosmos-quorum-acked-llsn", + "2126", + "x-ms-session-token", + "0:-1#2127", + "x-ms-request-charge", + "10.67", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "3e3453a2-b01f-4321-8458-e9c470aef45b", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "POST", + "path": "/dbs/test-db/colls/bot-storage/docs", + "body": { + "id": "pocoStoreItem", + "realId": "pocoStoreItem", + "document": { + "id": 1, + "count": 2 + } + }, + "status": 200, + "response": { + "id": "pocoStoreItem", + "realId": "pocoStoreItem", + "document": { + "id": 1, + "count": 2 + }, + "_rid": "OuRPAMPSwdkFAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkFAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b79fd55e01d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b79fd55e01d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=5;collectionSize=5;", + "lsn", + "2128", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2127", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2678", + "x-ms-cosmos-llsn", + "2128", + "x-ms-cosmos-quorum-acked-llsn", + "2127", + "x-ms-session-token", + "0:-1#2128", + "x-ms-request-charge", + "10.67", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "000ee02a-a0a9-4578-9ddf-333b3b4fc00e", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/pocoItem", + "body": "", + "status": 200, + "response": { + "id": "pocoItem", + "realId": "pocoItem", + "document": { + "id": 1, + "count": 2 + }, + "_rid": "OuRPAMPSwdkEAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkEAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b79f6ac101d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/pocoItem", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b79f6ac101d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=5;collectionSize=5;", + "lsn", + "2128", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2128", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2127", + "x-ms-transport-request-id", + "2679", + "x-ms-cosmos-llsn", + "2128", + "x-ms-cosmos-quorum-acked-llsn", + "2128", + "x-ms-cosmos-item-llsn", + "2127", + "x-ms-session-token", + "0:-1#2128", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "7db758e0-e68b-4003-be07-aae517f2ad88", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/pocoStoreItem", + "body": "", + "status": 200, + "response": { + "id": "pocoStoreItem", + "realId": "pocoStoreItem", + "document": { + "id": 1, + "count": 2 + }, + "_rid": "OuRPAMPSwdkFAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkFAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b79fd55e01d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/pocoStoreItem", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b79fd55e01d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=5;collectionSize=5;", + "lsn", + "2128", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2128", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2128", + "x-ms-transport-request-id", + "2680", + "x-ms-cosmos-llsn", + "2128", + "x-ms-cosmos-quorum-acked-llsn", + "2128", + "x-ms-cosmos-item-llsn", + "2128", + "x-ms-session-token", + "0:-1#2128", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "6cb6dc29-2ed6-4358-b681-b071fca5d9c0", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "POST", + "path": "/dbs/test-db/colls/bot-storage/docs", + "body": { + "id": "pocoItem", + "realId": "pocoItem", + "document": { + "id": 1, + "count": 123 + } + }, + "status": 200, + "response": { + "id": "pocoItem", + "realId": "pocoItem", + "document": { + "id": 1, + "count": 123 + }, + "_rid": "OuRPAMPSwdkEAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkEAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7a2e04e01d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7a2e04e01d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=5;collectionSize=5;", + "lsn", + "2129", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2128", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2681", + "x-ms-cosmos-llsn", + "2129", + "x-ms-cosmos-quorum-acked-llsn", + "2128", + "x-ms-session-token", + "0:-1#2129", + "x-ms-request-charge", + "10.67", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "f4f57161-6a2a-4fb3-98bf-e785ad130511", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/pocoItem", + "body": "", + "status": 200, + "response": { + "id": "pocoItem", + "realId": "pocoItem", + "document": { + "id": 1, + "count": 123 + }, + "_rid": "OuRPAMPSwdkEAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkEAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7a2e04e01d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/pocoItem", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7a2e04e01d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=5;collectionSize=5;", + "lsn", + "2129", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2129", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2129", + "x-ms-transport-request-id", + "2682", + "x-ms-cosmos-llsn", + "2129", + "x-ms-cosmos-quorum-acked-llsn", + "2129", + "x-ms-cosmos-item-llsn", + "2129", + "x-ms-session-token", + "0:-1#2129", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "724175f5-05c8-41ea-9f9b-52146e06fe3d", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/pocoStoreItem", + "body": "", + "status": 200, + "response": { + "id": "pocoStoreItem", + "realId": "pocoStoreItem", + "document": { + "id": 1, + "count": 2 + }, + "_rid": "OuRPAMPSwdkFAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkFAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b79fd55e01d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/pocoStoreItem", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b79fd55e01d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=5;collectionSize=5;", + "lsn", + "2129", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2129", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2128", + "x-ms-transport-request-id", + "2683", + "x-ms-cosmos-llsn", + "2129", + "x-ms-cosmos-quorum-acked-llsn", + "2129", + "x-ms-cosmos-item-llsn", + "2128", + "x-ms-session-token", + "0:-1#2129", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "91241c4d-9e34-4980-9efb-dc0029218fa7", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "POST", + "path": "/dbs/test-db/colls/bot-storage/docs", + "body": { + "id": "pocoItem", + "realId": "pocoItem", + "document": { + "id": 1, + "count": 100 + } + }, + "status": 200, + "response": { + "id": "pocoItem", + "realId": "pocoItem", + "document": { + "id": 1, + "count": 100 + }, + "_rid": "OuRPAMPSwdkEAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkEAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7a69b6501d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7a69b6501d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=5;collectionSize=5;", + "lsn", + "2130", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2129", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2684", + "x-ms-cosmos-llsn", + "2130", + "x-ms-cosmos-quorum-acked-llsn", + "2129", + "x-ms-session-token", + "0:-1#2130", + "x-ms-request-charge", + "10.67", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "51c21c60-cdd9-45f6-87d1-ac4426857b0b", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "POST", + "path": "/dbs/test-db/colls/bot-storage/docs", + "body": { + "id": "pocoStoreItem", + "realId": "pocoStoreItem", + "document": { + "id": 1, + "count": 100 + } + }, + "status": 200, + "response": { + "id": "pocoStoreItem", + "realId": "pocoStoreItem", + "document": { + "id": 1, + "count": 100 + }, + "_rid": "OuRPAMPSwdkFAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkFAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7a70c9d01d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7a70c9d01d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=5;collectionSize=5;", + "lsn", + "2131", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2130", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2685", + "x-ms-cosmos-llsn", + "2131", + "x-ms-cosmos-quorum-acked-llsn", + "2130", + "x-ms-session-token", + "0:-1#2131", + "x-ms-request-charge", + "10.67", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "106098e6-ec7b-4cdd-94fd-f9cc253e36db", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/pocoItem", + "body": "", + "status": 200, + "response": { + "id": "pocoItem", + "realId": "pocoItem", + "document": { + "id": 1, + "count": 100 + }, + "_rid": "OuRPAMPSwdkEAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkEAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7a69b6501d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/pocoItem", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7a69b6501d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=5;collectionSize=5;", + "lsn", + "2131", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2131", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2130", + "x-ms-transport-request-id", + "2686", + "x-ms-cosmos-llsn", + "2131", + "x-ms-cosmos-quorum-acked-llsn", + "2131", + "x-ms-cosmos-item-llsn", + "2130", + "x-ms-session-token", + "0:-1#2131", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "24810482-9628-4b31-b8f8-0a1098a7d15d", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/pocoStoreItem", + "body": "", + "status": 200, + "response": { + "id": "pocoStoreItem", + "realId": "pocoStoreItem", + "document": { + "id": 1, + "count": 100 + }, + "_rid": "OuRPAMPSwdkFAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkFAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7a70c9d01d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/pocoStoreItem", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7a70c9d01d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=5;collectionSize=5;", + "lsn", + "2131", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2131", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2131", + "x-ms-transport-request-id", + "2687", + "x-ms-cosmos-llsn", + "2131", + "x-ms-cosmos-quorum-acked-llsn", + "2131", + "x-ms-cosmos-item-llsn", + "2131", + "x-ms-session-token", + "0:-1#2131", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "68416d89-3b41-4f3f-abb4-4508ba0668c9", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/pocoStoreItem", + "body": "", + "status": 200, + "response": { + "id": "pocoStoreItem", + "realId": "pocoStoreItem", + "document": { + "id": 1, + "count": 100 + }, + "_rid": "OuRPAMPSwdkFAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkFAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7a70c9d01d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/pocoStoreItem", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7a70c9d01d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=5;collectionSize=5;", + "lsn", + "2131", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2131", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2131", + "x-ms-transport-request-id", + "2688", + "x-ms-cosmos-llsn", + "2131", + "x-ms-cosmos-quorum-acked-llsn", + "2131", + "x-ms-cosmos-item-llsn", + "2131", + "x-ms-session-token", + "0:-1#2131", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "331aab98-13d6-4fae-8344-cafc1013bfd4", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "POST", + "path": "/dbs/test-db/colls/bot-storage/docs", + "body": { + "id": "pocoStoreItem", + "realId": "pocoStoreItem", + "document": { + "id": 1, + "count": 100 + } + }, + "status": 200, + "response": { + "id": "pocoStoreItem", + "realId": "pocoStoreItem", + "document": { + "id": 1, + "count": 100 + }, + "_rid": "OuRPAMPSwdkFAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkFAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7ab1c7b01d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7ab1c7b01d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=5;collectionSize=5;", + "lsn", + "2132", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2131", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-transport-request-id", + "2689", + "x-ms-cosmos-llsn", + "2132", + "x-ms-cosmos-quorum-acked-llsn", + "2131", + "x-ms-session-token", + "0:-1#2132", + "x-ms-request-charge", + "10.29", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "ebb897ca-a6cd-40d0-907c-adf45e43d660", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/pocoItem", + "body": "", + "status": 200, + "response": { + "id": "pocoItem", + "realId": "pocoItem", + "document": { + "id": 1, + "count": 100 + }, + "_rid": "OuRPAMPSwdkEAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkEAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7a69b6501d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/pocoItem", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7a69b6501d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=5;collectionSize=5;", + "lsn", + "2132", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2132", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2130", + "x-ms-transport-request-id", + "2691", + "x-ms-cosmos-llsn", + "2132", + "x-ms-cosmos-quorum-acked-llsn", + "2132", + "x-ms-cosmos-item-llsn", + "2130", + "x-ms-session-token", + "0:-1#2132", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "6f7cfe8d-23e6-4782-b65e-397bd1709718", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + }, + { + "scope": "https://localhost:8081", + "method": "GET", + "path": "/dbs/test-db/colls/bot-storage/docs/pocoStoreItem", + "body": "", + "status": 200, + "response": { + "id": "pocoStoreItem", + "realId": "pocoStoreItem", + "document": { + "id": 1, + "count": 100 + }, + "_rid": "OuRPAMPSwdkFAAAAAAAAAA==", + "_self": "dbs/OuRPAA==/colls/OuRPAMPSwdk=/docs/OuRPAMPSwdkFAAAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-8b80-b7ab1c7b01d5\"", + "_attachments": "attachments/", + "_ts": 1572041303 + }, + "rawHeaders": [ + "Cache-Control", + "no-store, no-cache", + "Pragma", + "no-cache", + "Transfer-Encoding", + "chunked", + "Content-Type", + "application/json", + "Content-Location", + "https://localhost:8081/dbs/test-db/colls/bot-storage/docs/pocoStoreItem", + "Server", + "Microsoft-HTTPAPI/2.0", + "Access-Control-Allow-Origin", + "", + "Access-Control-Allow-Credentials", + "true", + "x-ms-last-state-change-utc", + "Fri, 25 Oct 2019 17:40:39.327 GMT", + "etag", + "\"00000000-0000-0000-8b80-b7ab1c7b01d5\"", + "x-ms-resource-quota", + "documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;", + "x-ms-resource-usage", + "documentSize=0;documentsSize=0;documentsCount=5;collectionSize=5;", + "lsn", + "2132", + "x-ms-schemaversion", + "1.8", + "x-ms-alt-content-path", + "dbs/test-db/colls/bot-storage", + "x-ms-content-path", + "OuRPAMPSwdk=", + "x-ms-quorum-acked-lsn", + "2132", + "x-ms-current-write-quorum", + "1", + "x-ms-current-replica-set-size", + "1", + "x-ms-xp-role", + "0", + "x-ms-global-Committed-lsn", + "-1", + "x-ms-number-of-read-regions", + "0", + "x-ms-item-lsn", + "2132", + "x-ms-transport-request-id", + "2690", + "x-ms-cosmos-llsn", + "2132", + "x-ms-cosmos-quorum-acked-llsn", + "2132", + "x-ms-cosmos-item-llsn", + "2132", + "x-ms-session-token", + "0:-1#2132", + "x-ms-request-charge", + "1", + "x-ms-serviceversion", + "version=2.4.0.0", + "x-ms-activity-id", + "9e550473-a86a-4eea-8803-6a26a9f72829", + "x-ms-gatewayversion", + "version=2.4.0.0", + "Date", + "Fri, 25 Oct 2019 22:08:22 GMT" + ] + } +] \ No newline at end of file diff --git a/libraries/botbuilder-azure/tests/TestData/expectedCalls.json b/libraries/botbuilder-azure/tests/TestData/expectedCalls.json index aded2c5198..b86d519904 100644 --- a/libraries/botbuilder-azure/tests/TestData/expectedCalls.json +++ b/libraries/botbuilder-azure/tests/TestData/expectedCalls.json @@ -72,7 +72,7 @@ "createBlockBlobFromTextAsync": [ "test-transcript", "test/logActivityTest/8d66eb2e84b8000-1.json", - "{\"type\":\"message\",\"timestamp\":{},\"id\":1,\"text\":\"testMessage\",\"channelId\":\"test\",\"from\":{\"id\":\"User1\"},\"conversation\":{\"id\":\"logActivityTest\"},\"recipient\":{\"id\":\"Bot1\",\"name\":\"2\"},\"serviceUrl\":\"http://foo.com/api/messages\"}", + "{\"type\":\"message\",\"timestamp\":\"2018-12-31T00:00:00.000Z\",\"id\":1,\"text\":\"testMessage\",\"channelId\":\"test\",\"from\":{\"id\":\"User1\"},\"conversation\":{\"id\":\"logActivityTest\"},\"recipient\":{\"id\":\"Bot1\",\"name\":\"2\"},\"serviceUrl\":\"http://foo.com/api/messages\"}", null ] }, @@ -83,7 +83,7 @@ { "fromid": "User1", "recipientid": "Bot1", - "timestamp": "1546214400000" + "timestamp": "2018-12-31T00:00:00.000Z" } ] }, diff --git a/libraries/botbuilder-azure/tests/azureBlobTranscriptStore.test.js b/libraries/botbuilder-azure/tests/azureBlobTranscriptStore.test.js index d099c58ca8..3abc7f7a5f 100644 --- a/libraries/botbuilder-azure/tests/azureBlobTranscriptStore.test.js +++ b/libraries/botbuilder-azure/tests/azureBlobTranscriptStore.test.js @@ -196,9 +196,8 @@ describe('The AzureBlobTranscriptStore', () => { }); it('should log an activity', async () => { - const date = {getTime: () => 1546214400000}; + const date = new Date(1546214400000); const activity = createActivity('logActivityTest', date); - await storage.logActivity(activity); const { mockFunctionCalls } = mockService; const { logActivity } = expectedCalls; diff --git a/libraries/botbuilder-azure/tests/cosmosDbPartitionedStorage.test.js b/libraries/botbuilder-azure/tests/cosmosDbPartitionedStorage.test.js new file mode 100644 index 0000000000..12b4bea661 --- /dev/null +++ b/libraries/botbuilder-azure/tests/cosmosDbPartitionedStorage.test.js @@ -0,0 +1,425 @@ +const assert = require('assert'); +const { CosmosDbPartitionedStorage } = require('../lib'); +const { AutoSaveStateMiddleware, ConversationState, MessageFactory, TestAdapter } = require('../../botbuilder-core'); +const { Dialog, DialogSet, TextPrompt, WaterfallDialog } = require('../../botbuilder-dialogs'); +const { CosmosClient } = require('@azure/cosmos'); +const { MockMode, usingNock } = require('./mockHelper'); +const nock = require('nock'); +const fs = require('fs'); +const https = require('https'); + +const mode = process.env.MOCK_MODE ? process.env.MOCK_MODE : MockMode.lockdown; + +// Endpoint and Authkey for the CosmosDB Emulator running locally +const cosmosDbEndpoint = 'https://localhost:8081'; +const authKey = 'C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=='; +const databaseId = 'test-db'; +const containerId = 'bot-storage'; + +const getSettings = () => { + return { + cosmosDbEndpoint, + authKey, + databaseId, + containerId + }; +}; + +const storage = new CosmosDbPartitionedStorage(getSettings()); + +const noEmulatorMessage = 'This test requires CosmosDB Emulator! go to https://aka.ms/documentdb-emulator-docs to download and install.'; +const emulatorPath = `C:/Program Files/Azure Cosmos DB Emulator/CosmosDB.Emulator.exe`; + +// Disable certificate checking when running tests locally +if (cosmosDbEndpoint.includes('localhost:8081')) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + console.warn('WARNING: Disabling SSL Verification because we detected the emulator was being used'); +} + +const hasEmulator = fs.existsSync(emulatorPath); + +const checkEmulator = () => { + if (!hasEmulator) { + console.warn(noEmulatorMessage); + } + return true; +}; + +// item to test the read and delete operations with partitionkey +let changes = {}; +changes['001'] = { + Location: 'ARG', + MessageList: ['Hi', 'how are u'] +}; + +// called before each test +const reset = async () => { + nock.cleanAll(); + nock.enableNetConnect(); + if (mode !== MockMode.lockdown) { + let settings = getSettings(); + + let client = new CosmosClient({ endpoint: settings.cosmosDbEndpoint, key: settings.authKey}); + try { + await client.database(settings.databaseId).delete(); + } catch (err) { } + await client.databases.create({ id: databaseId }); + } +}; + +const options = { + scope: getSettings().cosmosDbEndpoint +}; + +const testStorage = () => { + + it('should throw on invalid options', async function() { + const { nockDone } = await usingNock(this.test, mode, options); + + // No options. Should throw. + assert.throws(() => new CosmosDbPartitionedStorage(null), ReferenceError('CosmosDbPartitionedStorageOptions is required.')); + + // No endpoint. Should throw. + const noEndpoint = getSettings(); + noEndpoint.cosmosDbEndpoint = null; + assert.throws(() => new CosmosDbPartitionedStorage(noEndpoint), ReferenceError('cosmosDbEndpoint for CosmosDB is required.')); + + // No authKey. Should throw. + const noAuthKey = getSettings(); + noAuthKey.authKey = null; + assert.throws(() => new CosmosDbPartitionedStorage(noAuthKey), ReferenceError('authKey for CosmosDB is required.')); + + // No databaseId. Should throw. + const noDatabaseId = getSettings(); + noDatabaseId.databaseId = null; + assert.throws(() => new CosmosDbPartitionedStorage(noDatabaseId), ReferenceError('databaseId is for CosmosDB required.')); + + // No containerId. Should throw. + const noContainerId = getSettings(); + noContainerId.containerId = null; + assert.throws(() => new CosmosDbPartitionedStorage(noContainerId), ReferenceError('containerId for CosmosDB is required.')); + + // Passes CosmosClientOptions + const settingsWithClientOptions = getSettings(); + settingsWithClientOptions.cosmosClientOptions = { + agent: new https.Agent({ rejectUnauthorized: false }), + connectionPolicy: { requestTimeout: 999 }, + userAgentSuffix: 'test', + }; + + const client = new CosmosDbPartitionedStorage(settingsWithClientOptions); + await client.initialize(); // Force client to go through initialization + + assert.strictEqual(client.client.clientContext.connectionPolicy.requestTimeout, 999); + assert.strictEqual(client.client.clientContext.cosmosClientOptions.userAgentSuffix, 'test'); + + return nockDone(); + }); + + // NOTE: THESE TESTS REQUIRE THAT THE COSMOS DB EMULATOR IS INSTALLED AND STARTED !!!!!!!!!!!!!!!!! + it('should create an object', async function() { + if(checkEmulator()) { + const { nockDone } = await usingNock(this.test, mode, options); + + const storeItems = { + createPoco: { id: 1 }, + createPocoStoreItem: { id: 2 }, + }; + + await storage.write(storeItems); + + const readStoreItems = await storage.read(Object.keys(storeItems)); + + assert.strictEqual(storeItems.createPoco.id, readStoreItems.createPoco.id); + assert.strictEqual(storeItems.createPocoStoreItem.id, readStoreItems.createPocoStoreItem.id); + assert.notStrictEqual(readStoreItems.createPoco.eTag, null); + assert.notStrictEqual(readStoreItems.createPocoStoreItem.eTag, null); + + return nockDone(); + } + }); + + // NOTE: THESE TESTS REQUIRE THAT THE COSMOS DB EMULATOR IS INSTALLED AND STARTED !!!!!!!!!!!!!!!!! + it('should handle crazy keys', async function() { + if(checkEmulator()) { + const { nockDone } = await usingNock(this.test, mode, options); + + const key = `!@#$%^&*()~/\\><,.?';\"\`~`; + const storeItem = { id: 1 }; + const storeItems = { [key]: storeItem }; + + await storage.write(storeItems); + + const readStoreItems = await storage.read(Object.keys(storeItems)); + + assert.notStrictEqual(readStoreItems[key], null); + assert.strictEqual(readStoreItems[key].id, 1); + + return nockDone(); + } + }); + + // NOTE: THESE TESTS REQUIRE THAT THE COSMOS DB EMULATOR IS INSTALLED AND STARTED !!!!!!!!!!!!!!!!! + it('should return empty dictionary when reading empty keys', async function() { + if(checkEmulator()) { + const { nockDone } = await usingNock(this.test, mode, options); + + const state = await storage.read([]); + assert.deepStrictEqual(state, {}); + + return nockDone(); + } + }); + + // NOTE: THESE TESTS REQUIRE THAT THE COSMOS DB EMULATOR IS INSTALLED AND STARTED !!!!!!!!!!!!!!!!! + it('should throw when reading null keys', async function() { + if(checkEmulator()) { + const { nockDone } = await usingNock(this.test, mode, options); + + await assert.rejects(async () => await storage.read(null), ReferenceError(`Keys are required when reading.`)); + + return nockDone(); + } + }); + + // NOTE: THESE TESTS REQUIRE THAT THE COSMOS DB EMULATOR IS INSTALLED AND STARTED !!!!!!!!!!!!!!!!! + it('should throw when writing null keys', async function() { + if(checkEmulator()) { + const { nockDone } = await usingNock(this.test, mode, options); + + await assert.rejects(async () => await storage.write(null), ReferenceError(`Changes are required when writing.`)); + + return nockDone(); + } + }); + + // NOTE: THESE TESTS REQUIRE THAT THE COSMOS DB EMULATOR IS INSTALLED AND STARTED !!!!!!!!!!!!!!!!! + it('should not throw when writing no items', async function() { + if(checkEmulator()) { + const { nockDone } = await usingNock(this.test, mode, options); + + await assert.doesNotReject(async () => await storage.write([])); + + return nockDone(); + } + }); + + // NOTE: THESE TESTS REQUIRE THAT THE COSMOS DB EMULATOR IS INSTALLED AND STARTED !!!!!!!!!!!!!!!!! + it('should update an object', async function() { + if(checkEmulator()) { + const { nockDone } = await usingNock(this.test, mode, options); + + const originalStoreItems = { + pocoItem: { id: 1, count: 1 }, + pocoStoreItem: { id: 1, count: 1 }, + }; + + // first write should work + await storage.write(originalStoreItems); + + const loadedStoreItems = await storage.read(['pocoItem', 'pocoStoreItem']); + + const updatePocoItem = loadedStoreItems.pocoItem; + delete updatePocoItem.eTag; // pocoItems don't have eTag + const updatePocoStoreItem = loadedStoreItems.pocoStoreItem; + assert.notStrictEqual(updatePocoStoreItem.eTag, null); + + // 2nd write should work + updatePocoItem.count++; + updatePocoStoreItem.count++; + + await storage.write(loadedStoreItems); + + const reloadedStoreItems = await storage.read(Object.keys(loadedStoreItems)); + + const reloadedUpdatePocoItem = reloadedStoreItems.pocoItem; + const reloadedUpdatePocoStoreItem = reloadedStoreItems.pocoStoreItem; + + assert.notStrictEqual(reloadedUpdatePocoItem.eTag, null); + assert.notStrictEqual(updatePocoStoreItem.eTag, reloadedUpdatePocoStoreItem.eTag); + assert.strictEqual(reloadedUpdatePocoItem.count, 2); + assert.strictEqual(reloadedUpdatePocoStoreItem.count, 2); + + // Write with old eTag should succeed for non-storeitem + try { + updatePocoItem.count = 123; + + await storage.write({ pocoItem: updatePocoItem }); + } catch(err) { + assert.fail('Should not throw exception on write with pocoItem'); + } + + // Write with old eTag should FAIL for storeItem + try { + updatePocoStoreItem.count = 123; + + await storage.write({ pocoStoreItem, updatePocoStoreItem }); + assert.fail('Should have thrown exception on write with store item because of old eTag'); + } catch(err) { } + + const reloadedStoreItems2 = await storage.read(['pocoItem', 'pocoStoreItem']); + + const reloadedPocoItem2 = reloadedStoreItems2.pocoItem; + delete reloadedPocoItem2.eTag; + const reloadedPocoStoreItem2 = reloadedStoreItems2.pocoStoreItem; + + assert.strictEqual(reloadedPocoItem2.count, 123); + assert.strictEqual(reloadedPocoStoreItem2.count, 2); + + // write with wildcard etag should work + reloadedPocoItem2.count = 100; + reloadedPocoStoreItem2.count = 100; + reloadedPocoStoreItem2.eTag = '*'; + + const wildcardEtagdict = { + pocoItem: reloadedPocoItem2, + pocoStoreItem: reloadedPocoStoreItem2 + }; + + await storage.write(wildcardEtagdict); + + const reloadedStoreItems3 = await storage.read(['pocoItem', 'pocoStoreItem']); + + assert.strictEqual(reloadedStoreItems3.pocoItem.count, 100); + assert.strictEqual(reloadedStoreItems3.pocoStoreItem.count, 100); + + // Write with empty etag should not work + try { + const reloadedStoreItems4 = await storage.read(['pocoStoreItem']); + const reloadedStoreItem4 = reloadedStoreItems4.pocoStoreItem; + + assert.notStrictEqual(reloadedStoreItem4, null); + + reloadedStoreItem4.eTag = ''; + const dict2 = { pocoStoreItem: reloadedStoreItem4 }; + + await storage.write(dict2); + + assert.fail('Should have thrown exception on write with storeItem because of empty eTag'); + } catch (err) { } + + const finalStoreItems = await storage.read(['pocoItem', 'pocoStoreItem']); + assert.strictEqual(finalStoreItems.pocoItem.count, 100); + assert.strictEqual(finalStoreItems.pocoStoreItem.count, 100); + + return nockDone(); + } + }); + + // NOTE: THESE TESTS REQUIRE THAT THE COSMOS DB EMULATOR IS INSTALLED AND STARTED !!!!!!!!!!!!!!!!! + it('should delete an object', async function() { + if(checkEmulator()) { + const { nockDone } = await usingNock(this.test, mode, options); + + const storeItems = { + delete1: { id: 1, count: 1 } + }; + + await storage.write(storeItems); + + const readStoreItems = await storage.read(['delete1']); + + assert.notStrictEqual(readStoreItems.delete1.eTag, null); + assert.strictEqual(readStoreItems.delete1.count, 1); + + await storage.delete(['delete1']); + + const reloadedStoreItems = await storage.read(['delete1']); + + assert.strictEqual(reloadedStoreItems.delete1, undefined); + + return nockDone(); + } + }); + + // NOTE: THESE TESTS REQUIRE THAT THE COSMOS DB EMULATOR IS INSTALLED AND STARTED !!!!!!!!!!!!!!!!! + it('should not throw when deleting unknown object', async function() { + if(checkEmulator()) { + const { nockDone } = await usingNock(this.test, mode, options); + + await assert.doesNotReject(async () => { + await storage.delete(['unknown_key']); + }); + + return nockDone(); + } + }); + + // NOTE: THESE TESTS REQUIRE THAT THE COSMOS DB EMULATOR IS INSTALLED AND STARTED !!!!!!!!!!!!!!!!! + it('should correctly proceed through a waterfall dialog', async function() { + if(checkEmulator()) { + const { nockDone } = await usingNock(this.test, mode, options); + + const convoState = new ConversationState(storage); + + const dialogState = convoState.createProperty('dialogState'); + const dialogs = new DialogSet(dialogState); + + const adapter = new TestAdapter(async (turnContext) => { + const dc = await dialogs.createContext(turnContext); + + await dc.continueDialog(); + if (!turnContext.responded) { + await dc.beginDialog('waterfallDialog'); + } + }) + .use(new AutoSaveStateMiddleware(convoState)); + + dialogs.add(new TextPrompt('textPrompt', async (promptContext) => { + const result = promptContext.recognized.value; + if (result.length > 3) { + const succeededMessage = MessageFactory.text(`You got it at the ${ promptContext.attemptCount }th try!`); + await promptContext.context.sendActivity(succeededMessage); + return true; + } + + const reply = MessageFactory.text(`Please send a name that is longer than 3 characters. ${ promptContext.attemptCount }`); + await promptContext.context.sendActivity(reply); + return false; + })); + + const steps = [ + async (stepContext) => { + assert.strictEqual(typeof(stepContext.activeDialog.state['stepIndex']), 'number'); + await stepContext.context.sendActivity('step1'); + return Dialog.EndOfTurn; + }, + async (stepContext) => { + assert.strictEqual(typeof(stepContext.activeDialog.state['stepIndex']), 'number'); + await stepContext.prompt('textPrompt', { prompt: MessageFactory.text('Please type your name.') }); + }, + async (stepContext) => { + assert.strictEqual(typeof(stepContext.activeDialog.state['stepIndex']), 'number'); + await stepContext.context.sendActivity('step3'); + return Dialog.EndOfTurn; + }, + ]; + + dialogs.add(new WaterfallDialog('waterfallDialog', steps)); + + await adapter.send('hello') + .assertReply('step1') + .send('hello') + .assertReply('Please type your name.') + .send('hi') + .assertReply('Please send a name that is longer than 3 characters. 1') + .send('hi') + .assertReply('Please send a name that is longer than 3 characters. 2') + .send('hi') + .assertReply('Please send a name that is longer than 3 characters. 3') + .send('Kyle') + .assertReply('You got it at the 4th try!') + .assertReply('step3') + .startTest(); + + return nockDone(); + } + }); +}; + +describe('CosmosDbPartitionedStorage', function() { + this.timeout(20000); + before('cleanup', reset); + testStorage(); + after('cleanup', reset); +}); diff --git a/libraries/botbuilder-azure/tests/mockHelper.js b/libraries/botbuilder-azure/tests/mockHelper.js index bba8c3dac4..d48c9d7211 100644 --- a/libraries/botbuilder-azure/tests/mockHelper.js +++ b/libraries/botbuilder-azure/tests/mockHelper.js @@ -1,19 +1,18 @@ const fs = require('fs-extra'); const path = require('path'); -const nock = require('nock'); const nockBack = require('nock').back; const MockMode = Object.freeze({ - "wild": "wild", - "dryrun": "dryrun", - "record": "record", - "lockdown": "lockdown" + 'wild': 'wild', + 'dryrun': 'dryrun', + 'record': 'record', + 'lockdown': 'lockdown' }); function usingNock(test, mode, options = null) { const testDirectory = getMockDirectory(test); - const testFile = getFormatedNockFileName(test) + const testFile = getFormatedNockFileName(test); nockBack.setMode(mode); nockBack.fixtures = testDirectory; @@ -24,7 +23,36 @@ function usingNock(test, mode, options = null) { fixedScope = options.scope; } - const setFixedScope = function (requests) { + /** + * Test recordings fail when testing waterfall dialogs because the SDK sets a random instanceId GUID + * and it's included in the request body. The next two functions replace that instanceId with a constant + * so that tests using waterfall dialogs can pass when using the nock recordings + */ + const instanceIdRegEx = /"instanceId":"[\w-]{36}"/g; + const instanceIdReplacement = 'fakeInstanceId'; + + const replaceCalledInstanceId = function(scope) { + scope.filteringRequestBody = (body) => { + return body.replace(instanceIdRegEx, `"instanceId":"${ instanceIdReplacement }"`); + }; + }; + + const replaceRecordedInstanceId = function(requests) { + requests.map(req => { + if (req.body && req.body.document && req.body.document.dialogState && req.body.document.dialogState.dialogStack) { + req.body.document.dialogState.dialogStack.forEach(stack => { + if (stack.state && stack.state.values && stack.state.values.instanceId) { + stack.state.values.instanceId = instanceIdReplacement; + } + }); + } + return req; + }); + + return requests; + }; + + const setFixedScope = function(requests) { if (fixedScope) { requests = requests.map(req => { req.scope = fixedScope; @@ -32,10 +60,13 @@ function usingNock(test, mode, options = null) { }); } + requests = replaceRecordedInstanceId(requests); + return requests; - } + }; const nockBackOptions = { + before: replaceCalledInstanceId, afterRecord: setFixedScope, recorder: { output_objects: true, @@ -61,10 +92,10 @@ function getMockDirectory(test) { } function getFormatedNockFileName(test) { - return `${test.title.replace(/ /g, '_')}.json` + return `${ test.title.replace(/ /g, '_') }.json`; } module.exports = { MockMode: MockMode, usingNock: usingNock -} \ No newline at end of file +}; \ No newline at end of file diff --git a/libraries/botbuilder-azure/tsconfig.json b/libraries/botbuilder-azure/tsconfig.json index 3788218c0b..f7ed49127c 100644 --- a/libraries/botbuilder-azure/tsconfig.json +++ b/libraries/botbuilder-azure/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "ESNext", + "target": "es6", + "lib": ["es2015"], "module": "commonjs", "declaration": true, "sourceMap": true, diff --git a/libraries/botbuilder-core/package.json b/libraries/botbuilder-core/package.json index 302693db8d..1c3d309349 100644 --- a/libraries/botbuilder-core/package.json +++ b/libraries/botbuilder-core/package.json @@ -25,6 +25,7 @@ }, "devDependencies": { "@types/mocha": "^2.2.47", + "@types/node": "^12.6.9", "mocha": "^5.2.0", "nyc": "^11.4.1", "source-map-support": "^0.5.3", @@ -34,7 +35,7 @@ "scripts": { "test": "tsc && nyc mocha tests/", "build": "tsc", - "build-docs": "typedoc --theme markdown --entryPoint botbuilder --excludePrivate --includeDeclarations --ignoreCompilerErrors --module amd --out ..\\..\\doc\\botbuilder .\\lib\\index.d.ts ..\\botbuilder-core\\lib\\index.d.ts ..\\botbuilder-core-extensions\\lib\\index.d.ts ..\\botframework-schema\\lib\\index.d.ts --hideGenerator --name \"Bot Builder SDK\" --readme none", + "build-docs": "typedoc --theme markdown --entryPoint botbuilder --excludePrivate --includeDeclarations --ignoreCompilerErrors --module amd --out ..\\..\\doc\\botbuilder .\\lib\\index.d.ts ..\\botbuilder-core\\lib\\index.d.ts ..\\botframework-schema\\lib\\index.d.ts --hideGenerator --name \"Bot Builder SDK\" --readme none", "clean": "erase /q /s .\\lib", "set-version": "npm version --allow-same-version ${Version}" }, diff --git a/libraries/botbuilder-core/src/activityHandler.ts b/libraries/botbuilder-core/src/activityHandler.ts index 12e45b739d..828ec918a3 100644 --- a/libraries/botbuilder-core/src/activityHandler.ts +++ b/libraries/botbuilder-core/src/activityHandler.ts @@ -1,181 +1,306 @@ -/** - * @module botbuilder - */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { Activity, ActivityTypes, TurnContext } from '.'; +import { MessageReaction, TurnContext } from '.'; +import { ActivityHandlerBase } from './activityHandlerBase'; +/** + * Describes a bot activity event handler, for use with an [ActivityHandler](xref:botbuilder-core.ActivityHandler) object. + * + * @remarks + * **Parameters** + * + * | Name | Type | Description | + * | :--- | :--- | :--- | + * | `context` | [TurnContext](xref:botbuilder-core.TurnContext) | The context object for the turn. | + * | `next` | () => Promise | A continuation function for handling the activity. | + * + * **Returns** + * + * `any` + * + * The incoming activity is contained in the `context` object's [activity](xref:botbuilder-core.TurnContext.activity) property. + * Call the `next` function to continue the processing of activity events. Not doing so will stop propagation of events for this activity. + * + * A bot activity handler can return a value, to support _invoke_ activities. + */ export type BotHandler = (context: TurnContext, next: () => Promise) => Promise; /** - * Event-emitting base class bots. + * Event-emitting activity handler for bots. Extends [ActivityHandlerBase](xref:botbuilder-core.ActivityHandlerBase). * * @remarks - * This provides an extensible base class for handling incoming - * activities in an event-driven way. Developers may bind one or - * more handlers for each type of event. + * This provides an extensible class for handling incoming activities in an event-driven way. + * You can register an arbitrary set of handlers for each event type. * - * To bind a handler to an event, use the `on()` method, for example: + * To register a handler for an event, use the corresponding _on event_ method. If multiple handlers are + * registered for an event, they are run in the order in which they were registered. * - * ```Javascript - * bot.onMessage(async (context, next) => { - * // do something - * // then `await next()` to continue processing - * await next(); - * }); - * ``` - * - * A series of events will be emitted while the activity is being processed. - * Handlers can stop the propagation of the event by omitting a call to `next()`. + * This object emits a series of _events_ as it processes an incoming activity. + * A handler can stop the propagation of the event by not calling the continuation function. * - * * Turn - emitted for every activity - * * Type-specific - an event, based on activity.type - * * Sub-type - any specialized events, based on activity content - * * Dialog - the final event, used for processing Dialog actions + * | Event type | Description | + * | :--- | :--- | + * | Turn | Emitted first for every activity. | + * | Type-specific | Emitted for the specific activity type, before emitting an event for any sub-type. | + * | Sub-type | Emitted for certain specialized events, based on activity content. | + * | Dialog | Emitted as the final activity processing event. Designed for passing control to a dialog. | * - * A simple implementation: - * ```Javascript + * For example: + * + * ```typescript * const bot = new ActivityHandler(); * - * server.post('/api/messages', (req, res) => { - * adapter.processActivity(req, res, async (context) => { - * // Route to main dialog. - * await bot.run(context); - * }); + * server.post('/api/messages', (req, res) => { + * adapter.processActivity(req, res, async (context) => { + * // Route to main dialog. + * await bot.run(context); + * }); * }); * - * bot.onMessage(async (context, next) => { - * // do stuff - * await context.sendActivity(`Echo: ${ context.activity.text }`); - * // proceed with further processing - * await next(); - * }); + * bot.onTurn(async (context, next) => { + * // Handle a "turn" event. + * await context.sendActivity(`${ context.activity.type } activity received.`); + * // Continue with further processing. + * await next(); + * }) + * .onMessage(async (context, next) => { + * // Handle a message activity. + * await context.sendActivity(`Echo: ${ context.activity.text }`); + * // Continue with further processing. + * await next(); + * }); * ``` + * + * **See also** + * - The [Bot Framework Activity schema](https://aka.ms/botSpecs-activitySchema) */ -export class ActivityHandler { - private readonly handlers: {[type: string]: BotHandler[]} = {}; +export class ActivityHandler extends ActivityHandlerBase { + protected readonly handlers: {[type: string]: BotHandler[]} = {}; /** - * Bind a handler to the Turn event that is fired for every incoming activity, regardless of type + * Registers an activity event handler for the _turn_ event, emitted for every incoming activity, regardless of type. + * + * @param handler The event handler. + * * @remarks - * @param handler BotHandler A handler function in the form async(context, next) => { ... } + * Returns a reference to the [ActivityHandler](xref:botbuilder-core.ActivityHandler) object. */ public onTurn(handler: BotHandler): this { return this.on('Turn', handler); } /** - * Receives all incoming Message activities + * Registers an activity event handler for the _message_ event, emitted for every incoming message activity. + * + * @param handler The event handler. + * * @remarks - * Message activities represent content intended to be shown within a conversational interface. - * Message activities may contain text, speech, interactive cards, and binary or unknown attachments. - * Note that while most messages do contain text, this field is not always present! - * @param handler BotHandler A handler function in the form async(context, next) => { ... } + * Returns a reference to the [ActivityHandler](xref:botbuilder-core.ActivityHandler) object. + * + * Message activities represent content intended to be shown within a conversational interface + * and can contain text, speech, interactive cards, and binary or unknown attachments. + * Not all message activities contain text, the activity's [text](xref:botframework-schema.Activity.text) + * property can be `null` or `undefined`. */ public onMessage(handler: BotHandler): this { return this.on('Message', handler); } /** - * Receives all ConversationUpdate activities, regardless of whether members were added or removed + * Registers an activity event handler for the _conversation update_ event, emitted for every incoming + * conversation update activity. + * + * @param handler The event handler. + * * @remarks - * Conversation update activities describe a change in a conversation's members, description, existence, or otherwise. - * @param handler BotHandler A handler function in the form async(context, next) => { ... } + * Returns a reference to the [ActivityHandler](xref:botbuilder-core.ActivityHandler) object. + * + * Conversation update activities describe a changes to a conversation's metadata, such as title, participants, + * or other channel-specific information. + * + * To handle when members are added to or removed from the conversation, use the + * [onMembersAdded](xref:botbuilder-core.ActivityHandler.onMembersAdded) and + * [onMembersRemoved](xref:botbuilder-core.ActivityHandler.onMembersRemoved) sub-type event handlers. */ public onConversationUpdate(handler: BotHandler): this { return this.on('ConversationUpdate', handler); } /** - * Receives only ConversationUpdate activities representing members being added. + * Registers an activity event handler for the _members added_ event, emitted for any incoming + * conversation update activity that includes members added to the conversation. + * + * @param handler The event handler. + * * @remarks - * context.activity.membersAdded will include at least one entry. - * @param handler BotHandler A handler function in the form async(context, next) => { ... } + * Returns a reference to the [ActivityHandler](xref:botbuilder-core.ActivityHandler) object. + * + * The activity's [membersAdded](xref:botframework-schema.Activity.membersAdded) property + * contains the members added to the conversation, which can include the bot. + * + * To handle conversation update events in general, use the + * [onConversationUpdate](xref:botbuilder-core.ActivityHandler.onConversationUpdate) type-specific event handler. */ public onMembersAdded(handler: BotHandler): this { return this.on('MembersAdded', handler); } /** - * Receives only ConversationUpdate activities representing members being removed. + * Registers an activity event handler for the _members removed_ event, emitted for any incoming + * conversation update activity that includes members removed from the conversation. + * + * @param handler The event handler. + * * @remarks - * context.activity.membersRemoved will include at least one entry. - * @param handler BotHandler A handler function in the form async(context, next) => { ... } + * Returns a reference to the [ActivityHandler](xref:botbuilder-core.ActivityHandler) object. + * + * The activity's [membersRemoved](xref:botframework-schema.Activity.membersRemoved) property + * contains the members removed from the conversation, which can include the bot. + * + * To handle conversation update events in general, use the + * [onConversationUpdate](xref:botbuilder-core.ActivityHandler.onConversationUpdate) type-specific event handler. */ public onMembersRemoved(handler: BotHandler): this { return this.on('MembersRemoved', handler); } /** - * Receives only MessageReaction activities, regardless of whether message reactions were added or removed + * Registers an activity event handler for the _message reaction_ event, emitted for every incoming + * message reaction activity. + * + * @param handler The event handler. + * * @remarks - * MessageReaction activities are sent to the bot when a message reacion, such as 'like' or 'sad' are - * associated with an activity previously sent from the bot. - * @param handler BotHandler A handler function in the form async(context, next) => { ... } + * Returns a reference to the [ActivityHandler](xref:botbuilder-core.ActivityHandler) object. + * + * Message reaction activities represent a social interaction on an existing message activity + * within a conversation. The original activity is referred to by the message reaction activity's + * [replyToId](xref:botframework-schema.Activity.replyToId) property. The + * [from](xref:botframework-schema.Activity.from) property represents the source of the reaction, + * such as the user that reacted to the message. + * + * To handle when reactions are added to or removed from messages in the conversation, use the + * [onReactionsAdded](xref:botbuilder-core.ActivityHandler.onReactionsAdded) and + * [onReactionsRemoved](xref:botbuilder-core.ActivityHandler.onReactionsRemoved) sub-type event handlers. */ public onMessageReaction(handler: BotHandler): this { return this.on('MessageReaction', handler); } /** - * Receives only MessageReaction activities representing message reactions being added. + * Registers an activity event handler for the _reactions added_ event, emitted for any incoming + * message reaction activity that describes reactions added to a message. + * + * @param handler The event handler. + * * @remarks - * context.activity.reactionsAdded will include at least one entry. - * @param handler BotHandler A handler function in the form async(context, next) => { ... } + * Returns a reference to the [ActivityHandler](xref:botbuilder-core.ActivityHandler) object. + * + * The activity's [reactionsAdded](xref:botframework-schema.Activity.reactionsAdded) property + * includes one or more reactions that were added. + * + * To handle message reaction events in general, use the + * [onMessageReaction](xref:botbuilder-core.ActivityHandler.onMessageReaction) type-specific event handler. */ public onReactionsAdded(handler: BotHandler): this { return this.on('ReactionsAdded', handler); } /** - * Receives only MessageReaction activities representing message reactions being removed. + * Registers an activity event handler for the _reactions removed_ event, emitted for any incoming + * message reaction activity that describes reactions removed from a message. + * + * @param handler The event handler. + * * @remarks - * context.activity.reactionsRemoved will include at least one entry. - * @param handler BotHandler A handler function in the form async(context, next) => { ... } + * Returns a reference to the [ActivityHandler](xref:botbuilder-core.ActivityHandler) object. + * + * The activity's [reactionsRemoved](xref:botframework-schema.Activity.reactionsRemoved) property + * includes one or more reactions that were removed. + * + * To handle message reaction events in general, use the + * [onMessageReaction](xref:botbuilder-core.ActivityHandler.onMessageReaction) type-specific event handler. */ public onReactionsRemoved(handler: BotHandler): this { return this.on('ReactionsRemoved', handler); } /** - * Receives all Event activities. + * Registers an activity event handler for the _event_ event, emitted for every incoming event activity. + * + * @param handler The event handler. + * * @remarks + * Returns a reference to the [ActivityHandler](xref:botbuilder-core.ActivityHandler) object. + * * Event activities communicate programmatic information from a client or channel to a bot. - * The meaning of an event activity is defined by the `name` field. - * @param handler BotHandler A handler function in the form async(context, next) => { ... } + * The meaning of an event activity is defined by the activity's + * [name](xref:botframework-schema.Activity.name) property, which is meaningful within the scope + * of a channel. Event activities are designed to carry both interactive information (such as + * button clicks) and non-interactive information (such as a notification of a client + * automatically updating an embedded speech model). + * + * To handle a `tokens/response` event event, use the + * [onTokenResponseEvent](xref:botbuilder-core.ActivityHandler.onTokenResponseEvent) sub-type + * event handler. To handle other named events, add logic to this handler. */ public onEvent(handler: BotHandler): this { return this.on('Event', handler); } /** - * Receives event activities of type 'tokens/response' + * Registers an activity event handler for the _tokens-response_ event, emitted for any incoming + * `tokens/response` event activity. These are generated as part of the OAuth authentication flow. + * + * @param handler The event handler. + * * @remarks - * These events occur during the oauth flow - * @param handler BotHandler A handler function in the form async(context, next) => { ... } + * Returns a reference to the [ActivityHandler](xref:botbuilder-core.ActivityHandler) object. + * + * The activity's [value](xref:botframework-schema.Activity.value) property contains the user token. + * + * If your bot handles authentication using an [OAuthPrompt](xref:botbuilder-dialogs.OAuthPrompt) + * within a dialog, then the dialog will need to receive this activity to complete the authentication flow. + * + * To handle other named events and event events in general, use the + * [onEvent](xref:botbuilder-core.ActivityHandler.onEvent) type-specific event handler. */ public onTokenResponseEvent(handler: BotHandler): this { return this.on('TokenResponseEvent', handler); } /** - * UnrecognizedActivityType will fire if an activity is received with a type that has not previously been defined. + * Registers an activity event handler for the _unrecognized activity type_ event, emitted for an + * incoming activity with a type for which the [ActivityHandler](xref:botbuilder-core.ActivityHandler) + * doesn't provide an event handler. + * + * @param handler The event handler. + * * @remarks - * Some channels or custom adapters may create Actitivies with different, "unofficial" types. - * These events will be passed through as UnrecognizedActivityType events. - * Check `context.activity.type` for the type value. - * @param handler BotHandler A handler function in the form async(context, next) => { ... } + * Returns a reference to the [ActivityHandler](xref:botbuilder-core.ActivityHandler) object. + * + * The `ActivityHandler` does not define events for all activity types defined in the + * [Bot Framework Activity schema](http://aka.ms/botSpecs-activitySchema). In addition, + * channels and custom adapters can create [Activities](xref:botframework-schema.Activity) with + * types not in the schema. When the activity handler receives such an event, it emits an unrecognized activity type event. + * + * The activity's [type](xref:botframework-schema.Activity.type) property contains the activity type. */ public onUnrecognizedActivityType(handler: BotHandler): this { return this.on('UnrecognizedActivityType', handler); } /** - * onDialog fires at the end of the event emission process, and should be used to handle Dialog activity. + * Registers an activity event handler for the _dialog_ event, emitted as the last event for an incoming activity. + * + * @param handler The event handler. + * * @remarks - * Sample code: + * Returns a reference to the [ActivityHandler](xref:botbuilder-core.ActivityHandler) object. + * + * For example: * ```javascript * bot.onDialog(async (context, next) => { * if (context.activity.type === ActivityTypes.Message) { @@ -187,16 +312,22 @@ export class ActivityHandler { * await next(); * }); * ``` - * @param handler BotHandler A handler function in the form async(context, next) => { ... } */ public onDialog(handler: BotHandler): this { return this.on('Dialog', handler); } /** - * `run()` is the main "activity handler" function used to ingest activities into the event emission process. + * Called to initiate the event emission process. + * + * @param context The context object for the current turn. + * * @remarks - * Sample code: + * Typically, you would provide this method as the function handler that the adapter calls + * to perform the bot's logic after the received activity has been pre-processed by the adapter + * and routed through any middleware. + * + * For example: * ```javascript * server.post('/api/messages', (req, res) => { * adapter.processActivity(req, res, async (context) => { @@ -205,82 +336,252 @@ export class ActivityHandler { * }); * }); * ``` - * - * @param context TurnContext A TurnContext representing an incoming Activity from an Adapter + * + * **See also** + * - [BotFrameworkAdapter.processActivity](xref:botbuilder.BotFrameworkAdapter.processActivity) */ public async run(context: TurnContext): Promise { + await super.run(context); + } + + /** + * Called at the start of the event emission process. + * + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to use custom logic for emitting events. + * + * The default logic is to call any handlers registered via [onTurn](xref:botbuilder-core.ActivityHandler.onTurn), + * and then continue by calling [ActivityHandlerBase.onTurnActivity](xref:botbuilder-core.ActivityHandlerBase.onTurnActivity). + */ + protected async onTurnActivity(context: TurnContext): Promise { + await this.handle(context, 'Turn', async () => { + await super.onTurnActivity(context); + }); + } - if (!context) { - throw new Error(`Missing TurnContext parameter`); + /** + * Runs all registered _message_ handlers and then continues the event emission process. + * + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to support channel-specific behavior across multiple channels. + * + * The default logic is to call any handlers registered via + * [onMessage](xref:botbuilder-core.ActivityHandler.onMessage), + * and then continue by calling [defaultNextEvent](xref:botbuilder-core.ActivityHandler.defaultNextEvent). + */ + protected async onMessageActivity(context: TurnContext): Promise { + await this.handle(context, 'Message', this.defaultNextEvent(context)); + } + + /** + * Runs all registered _unrecognized activity type_ handlers and then continues the event emission process. + * + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to support channel-specific behavior across multiple channels. + * + * The default logic is to call any handlers registered via + * [onUnrecognizedActivityType](xref:botbuilder-core.ActivityHandler.onUnrecognizedActivityType), + * and then continue by calling [defaultNextEvent](xref:botbuilder-core.ActivityHandler.defaultNextEvent). + */ + protected async onUnrecognizedActivity(context: TurnContext): Promise { + await this.handle(context, 'UnrecognizedActivityType', this.defaultNextEvent(context)); + } + + /** + * Runs all registered _conversation update_ handlers and then continues the event emission process. + * + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to support channel-specific behavior across multiple channels. + * + * The default logic is to call any handlers registered via + * [onConversationUpdate](xref:botbuilder-core.ActivityHandler.onConversationUpdate), + * and then continue by calling + * [dispatchConversationUpdateActivity](xref:botbuilder-core.ActivityHandler.dispatchConversationUpdateActivity). + */ + protected async onConversationUpdateActivity(context: TurnContext): Promise { + await this.handle(context, 'ConversationUpdate', async () => { + await this.dispatchConversationUpdateActivity(context); + }); + } + + /** + * Runs the _conversation update_ sub-type handlers, as appropriate, and then continues the event emission process. + * + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to support channel-specific behavior across multiple channels or to add + * custom conversation update sub-type events. + * + * The default logic is: + * - If any members were added, call handlers registered via [onMembersAdded](xref:botbuilder-core.ActivityHandler.onMembersAdded). + * - If any members were removed, call handlers registered via [onMembersRemoved](xref:botbuilder-core.ActivityHandler.onMembersRemoved). + * - Continue by calling [defaultNextEvent](xref:botbuilder-core.ActivityHandler.defaultNextEvent). + */ + protected async dispatchConversationUpdateActivity(context: TurnContext): Promise { + if (context.activity.membersAdded && context.activity.membersAdded.length > 0) { + await this.handle(context, 'MembersAdded', this.defaultNextEvent(context)); + } else if (context.activity.membersRemoved && context.activity.membersRemoved.length > 0) { + await this.handle(context, 'MembersRemoved', this.defaultNextEvent(context)); + } else { + await this.defaultNextEvent(context)(); } + } + + /** + * Runs all registered _message reaction_ handlers and then continues the event emission process. + * + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to support channel-specific behavior across multiple channels. + * + * The default logic is to call any handlers registered via + * [onMessageReaction](xref:botbuilder-core.ActivityHandler.onMessageReaction), + * and then continue by calling + * [dispatchMessageReactionActivity](xref:botbuilder-core.ActivityHandler.dispatchMessageReactionActivity). + */ + protected async onMessageReactionActivity(context: TurnContext): Promise { + await this.handle(context, 'MessageReaction', async () => { + await this.dispatchMessageReactionActivity(context); + }); + } - if (!context.activity) { - throw new Error(`TurnContext does not include an activity`); + /** + * Runs all registered _reactions added_ handlers and then continues the event emission process. + * + * @param reactionsAdded The list of reactions added. + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to support channel-specific behavior across multiple channels. + * + * The default logic is to call any handlers registered via + * [onReactionsAdded](xref:botbuilder-core.ActivityHandler.onReactionsAdded), + * and then continue by calling [defaultNextEvent](xref:botbuilder-core.ActivityHandler.defaultNextEvent). + */ + protected async onReactionsAddedActivity(reactionsAdded: MessageReaction[], context: TurnContext): Promise { + await this.handle(context, 'ReactionsAdded', this.defaultNextEvent(context)); + } + + /** + * Runs all registered _reactions removed_ handlers and then continues the event emission process. + * + * @param reactionsRemoved The list of reactions removed. + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to support channel-specific behavior across multiple channels. + * + * The default logic is to call any handlers registered via + * [onReactionsRemoved](xref:botbuilder-core.ActivityHandler.onReactionsRemoved), + * and then continue by calling [defaultNextEvent](xref:botbuilder-core.ActivityHandler.defaultNextEvent). + */ + protected async onReactionsRemovedActivity(reactionsRemoved: MessageReaction[], context: TurnContext): Promise { + await this.handle(context, 'ReactionsRemoved', this.defaultNextEvent(context)); + } + + /** + * Runs the _message reaction_ sub-type handlers, as appropriate, and then continues the event emission process. + * + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to support channel-specific behavior across multiple channels or to add + * custom message reaction sub-type events. + * + * The default logic is: + * - If reactions were added, call handlers registered via [onReactionsAdded](xref:botbuilder-core.ActivityHandler.onReactionsAdded). + * - If reactions were removed, call handlers registered via [onMembersRemoved](xref:botbuilder-core.ActivityHandler.onMembersRemoved). + * - Continue by calling [defaultNextEvent](xref:botbuilder-core.ActivityHandler.defaultNextEvent). + */ + protected async dispatchMessageReactionActivity(context: TurnContext): Promise { + if (context.activity.reactionsAdded || context.activity.reactionsRemoved) { + await super.onMessageReactionActivity(context); + } else { + await this.defaultNextEvent(context)(); } + } - if (!context.activity.type) { - throw new Error(`Activity is missing it's type`); + /** + * Runs all registered event_ handlers and then continues the event emission process. + * + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to support channel-specific behavior across multiple channels. + * + * The default logic is to call any handlers registered via + * [onEvent](xref:botbuilder-core.ActivityHandler.onEvent), + * and then continue by calling + * [dispatchEventActivity](xref:botbuilder-core.ActivityHandler.dispatchEventActivity). + */ + protected async onEventActivity(context: TurnContext): Promise { + await this.handle(context, 'Event', async () => { + await this.dispatchEventActivity(context); + }); + } + + /** + * Runs the _event_ sub-type handlers, as appropriate, and then continues the event emission process. + * + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to support channel-specific behavior across multiple channels or to add custom event sub-type events. + * For certain channels, such as Web Chat and custom Direct Line clients, developers can emit custom event activities from the client. + * + * The default logic is: + * - If the activity is a 'tokens/response' event, call handlers registered via + * [onTokenResponseEvent](xref:botbuilder-core.ActivityHandler.onTokenResponseEvent). + * - Continue by calling [defaultNextEvent](xref:botbuilder-core.ActivityHandler.defaultNextEvent). + */ + protected async dispatchEventActivity(context: TurnContext): Promise { + if (context.activity.name === 'tokens/response') { + await this.handle(context, 'TokenResponseEvent', this.defaultNextEvent(context)); + } else { + await this.defaultNextEvent(context)(); } - - // Allow the dialog system to be triggered at the end of the chain + } + + /** + * Called at the end of the event emission process. + * + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to use custom logic for emitting events. + * + * The default logic is to call any handlers registered via [onDialog](xref:botbuilder-core.ActivityHandler.onDialog), + * and then complete the event emission process. + */ + protected defaultNextEvent(context: TurnContext): () => Promise { const runDialogs = async (): Promise => { await this.handle(context, 'Dialog', async () => { // noop }); }; - - // List of all Activity Types: - // https://github.com/Microsoft/botbuilder-js/blob/master/libraries/botframework-schema/src/index.ts#L1627 - await this.handle(context, 'Turn', async () => { - switch (context.activity.type) { - case ActivityTypes.Message: - await this.handle(context, 'Message', runDialogs); - break; - case ActivityTypes.ConversationUpdate: - await this.handle(context, 'ConversationUpdate', async () => { - if (context.activity.membersAdded && context.activity.membersAdded.length > 0) { - await this.handle(context, 'MembersAdded', runDialogs); - } else if (context.activity.membersRemoved && context.activity.membersRemoved.length > 0) { - await this.handle(context, 'MembersRemoved', runDialogs); - } else { - await runDialogs(); - } - }); - break; - case ActivityTypes.MessageReaction: - await this.handle(context, 'MessageReaction', async () => { - if (context.activity.reactionsAdded && context.activity.reactionsAdded.length > 0) { - await this.handle(context, 'ReactionsAdded', runDialogs); - } else if (context.activity.reactionsRemoved && context.activity.reactionsRemoved.length > 0) { - await this.handle(context, 'ReactionsRemoved', runDialogs); - } else { - await runDialogs(); - } - }); - break; - case ActivityTypes.Event: - await this.handle(context, 'Event', async () => { - if (context.activity.name === 'tokens/response') { - await this.handle(context, 'TokenResponseEvent', runDialogs); - } else { - await runDialogs(); - } - }); - break; - default: - // handler for unknown or unhandled types - await this.handle(context, 'UnrecognizedActivityType', runDialogs); - break; - } - }); + return runDialogs; } /** - * Private method used to bind handlers to events by name - * @param type string - * @param handler BotHandler + * Registers a bot event handler to receive a specific event. + * + * @param type The identifier for the event type. + * @param handler The event handler to register. + * + * @remarks + * Returns a reference to the [ActivityHandler](xref:botbuilder-core.ActivityHandler) object. */ - private on(type: string, handler: BotHandler) { + protected on(type: string, handler: BotHandler) { if (!this.handlers[type]) { this.handlers[type] = [handler]; } else { @@ -290,11 +591,19 @@ export class ActivityHandler { } /** - * Private method used to fire events and execute any bound handlers - * @param type string - * @param handler BotHandler + * Emits an event and executes any registered handlers. + * + * @param context The context object for the current turn. + * @param type The identifier for the event type. + * @param onNext The continuation function to call after all registered handlers for this event complete. + * + * @remarks + * Runs any registered handlers for this event type and then calls the continuation function. + * + * This optionally produces a return value, to support _invoke_ activities. If multiple handlers + * produce a return value, the first one produced is returned. */ - private async handle(context: TurnContext, type: string, onNext: () => Promise): Promise { + protected async handle(context: TurnContext, type: string, onNext: () => Promise): Promise { let returnValue: any = null; async function runHandler(index: number): Promise { @@ -318,5 +627,4 @@ export class ActivityHandler { return returnValue; } - } diff --git a/libraries/botbuilder-core/src/activityHandlerBase.ts b/libraries/botbuilder-core/src/activityHandlerBase.ts new file mode 100644 index 0000000000..95bc6fa272 --- /dev/null +++ b/libraries/botbuilder-core/src/activityHandlerBase.ts @@ -0,0 +1,260 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { + ActivityTypes, + ChannelAccount, + MessageReaction, + TurnContext } from '.'; + +/** + * Defines the core behavior for event-emitting activity handlers for bots. + * + * @remarks + * This provides an extensible class for handling incoming activities in an event-driven way. + * You can register an arbitrary set of handlers for each event type. + * + * To register a handler for an event, use the corresponding _on event_ method. If multiple handlers are + * registered for an event, they are run in the order in which they were registered. + * + * This object emits a series of _events_ as it processes an incoming activity. + * A handler can stop the propagation of the event by not calling the continuation function. + * + * | Event type | Description | + * | :--- | :--- | + * | Type-specific | Emitted for the specific activity type, before emitting an event for any sub-type. | + * | Sub-type | Emitted for certain specialized events, based on activity content. | + * + * **See also** + * - The [Bot Framework Activity schema](https://aka.ms/botSpecs-activitySchema) + */ +export class ActivityHandlerBase { + /** + * Called at the start of the event emission process. + * + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to use custom logic for emitting events. + * + * The default logic is to call any type-specific and sub-type handlers registered via + * the various _on event_ methods. Type-specific events are defined for: + * - Message activities + * - Conversation update activities + * - Message reaction activities + * - Event activities + * - _Unrecognized_ activities, ones that this class has not otherwise defined an _on event_ method for. + */ + protected async onTurnActivity(context: TurnContext): Promise { + switch (context.activity.type) { + case ActivityTypes.Message: + await this.onMessageActivity(context); + break; + case ActivityTypes.ConversationUpdate: + await this.onConversationUpdateActivity(context); + break; + case ActivityTypes.MessageReaction: + await this.onMessageReactionActivity(context); + break; + case ActivityTypes.Event: + await this.onEventActivity(context); + break; + default: + // handler for unknown or unhandled types + await this.onUnrecognizedActivity(context); + break; + } + } + + /** + * Provides a hook for emitting the _message_ event. + * + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to run registered _message_ handlers and then continue the event + * emission process. + */ + protected async onMessageActivity(context: TurnContext): Promise { + return; + } + + /** + * Provides a hook for emitting the _conversation update_ event. + * + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to run registered _conversation update_ handlers and then continue the event + * emission process. + * + * The default logic is: + * - If members other than the bot were added to the conversation, + * call [onMembersAddedActivity](xref:botbuilder-core.ActivityHandlerBase.onMembersAddedActivity). + * - If members other than the bot were removed from the conversation, + * call [onMembersRemovedActivity](xref:botbuilder-core.ActivityHandlerBase.onMembersRemovedActivity). + */ + protected async onConversationUpdateActivity(context: TurnContext): Promise { + if (context.activity.membersAdded && context.activity.membersAdded.length > 0) { + if (context.activity.membersAdded.filter(m => context.activity.recipient && context.activity.recipient.id !== m.id).length) { + await this.onMembersAddedActivity(context.activity.membersAdded, context); + } + } else if (context.activity.membersRemoved && context.activity.membersRemoved.length > 0) { + if (context.activity.membersRemoved.filter(m => context.activity.recipient && context.activity.recipient.id !== m.id).length) { + await this.onMembersRemovedActivity(context.activity.membersRemoved, context); + } + } + } + + /** + * Provides a hook for emitting the _message reaction_ event. + * + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to run registered _message reaction_ handlers and then continue the event + * emission process. + * + * The default logic is: + * - If reactions were added to a message, + * call [onReactionsAddedActivity](xref:botbuilder-core.ActivityHandlerBase.onReactionsAddedActivity). + * - If reactions were removed from a message, + * call [onReactionsRemovedActivity](xref:botbuilder-core.ActivityHandlerBase.onReactionsRemovedActivity). + */ + protected async onMessageReactionActivity(context: TurnContext): Promise { + if (context.activity.reactionsAdded && context.activity.reactionsAdded.length > 0) { + await this.onReactionsAddedActivity(context.activity.reactionsAdded, context); + } else if (context.activity.reactionsRemoved && context.activity.reactionsRemoved.length > 0) { + await this.onReactionsRemovedActivity(context.activity.reactionsRemoved, context); + } + } + + /** + * Provides a hook for emitting the _event_ event. + * + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to run registered _event_ handlers and then continue the event + * emission process. + */ + protected async onEventActivity(context: TurnContext): Promise { + return; + } + + /** + * Provides a hook for emitting the _unrecognized_ event. + * + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to run registered _unrecognized_ handlers and then continue the event + * emission process. + */ + protected async onUnrecognizedActivity(context: TurnContext): Promise { + return; + } + + /** + * Provides a hook for emitting the _members added_ event, + * a sub-type of the _conversation update_ event. + * + * @param membersAdded An array of the members added to the conversation. + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to run registered _members added_ handlers and then continue the event + * emission process. + */ + protected async onMembersAddedActivity(membersAdded: ChannelAccount[], context: TurnContext): Promise { + return; + } + + /** + * Provides a hook for emitting the _members removed_ event, + * a sub-type of the _conversation update_ event. + * + * @param membersRemoved An array of the members removed from the conversation. + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to run registered _members removed_ handlers and then continue the event + * emission process. + */ + protected async onMembersRemovedActivity(membersRemoved: ChannelAccount[], context: TurnContext): Promise { + return; + } + + /** + * Provides a hook for emitting the _reactions added_ event, + * a sub-type of the _message reaction_ event. + * + * @param reactionsAdded An array of the reactions added to a message. + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to run registered _reactions added_ handlers and then continue the event + * emission process. + */ + protected async onReactionsAddedActivity(reactionsAdded: MessageReaction[], context: TurnContext): Promise { + return; + } + + /** + * Provides a hook for emitting the _reactions removed_ event, + * a sub-type of the _message reaction_ event. + * + * @param reactionsRemoved An array of the reactions removed from a message. + * @param context The context object for the current turn. + * + * @remarks + * Overwrite this method to run registered _reactions removed_ handlers and then continue the event + * emission process. + */ + protected async onReactionsRemovedActivity(reactionsRemoved: MessageReaction[], context: TurnContext): Promise { + return; + } + + /** + * Called to initiate the event emission process. + * + * @param context The context object for the current turn. + * + * @remarks + * Typically, you would provide this method as the function handler that the adapter calls + * to perform the bot's logic after the received activity has been pre-processed by the adapter + * and routed through any middleware. + * + * For example: + * ```javascript + * server.post('/api/messages', (req, res) => { + * adapter.processActivity(req, res, async (context) => { + * // Route to main dialog. + * await bot.run(context); + * }); + * }); + * ``` + * + * **See also** + * - [BotFrameworkAdapter.processActivity](xref:botbuilder.BotFrameworkAdapter.processActivity) + */ + public async run(context: TurnContext): Promise { + + if (!context) { + throw new Error(`Missing TurnContext parameter`); + } + + if (!context.activity) { + throw new Error(`TurnContext does not include an activity`); + } + + if (!context.activity.type) { + throw new Error(`Activity is missing it's type`); + } + + // List of all Activity Types: + // https://github.com/Microsoft/botbuilder-js/blob/master/libraries/botframework-schema/src/index.ts#L1627 + await this.onTurnActivity(context); + } +} diff --git a/libraries/botbuilder-core/src/botAdapter.ts b/libraries/botbuilder-core/src/botAdapter.ts index 41ec41eb24..e679d33be4 100644 --- a/libraries/botbuilder-core/src/botAdapter.ts +++ b/libraries/botbuilder-core/src/botAdapter.ts @@ -1,6 +1,3 @@ -/** - * @module botbuilder - */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. @@ -11,44 +8,87 @@ import { Middleware, MiddlewareHandler, MiddlewareSet } from './middlewareSet'; import { TurnContext } from './turnContext'; /** - * Abstract base class for all adapter plugins. + * Defines the core behavior of a bot adapter that can connect a bot to a service endpoint. * * @remarks - * Adapters manage the communication between the bot and a user over a specific channel, or set - * of channels. + * The bot adapter encapsulates authentication processes and sends activities to and receives + * activities from the Bot Connector Service. When your bot receives an activity, the adapter + * creates a turn context object, passes it to your bot application logic, and sends responses + * back to the user's channel. + * + * The adapter processes and directs incoming activities in through the bot middleware pipeline to + * your bot logic and then back out again. As each activity flows in and out of the bot, each + * piece of middleware can inspect or act upon the activity, both before and after the bot logic runs. + * Use the [use](xref:botbuilder-core.BotAdapter.use) method to add [Middleware](xref:botbuilder-core.Middleware) + * objects to your adapter's middleware collection. + * + * For more information, see the articles on + * [How bots work](https://docs.microsoft.com/azure/bot-service/bot-builder-basics) and + * [Middleware](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-middleware). */ export abstract class BotAdapter { - private middleware: MiddlewareSet = new MiddlewareSet(); + protected middleware: MiddlewareSet = new MiddlewareSet(); private turnError: (context: TurnContext, error: Error) => Promise; /** - * Sends a set of activities to the user. + * Asynchronously sends a set of outgoing activities to a channel server. + * + * This method supports the framework and is not intended to be called directly for your code. + * Use the turn context's [sendActivity](xref:botbuilder-core.TurnContext.sendActivity) or + * [sendActivities](xref:botbuilder-core.TurnContext.sendActivities) method from your bot code. * + * @param context The context object for the turn. + * @param activities The activities to send. + * + * @returns An array of [ResourceResponse](xref:) + * * @remarks - * An array of responses from the server will be returned. - * @param context Context for the current turn of conversation with the user. - * @param activities Set of activities being sent. + * The activities will be sent one after another in the order in which they're received. A + * response object will be returned for each sent activity. For `message` activities this will + * contain the ID of the delivered message. */ public abstract sendActivities(context: TurnContext, activities: Partial[]): Promise; /** - * Replaces an existing activity. - * @param context Context for the current turn of conversation with the user. - * @param activity New replacement activity. The activity should already have it's ID information populated. + * Asynchronously replaces a previous activity with an updated version. + * + * This interface supports the framework and is not intended to be called directly for your code. + * Use [TurnContext.updateActivity](xref:botbuilder-core.TurnContext.updateActivity) to update + * an activity from your bot code. + * + * @param context The context object for the turn. + * @param activity The updated version of the activity to replace. + * + * @remarks + * Not all channels support this operation. For channels that don't, this call may throw an exception. */ public abstract updateActivity(context: TurnContext, activity: Partial): Promise; /** - * Deletes an existing activity. - * @param context Context for the current turn of conversation with the user. - * @param reference Conversation reference of the activity being deleted. + * Asynchronously deletes an existing activity. + * + * This interface supports the framework and is not intended to be called directly for your code. + * Use [TurnContext.deleteActivity](xref:botbuilder-core.TurnContext.deleteActivity) to delete + * an activity from your bot code. + * + * @param context The context object for the turn. + * @param reference Conversation reference information for the activity to delete. + * + * @remarks + * Not all channels support this operation. For channels that don't, this call may throw an exception. */ public abstract deleteActivity(context: TurnContext, reference: Partial): Promise; /** - * Proactively continues an existing conversation. - * @param reference Conversation reference for the conversation being continued. - * @param logic Function to execute for performing the bots logic. + * Asynchronously resumes a conversation with a user, possibly after some time has gone by. + * + * @param reference A reference to the conversation to continue. + * @param logic The asynchronous method to call after the adapter middleware runs. + * + * @remarks + * This is often referred to as a _proactive notification_, the bot can proactively + * send a message to a conversation or user without waiting for an incoming message. + * For example, a bot can use this method to send notifications or coupons to a user. */ public abstract continueConversation( reference: Partial, @@ -56,8 +96,15 @@ export abstract class BotAdapter { ) => Promise): Promise; /** - * Gets/sets a error handler that will be called anytime an uncaught exception is raised during - * a turn. + * Gets or sets an error handler that can catch exceptions in the middleware or application. + * + * @remarks + * The error handler is called with these parameters: + * + * | Name | Type | Description | + * | :--- | :--- | :--- | + * | `context` | [TurnContext](xref:botbuilder-core.TurnContext) | The context object for the turn. | + * | `error` | `Error` | The Node.js error thrown. | */ public get onTurnError(): (context: TurnContext, error: Error) => Promise { return this.turnError; @@ -68,8 +115,13 @@ export abstract class BotAdapter { } /** - * Registers middleware handlers(s) with the adapter. - * @param middleware One or more middleware handlers(s) to register. + * Adds middleware to the adapter's pipeline. + * + * @param middleware The middleware or middleware handlers to add. + * + * @remarks + * Middleware is added to the adapter at initialization time. + * Each turn, the adapter calls its middleware in the order in which you added it. */ public use(...middleware: (MiddlewareHandler|Middleware)[]): this { MiddlewareSet.prototype.use.apply(this.middleware, middleware); @@ -78,19 +130,24 @@ export abstract class BotAdapter { } /** - * Executes the adapters middleware chain. + * Starts activity processing for the current bot turn. * + * @param context The context object for the turn. + * @param next A callback method to run at the end of the pipeline. + * * @remarks - * This should be be called by the parent class to run the adapters middleware chain. The - * `next()` handler passed to the method will be called at the end of the chain. + * The adapter creates a revokable proxy for the turn context and then calls its middleware in + * the order in which you added it. If the middleware chain completes without short circuiting, + * the adapter calls the callback method. If any middleware short circuits, the adapter does not + * call any of the subsequent middleware or the callback method, and the pipeline short circuits. + * + * The adapter calls middleware with a `next` parameter, which represents the next step in the + * pipeline. Middleware should call the `next` method to continue processing without short circuiting. * - * While the context object is passed in from the caller is created by the caller, what gets - * passed to the `next()` handler is a wrapped version of the context which will automatically - * be revoked upon completion of the turn. This causes the bots logic to throw an error if it - * tries to use the context object after the turn completes. - * @param context Context for the current turn of conversation with the user. - * @param next Function to call at the end of the middleware chain. - * @param next.revocableContext A revocable version of the context object. + * When the turn is initiated by a user activity (reactive messaging), the callback method will + * be a reference to the bot's turn handler. When the turn is initiated by a call to + * [continueConversation](xref:botbuilder-core.BotAdapter.continueConversation) (proactive messaging), + * the callback method is the callback method that was provided in the call. */ protected runMiddleware(context: TurnContext, next: (revocableContext: TurnContext) => Promise): Promise { // Wrap context with revocable proxy diff --git a/libraries/botbuilder-core/src/botStatePropertyAccessor.ts b/libraries/botbuilder-core/src/botStatePropertyAccessor.ts index 01f0f206e6..08c4a4af2f 100644 --- a/libraries/botbuilder-core/src/botStatePropertyAccessor.ts +++ b/libraries/botbuilder-core/src/botStatePropertyAccessor.ts @@ -9,9 +9,14 @@ import { BotState } from './botState'; import { TurnContext } from './turnContext'; /** - * An interface components can use to read and write individual properties to the bot's state - * management system. - * @param T (Optional) type of property being persisted. Defaults to `any` type. + * Defines methods for accessing a state property created in a + * [BotState](xref:botbuilder-core.BotState) object. + * + * @typeparam T Optional. The type of the state property to access. Default type is `any`. + * + * @remarks + * To create a state property in a state management objet, use the + * [createProperty\](xref:botbuilder-core.BotState.createProperty) method. */ export interface StatePropertyAccessor { /** diff --git a/libraries/botbuilder-core/src/cardFactory.ts b/libraries/botbuilder-core/src/cardFactory.ts index 94bcdca760..bdef2cdef9 100644 --- a/libraries/botbuilder-core/src/cardFactory.ts +++ b/libraries/botbuilder-core/src/cardFactory.ts @@ -1,21 +1,18 @@ -/** - * @module botbuilder - */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { ActionTypes, AnimationCard, Attachment, AudioCard, CardAction, CardImage, HeroCard, MediaUrl, OAuthCard, ReceiptCard, SigninCard, ThumbnailCard, VideoCard } from 'botframework-schema'; +import { ActionTypes, AnimationCard, Attachment, AudioCard, CardAction, CardImage, HeroCard, MediaUrl, OAuthCard, O365ConnectorCard, ReceiptCard, SigninCard, ThumbnailCard, VideoCard } from 'botframework-schema'; /** - * A set of utility functions designed to assist with the formatting of the various card types a - * bot can return. + * Provides methods for formatting the various card types a bot can return. * * @remarks - * All of these functions return an `Attachment` which can be added to an `Activity` directly or - * passed as input to a `MessageFactory` method. + * All of these functions return an [Attachment](xref:botframework-schema.Attachment) object, + * which can be added to an existing activity's [attachments](xref:botframework-schema.Activity.attachments) collection directly or + * passed as input to one of the [MessageFactory](xref:botbuilder-core.MessageFactory) methods to generate a new activity. * - * The following example shows sending a message containing a single hero card: + * This example sends a message that contains a single hero card. * * ```javascript * const { MessageFactory, CardFactory } = require('botbuilder'); @@ -31,7 +28,7 @@ import { ActionTypes, AnimationCard, Attachment, AudioCard, CardAction, CardImag */ export class CardFactory { /** - * List of content types for each card style. + * Lists the content type schema for each card style. */ public static contentTypes: any = { adaptiveCard: 'application/vnd.microsoft.card.adaptive', @@ -40,24 +37,28 @@ export class CardFactory { heroCard: 'application/vnd.microsoft.card.hero', receiptCard: 'application/vnd.microsoft.card.receipt', oauthCard: 'application/vnd.microsoft.card.oauth', + o365ConnectorCard: 'application/vnd.microsoft.teams.card.o365connector', signinCard: 'application/vnd.microsoft.card.signin', thumbnailCard: 'application/vnd.microsoft.card.thumbnail', videoCard: 'application/vnd.microsoft.card.video' }; /** - * Returns an attachment for an adaptive card. + * Returns an attachment for an Adaptive Card. * + * @param card A description of the Adaptive Card to return. + * * @remarks - * Adaptive Cards are a new way for bots to send interactive and immersive card content to - * users. For channels that don't yet support Adaptive Cards natively, the Bot Framework will - * down render the card to an image that's been styled to look good on the target channel. For + * Adaptive Cards are an open card exchange format enabling developers to exchange UI content in a common and consistent way. + * For channels that don't yet support Adaptive Cards natively, the Bot Framework will + * down-render the card to an image that's been styled to look good on the target channel. For * channels that support [hero cards](#herocards) you can continue to include Adaptive Card * actions and they will be sent as buttons along with the rendered version of the card. * * For more information about Adaptive Cards and to download the latest SDK, visit * [adaptivecards.io](http://adaptivecards.io/). * + * For example: * ```JavaScript * const card = CardFactory.adaptiveCard({ * "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", @@ -77,7 +78,6 @@ export class CardFactory { * ] * }); * ``` - * @param card The adaptive card to return as an attachment. */ public static adaptiveCard(card: any): Attachment { return { contentType: CardFactory.contentTypes.adaptiveCard, content: card }; @@ -85,10 +85,12 @@ export class CardFactory { /** * Returns an attachment for an animation card. - * @param title The cards title. - * @param media Media URL's for the card. - * @param buttons (Optional) set of buttons to include on the card. - * @param other (Optional) additional properties to include on the card. + * + * @param title The card title. + * @param media The media URLs for the card. + * @param buttons Optional. The array of buttons to include on the card. Each `string` in the array + * is converted to an `imBack` button with a title and value set to the value of the string. + * @param other Optional. Any additional properties to include on the card. */ public static animationCard( title: string, @@ -101,10 +103,12 @@ export class CardFactory { /** * Returns an attachment for an audio card. - * @param title The cards title. - * @param media Media URL's for the card. - * @param buttons (Optional) set of buttons to include on the card. - * @param other (Optional) additional properties to include on the card. + * + * @param title The card title. + * @param media The media URL for the card. + * @param buttons Optional. The array of buttons to include on the card. Each `string` in the array + * is converted to an `imBack` button with a title and value set to the value of the string. + * @param other Optional. Any additional properties to include on the card. */ public static audioCard( title: string, @@ -118,10 +122,19 @@ export class CardFactory { /** * Returns an attachment for a hero card. * + * @param title The card title. + * @param text Optional. The card text. + * @param images Optional. The array of images to include on the card. Each element can be a + * [CardImage](ref:botframework-schema.CardImage) or the URL of the image to include. + * @param buttons Optional. The array of buttons to include on the card. Each `string` in the array + * is converted to an `imBack` button with a title and value set to the value of the string. + * @param other Optional. Any additional properties to include on the card. + * * @remarks - * Hero cards tend to have one dominant full width image and the cards text & buttons can - * usually be found below the image. - * + * Hero cards tend to have one dominant, full-width image. + * Channels typically render the card's text and buttons below the image. + * + * For example: * ```javascript * const card = CardFactory.heroCard( * 'White T-Shirt', @@ -129,11 +142,6 @@ export class CardFactory { * ['buy'] * ); * ``` - * @param title The cards title. - * @param text (Optional) text field for the card. - * @param images (Optional) set of images to include on the card. - * @param buttons (Optional) set of buttons to include on the card. - * @param other (Optional) additional properties to include on the card. */ public static heroCard( title: string, @@ -157,21 +165,24 @@ export class CardFactory { ): Attachment { const a: Attachment = CardFactory.thumbnailCard(title, text, images, buttons, other); a.contentType = CardFactory.contentTypes.heroCard; - return a; } /** - * Returns an attachment for an OAuth card used by the Bot Frameworks Single Sign On (SSO) - * service. + * Returns an attachment for an OAuth card. + * * @param connectionName The name of the OAuth connection to use. - * @param title Title of the cards signin button. - * @param text (Optional) additional text to include on the card. + * @param title The title for the card's sign-in button. + * @param text Optional. Additional text to include on the card. + * @param link Optional. The sign-in link to use. + * + * @remarks + * OAuth cards support the Bot Framework's single sign on (SSO) service. */ - public static oauthCard(connectionName: string, title: string, text?: string): Attachment { + public static oauthCard(connectionName: string, title: string, text?: string, link?: string): Attachment { const card: Partial = { buttons: [ - { type: ActionTypes.Signin, title: title, value: undefined, channelData: undefined } + { type: ActionTypes.Signin, title: title, value: link, channelData: undefined } ], connectionName: connectionName }; @@ -180,23 +191,52 @@ export class CardFactory { return { contentType: CardFactory.contentTypes.oauthCard, content: card }; } + + /** + * Returns an attachment for an Office 365 connector card. + * + * @param card a description of the Office 365 connector card to return. + * + * @remarks + * For example: + * ```JavaScript + * const card = CardFactory.o365ConnectorCard({ + * "title": "card title", + * "text": "card text", + * "summary": "O365 card summary", + * "themeColor": "#E67A9E", + * "sections": [ + * { + * "title": "**section title**", + * "text": "section text", + * "activityTitle": "activity title", + * } + * ] + * }); + * ``` + */ + public static o365ConnectorCard(card: O365ConnectorCard): Attachment { + return { contentType: CardFactory.contentTypes.o365ConnectorCard, content: card }; + } + /** * Returns an attachment for a receipt card. - * @param card The adaptive card to return as an attachment. + * + * @param card A description of the receipt card to return. */ public static receiptCard(card: ReceiptCard): Attachment { return { contentType: CardFactory.contentTypes.receiptCard, content: card }; } /** - * Returns an attachment for a signin card. + * Returns an attachment for a sign-in card. * + * @param title The title for the card's sign-in button. + * @param url The URL of the sign-in page to use. + * @param text Optional. Additional text to include on the card. + * * @remarks - * For channels that don't natively support signin cards an alternative message will be - * rendered. - * @param title Title of the cards signin button. - * @param url The link to the signin page the user needs to visit. - * @param text (Optional) additional text to include on the card. + * For channels that don't natively support sign-in cards, an alternative message is rendered. */ public static signinCard(title: string, url: string, text?: string): Attachment { const card: SigninCard = { buttons: [{ type: ActionTypes.Signin, title: title, value: url, channelData: undefined }] }; @@ -208,16 +248,19 @@ export class CardFactory { /** * Returns an attachment for a thumbnail card. * + * @param title The card title. + * @param text Optional. The card text. + * @param images Optional. The array of images to include on the card. Each element can be a + * [CardImage](ref:botframework-schema.CardImage) or the URL of the image to include. + * @param buttons Optional. The array of buttons to include on the card. Each `string` in the array + * is converted to an `imBack` button with a title and value set to the value of the string. + * @param other Optional. Any additional properties to include on the card. + * * @remarks - * Thumbnail cards are similar to [hero cards](#herocard) but instead of a full width image, - * they're typically rendered with a smaller thumbnail version of the image on either side - * and the text will be rendered in column next to the image. Any buttons will typically - * show up under the card. - * @param title The cards title. - * @param text (Optional) text field for the card. - * @param images (Optional) set of images to include on the card. - * @param buttons (Optional) set of buttons to include on the card. - * @param other (Optional) additional properties to include on the card. + * Thumbnail cards are similar to hero cards but instead of a full width image, + * they're typically rendered with a smaller thumbnail version of the image. + * Channels typically render the card's text to one side of the image, + * with any buttons displayed below the card. */ public static thumbnailCard( title: string, @@ -256,10 +299,12 @@ export class CardFactory { /** * Returns an attachment for a video card. - * @param title The cards title. - * @param media Media URLs for the card. - * @param buttons (Optional) set of buttons to include on the card. - * @param other (Optional) additional properties to include on the card. + * + * @param title The card title. + * @param media The media URLs for the card. + * @param buttons Optional. The array of buttons to include on the card. Each `string` in the array + * is converted to an `imBack` button with a title and value set to the value of the string. + * @param other Optional. Any additional properties to include on the card. */ public static videoCard( title: string, @@ -273,10 +318,8 @@ export class CardFactory { /** * Returns a properly formatted array of actions. * - * @remarks - * Supports converting strings to `messageBack` actions (note: using 'imBack' for now as - * 'messageBack' doesn't work properly in emulator.) - * @param actions Array of card actions or strings. Strings will be converted to `messageBack` actions. + * @param actions The array of action to include on the card. Each `string` in the array + * is converted to an `imBack` button with a title and value set to the value of the string. */ public static actions(actions: (CardAction | string)[] | undefined): CardAction[] { const list: CardAction[] = []; @@ -293,7 +336,9 @@ export class CardFactory { /** * Returns a properly formatted array of card images. - * @param images Array of card images or strings. Strings will be converted to card images. + * + * @param images The array of images to include on the card. Each element can be a + * [CardImage](ref:botframework-schema.CardImage) or the URL of the image to include. */ public static images(images: (CardImage | string)[] | undefined): CardImage[] { const list: CardImage[] = []; @@ -309,8 +354,9 @@ export class CardFactory { } /** - * Returns a properly formatted array of media url objects. - * @param links Array of media url objects or strings. Strings will be converted to a media url object. + * Returns a properly formatted array of media URL objects. + * + * @param links The media URLs. Each `string` is converted to a media URL object. */ public static media(links: (MediaUrl | string)[] | undefined): MediaUrl[] { const list: MediaUrl[] = []; diff --git a/libraries/botbuilder-core/src/index.ts b/libraries/botbuilder-core/src/index.ts index 840de56ecc..51e5c9bef7 100644 --- a/libraries/botbuilder-core/src/index.ts +++ b/libraries/botbuilder-core/src/index.ts @@ -8,6 +8,7 @@ export * from 'botframework-schema'; export * from './activityHandler'; +export * from './activityHandlerBase'; export * from './autoSaveStateMiddleware'; export * from './botAdapter'; export * from './botAdapterSet'; @@ -26,6 +27,7 @@ export * from './privateConversationState'; export * from './propertyManager'; export * from './recognizerResult'; export * from './showTypingMiddleware'; +export * from './skypeMentionNormalizeMiddleware'; export * from './storage'; export * from './telemetryLoggerMiddleware'; export * from './testAdapter'; @@ -33,3 +35,4 @@ export * from './transcriptLogger'; export * from './turnContext'; export * from './userState'; export * from './userTokenProvider'; +export * from './userTokenSettings'; diff --git a/libraries/botbuilder-core/src/internal.ts b/libraries/botbuilder-core/src/internal.ts index 4e36afaaf6..f9302d8db1 100644 --- a/libraries/botbuilder-core/src/internal.ts +++ b/libraries/botbuilder-core/src/internal.ts @@ -20,7 +20,7 @@ export function shallowCopy(value: T): T { */ export function makeRevocable>(target: T, handler?: ProxyHandler): { proxy: T; revoke(): void } { // Ensure proxy supported (some browsers don't) - if (Proxy && Proxy.revocable) { + if (typeof Proxy !== 'undefined' && Proxy.revocable) { return Proxy.revocable(target, handler || {}); } else { return { proxy: target, revoke: (): void => { diff --git a/libraries/botbuilder-core/src/skypeMentionNormalizeMiddleware.ts b/libraries/botbuilder-core/src/skypeMentionNormalizeMiddleware.ts new file mode 100644 index 0000000000..770bd97526 --- /dev/null +++ b/libraries/botbuilder-core/src/skypeMentionNormalizeMiddleware.ts @@ -0,0 +1,42 @@ +/** + * @module botbuilder + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { Activity, Middleware, TurnContext } from 'botbuilder-core'; + + +/** + * Middleware to patch mention Entities from Skype since they don't conform to expected values. + * Bots that interact with Skype should use this middleware if mentions are used. + * + * @remarks + * A Skype mention "text" field is of the format: + * botname + * But Activity.Text doesn't contain those tags and RemoveMentionText can't remove + * the entity from Activity.Text. + * This will remove the nodes, leaving just the name. + */ +export class SkypeMentionNormalizeMiddleware implements Middleware { + public static normalizeSkypeMentionText(activity: Activity): void { + if (activity.channelId === 'skype' && activity.type === 'message'){ + activity.entities.map((element): void => { + if(element.type === 'mention'){ + const text = element['text']; + const end = text.indexOf('>'); + if(end > -1){ + const start = text.indexOf('<', end); + if(start > -1) element['text'] = text.substring(end + 1, start); + } + } + }); + } + } + + public async onTurn(turnContext: TurnContext, next: () => Promise): Promise { + SkypeMentionNormalizeMiddleware.normalizeSkypeMentionText(turnContext.activity); + await next(); + } +} diff --git a/libraries/botbuilder-core/src/testAdapter.ts b/libraries/botbuilder-core/src/testAdapter.ts index 8d007b0fca..b16754220e 100644 --- a/libraries/botbuilder-core/src/testAdapter.ts +++ b/libraries/botbuilder-core/src/testAdapter.ts @@ -6,7 +6,7 @@ * Licensed under the MIT License. */ // tslint:disable-next-line:no-require-imports -import assert = require('assert'); +import assert from 'assert'; import { Activity, ActivityTypes, ConversationReference, ResourceResponse, TokenResponse } from 'botframework-schema'; import { BotAdapter } from './botAdapter'; import { TurnContext } from './turnContext'; diff --git a/libraries/botbuilder-core/src/transcriptLogger.ts b/libraries/botbuilder-core/src/transcriptLogger.ts index be0d3a53d2..2a83b88554 100644 --- a/libraries/botbuilder-core/src/transcriptLogger.ts +++ b/libraries/botbuilder-core/src/transcriptLogger.ts @@ -49,7 +49,29 @@ export class TranscriptLoggerMiddleware implements Middleware { context.onSendActivities(async (ctx: TurnContext, activities: Partial[], next2: () => Promise) => { // run full pipeline const responses: ResourceResponse[] = await next2(); - activities.forEach((a: ResourceResponse) => this.logActivity(transcript, this.cloneActivity(a))); + + activities.map((a: Partial, index: number) => { + const clonedActivity = this.cloneActivity(a); + // If present, set the id of the cloned activity to the id received from the server. + if (index < responses.length) { + clonedActivity.id = responses[index].id; + } + + // For certain channels, a ResourceResponse with an id is not always sent to the bot. + // This fix uses the timestamp on the activity to populate its id for logging the transcript. + // If there is no outgoing timestamp, the current time for the bot is used for the activity.id. + // See https://github.com/microsoft/botbuilder-js/issues/1122 + if (!clonedActivity.id) { + const prefix = `g_${Math.random().toString(36).slice(2,8)}`; + if (clonedActivity.timestamp) { + clonedActivity.id = `${prefix}${new Date(clonedActivity.timestamp).getTime().toString()}`; + } else { + clonedActivity.id = `${prefix}${new Date().getTime().toString()}`; + } + } + + this.logActivity(transcript, clonedActivity); + }); return responses; }); @@ -138,13 +160,13 @@ export class TranscriptLoggerMiddleware implements Middleware { * Error logging helper function. * @param err Error or object to console.error out. */ - private transcriptLoggerErrorHandler(err: Error|any): void { + private transcriptLoggerErrorHandler(err: Error | any): void { // tslint:disable:no-console if (err instanceof Error) { - console.error(`TranscriptLoggerMiddleware logActivity failed: "${ err.message }"`); + console.error(`TranscriptLoggerMiddleware logActivity failed: "${err.message}"`); console.error(err.stack); } else { - console.error(`TranscriptLoggerMiddleware logActivity failed: "${ JSON.stringify(err) }"`); + console.error(`TranscriptLoggerMiddleware logActivity failed: "${JSON.stringify(err)}"`); } // tslint:enable:no-console } diff --git a/libraries/botbuilder-core/src/turnContext.ts b/libraries/botbuilder-core/src/turnContext.ts index 632d8dd335..772f4cd5b7 100644 --- a/libraries/botbuilder-core/src/turnContext.ts +++ b/libraries/botbuilder-core/src/turnContext.ts @@ -1,6 +1,3 @@ -/** - * @module botbuilder - */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. @@ -10,11 +7,35 @@ import { BotAdapter } from './botAdapter'; import { shallowCopy } from './internal'; /** - * Signature implemented by functions registered with `context.onSendActivity()`. - * - * ```TypeScript - * type SendActivitiesHandler = (context: TurnContext, activities: Partial[], next: () => Promise) => Promise; - * ``` + * A handler that can participate in send activity events for the current turn. + * + * @remarks + * **Parameters** + * + * | Name | Type | Description | + * | :--- | :--- | :--- | + * | `context` | [TurnContext](xref:botbuilder-core.TurnContext) | The context object for the turn. | + * | `activities` | Partial\<[Activity](xref:botframework-schema.Activity)>[] | The activities to send. | + * | `next` | () => Promise\<[ResourceResponse](xref:botframework-schema.ResourceResponse)[]> | The function to call to continue event processing. | + * + * **Returns** + * + * Promise\<[ResourceResponse](xref:botframework-schema.ResourceResponse)[]> + * + * A handler calls the `next` function to pass control to the next registered handler. If a handler + * doesn't call the `next` function, the adapter does not call any of the subsequent handlers and + * does not send the activities to the user. + * + * If the activities are successfully sent, the `next` function returns an array of + * [ResourceResponse](xref:botframework-schema.ResourceResponse) objects containing the IDs that the + * receiving channel assigned to the activities. Use this array as the return value of this handler. + * + * **See also** + * + * - [BotAdapter](xref:botbuilder-core.BotAdapter) + * - [UpdateActivityHandler](xref:botbuilder-core.UpdateActivityHandler) + * - [DeleteActivityHandler](xref:botbuilder-core.DeleteActivityHandler) + * - [TurnContext.onSendActivities](xref:botbuilder-core.TurnContext.onSendActivities) */ export type SendActivitiesHandler = ( context: TurnContext, @@ -23,20 +44,58 @@ export type SendActivitiesHandler = ( ) => Promise; /** - * Signature implemented by functions registered with `context.onUpdateActivity()`. - * - * ```TypeScript - * type UpdateActivityHandler = (context: TurnContext, activity: Partial, next: () => Promise) => Promise; - * ``` + * A handler that can participate in update activity events for the current turn. + * + * @remarks + * **Parameters** + * + * | Name | Type | Description | + * | :--- | :--- | :--- | + * | `context` | [TurnContext](xref:botbuilder-core.TurnContext) | The context object for the turn. | + * | `activities` | Partial\<[Activity](xref:botframework-schema.Activity)> | The replacement activity. | + * | `next` | () => Promise\ | The function to call to continue event processing. | + * + * A handler calls the `next` function to pass control to the next registered handler. + * If a handler doesn’t call the `next` function, the adapter does not call any of the + * subsequent handlers and does not attempt to update the activity. + * + * The `activity` parameter's [id](xref:botframework-schema.Activity.id) property indicates which activity + * in the conversation to replace. + * + * **See also** + * + * - [BotAdapter](xref:botbuilder-core.BotAdapter) + * - [SendActivitiesHandler](xref:botbuilder-core.SendActivitiesHandler) + * - [DeleteActivityHandler](xref:botbuilder-core.DeleteActivityHandler) + * - [TurnContext.onUpdateActivity](xref:botbuilder-core.TurnContext.onUpdateActivity) */ export type UpdateActivityHandler = (context: TurnContext, activity: Partial, next: () => Promise) => Promise; /** - * Signature implemented by functions registered with `context.onDeleteActivity()`. - * - * ```TypeScript - * type DeleteActivityHandler = (context: TurnContext, reference: Partial, next: () => Promise) => Promise; - * ``` + * A handler that can participate in delete activity events for the current turn. + * + * @remarks + * **Parameters** + * + * | Name | Type | Description | + * | :--- | :--- | :--- | + * | `context` | [TurnContext](xref:botbuilder-core.TurnContext) | The context object for the turn. | + * | `reference` | Partial\<[ConversationReference](xref:botframework-schema.ConversationReference)> | The conversation containing the activity to delete. | + * | `next` | () => Promise\ | The function to call to continue event processing. | + * + * A handler calls the `next` function to pass control to the next registered handler. + * If a handler doesn’t call the `next` function, the adapter does not call any of the + * subsequent handlers and does not attempt to delete the activity. + * + * The `reference` parameter's [activityId](xref:botframework-schema.ConversationReference.activityId) property indicates which activity + * in the conversation to delete. + * + * **See also** + * + * - [BotAdapter](xref:botbuilder-core.BotAdapter) + * - [SendActivitiesHandler](xref:botbuilder-core.SendActivitiesHandler) + * - [UpdateActivityHandler](xref:botbuilder-core.UpdateActivityHandler) + * - [TurnContext.onDeleteActivity](xref:botbuilder-core.TurnContext.onDeleteActivity) */ export type DeleteActivityHandler = ( context: TurnContext, @@ -44,15 +103,17 @@ export type DeleteActivityHandler = ( next: () => Promise ) => Promise; +export const BotCallbackHandlerKey = 'botCallbackHandler'; + // tslint:disable-next-line:no-empty-interface export interface TurnContext {} /** - * Context object containing information cached for a single turn of conversation with a user. + * Provides context for a turn of a bot. * * @remarks - * This will typically be created by the adapter you're using and then passed to middleware and - * your bots logic. + * Context provides information needed to process an incoming activity. The context object is + * created by a [BotAdapter](xref:botbuilder-core.BotAdapter) and persists for the length of the turn. */ export class TurnContext { private _adapter: BotAdapter | undefined; @@ -64,12 +125,24 @@ export class TurnContext { private _onDeleteActivity: DeleteActivityHandler[] = []; /** - * Creates a new TurnContext instance. - * @param adapterOrContext Adapter that constructed the context or a context object to clone. - * @param request Request being processed. + * Creates an new instance of the [TurnContext](xref:xref:botbuilder-core.TurnContext) class. + * + * @param adapterOrContext The adapter creating the context. + * @param request The incoming activity for the turn. */ constructor(adapterOrContext: BotAdapter, request: Partial); + /** + * Creates an new instance of the [TurnContext](xref:xref:botbuilder-core.TurnContext) class. + * + * @param adapterOrContext The context object to clone. + */ constructor(adapterOrContext: TurnContext); + /** + * Creates an new instance of the [TurnContext](xref:xref:botbuilder-core.TurnContext) class. + * + * @param adapterOrContext The adapter creating the context or the context object to clone. + * @param request Optional. The incoming activity for the turn. + */ constructor(adapterOrContext: BotAdapter|TurnContext, request?: Partial) { if (adapterOrContext instanceof TurnContext) { adapterOrContext.copyTo(this); @@ -80,58 +153,84 @@ export class TurnContext { } /** - * Rewrites the activity text without any at mention. - * Use with caution because this function is altering the text on the Activity. + * Removes at mentions for the activity's [recipient](xref:botframework-schema.Activity.recipient) + * from the text of an activity and returns the updated text. + * Use with caution; this function alters the activity's [text](xref:botframework-schema.Activity.text) property. * + * @param activity The activity to remove at mentions from. + * * @remarks - * Some channels, for example Microsoft Teams, add at mention details into the text on a message activity. - * This can interfere with later processing. This is a helper function to remove the at mention. + * Some channels, for example Microsoft Teams, add at-mention details to the text of a message activity. + * + * Use this helper method to modify the activity's [text](xref:botframework-schema.Activity.text) property. + * It removes all at mentions of the activity's [recipient](xref:botframework-schema.Activity.recipient) + * and then returns the updated property value. * + * For example: * ```JavaScript - * const updatedText = TurnContext.removeRecipientMention(context.request); + * const updatedText = TurnContext.removeRecipientMention(turnContext.request); * ``` - * @param activity The activity to alter the text on + * **See also** + * - [removeMentionText](xref:botbuilder-core.TurnContext.removeMentionText) */ public static removeRecipientMention(activity: Partial): string { return TurnContext.removeMentionText(activity, activity.recipient.id); } /** - * Rewrites the activity text without any at mention. Specifying a particular recipient id. - * Use with caution because this function is altering the text on the Activity. - * + * Removes at mentions for a given ID from the text of an activity and returns the updated text. + * Use with caution; this function alters the activity's [text](xref:botframework-schema.Activity.text) property. + * + * @param activity The activity to remove at mentions from. + * @param id The ID of the user or bot to remove at mentions for. + * * @remarks - * Some channels, for example Microsoft Teams, add at mention details into the text on a message activity. - * This can interfere with later processing. This is a helper function to remove the at mention. - * + * Some channels, for example Microsoft Teams, add at mentions to the text of a message activity. + * + * Use this helper method to modify the activity's [text](xref:botframework-schema.Activity.text) property. + * It removes all at mentions for the given bot or user ID and then returns the updated property value. + * + * For example, when you remove mentions of **echoBot** from an activity containing the text "@echoBot Hi Bot", + * the activity text is updated, and the method returns "Hi Bot". + * + * The format of a mention [entity](xref:botframework-schema.Entity) is channel-dependent. + * However, the mention's [text](xref:botframework-schema.Mention.text) property should contain + * the exact text for the user as it appears in the activity text. + * + * For example, whether the channel uses "username" or "@username", this string is in + * the activity's text, and this method will remove all occurrences of that string from the text. + * + * For example: * ```JavaScript - * const updatedText = TurnContext.removeRecipientMention(context.request); + * const updatedText = TurnContext.removeMentionText(activity, activity.recipient.id); * ``` - * @param activity The activity to alter the text on - * @param id The recipient id of the at mention + * **See also** + * - [removeRecipientMention](xref:botbuilder-core.TurnContext.removeRecipientMention) */ public static removeMentionText(activity: Partial, id: string): string { - var mentions = TurnContext.getMentions(activity); - for (var i=0; i)(.*?)(?=<\/at>)/i); - if (mentionNameMatch.length > 0) { - activity.text = activity.text.replace(mentionNameMatch[0], ''); - activity.text = activity.text.replace(/<\/at>/g, ''); - } - } + const mentions = TurnContext.getMentions(activity); + const mentionsFiltered = mentions.filter((mention): boolean => mention.mentioned.id === id); + if (mentionsFiltered.length) { + activity.text = activity.text.replace(mentionsFiltered[0].text, '').trim(); } return activity.text; } /** - * Returns the mentions on an activity. + * Gets all at-mention entities included in an activity. * + * @param activity The activity. + * + * @remarks + * The activity's [entities](xref:botframework-schema.Activity.entities) property contains a flat + * list of metadata objects pertaining to this activity and can contain + * [mention](xref:botframework-schema.Mention) entities. This method returns all such entities + * for a given activity. + * + * For example: * ```JavaScript - * const mentions = TurnContext.getMentions(context.request); + * const mentions = TurnContext.getMentions(turnContext.request); * ``` - * @param activity The activity to alter the text on - * @param id The recipient id of the at mention */ public static getMentions(activity: Partial): Mention[] { var result: Mention[] = []; @@ -146,16 +245,21 @@ export class TurnContext { } /** - * Returns the conversation reference for an activity. + * Copies conversation reference information from an activity. * + * @param activity The activity to get the information from. + * * @remarks - * This can be saved as a plain old JSON object and then later used to message the user - * proactively. + * You can save the conversation reference as a JSON object and use it later to proactively message the user. * + * For example: * ```JavaScript * const reference = TurnContext.getConversationReference(context.request); * ``` - * @param activity The activity to copy the conversation reference from + * + * **See also** + * + * - [BotAdapter.continueConversation](xref:botbuilder-core.BotAdapter.continueConversation) */ public static getConversationReference(activity: Partial): Partial { return { @@ -169,21 +273,18 @@ export class TurnContext { } /** - * Updates an activity with the delivery information from a conversation reference. - * + * Updates an activity with the delivery information from an existing conversation reference. + * + * @param activity The activity to update. + * @param reference The conversation reference to copy delivery information from. + * @param isIncoming Optional. `true` to treat the activity as an incoming activity, where the + * bot is the recipient; otherwise, `false`. Default is `false`, and the activity will show + * the bot as the sender. + * * @remarks - * Calling this after [getConversationReference()](#getconversationreference) on an incoming - * activity will properly address the reply to a received activity. - * - * ```JavaScript - * // Send a typing indicator without going through a middleware listeners. - * const reference = TurnContext.getConversationReference(context.activity); - * const activity = TurnContext.applyConversationReference({ type: 'typing' }, reference); - * await context.adapter.sendActivities([activity]); - * ``` - * @param activity Activity to copy delivery information to. - * @param reference Conversation reference containing delivery information. - * @param isIncoming (Optional) flag indicating whether the activity is an incoming or outgoing activity. Defaults to `false` indicating the activity is outgoing. + * Call the [getConversationReference](xref:botbuilder-core.TurnContext.getConversationReference) + * method on an incoming activity to get a conversation reference that you can then use + * to update an outgoing activity with the correct delivery information. */ public static applyConversationReference( activity: Partial, @@ -207,18 +308,26 @@ export class TurnContext { } /** - * Create a ConversationReference based on an outgoing Activity's ResourceResponse + * Copies conversation reference information from a resource response for a sent activity. + * + * @param activity The sent activity. + * @param reply The resource response for the activity, returned by the + * [sendActivity](xref:botbuilder-core.TurnContext.sendActivity) or + * [sendActivities](xref:botbuilder-core.TurnContext.sendActivities) method. * * @remarks - * This method can be used to create a ConversationReference that can be stored - * and used later to delete or update the activity. + * You can save the conversation reference as a JSON object and use it later to update or delete the message. + * + * For example: * ```javascript * var reply = await context.sendActivity('Hi'); * var reference = TurnContext.getReplyConversationReference(context.activity, reply); * ``` - * - * @param activity Activity from which to pull Conversation info - * @param reply ResourceResponse returned from sendActivity + * + * **See also** + * + * - [deleteActivity](xref:botbuilder-core.TurnContext.deleteActivity) + * - [updateActivity](xref:botbuilder-core.TurnContext.updateActivity) */ public static getReplyConversationReference( activity: Partial, @@ -233,18 +342,66 @@ export class TurnContext { } /** - * Sends a single activity or message to the user. + * Asynchronously sends an activity to the sender of the incoming activity. * + * @param name The activity or text to send. + * @param value Optional. The text to be spoken by your bot on a speech-enabled channel. + * @param valueType Optional. Indicates whether your bot is accepting, expecting, or ignoring user + * @param label Optional. Indicates whether your bot is accepting, expecting, or ignoring user + * * @remarks - * This ultimately calls [sendActivities()](#sendactivites) and is provided as a convenience to - * make formating and sending individual activities easier. + * Creates and sends a Trace activity. Trace activities are only sent when the channel is the emulator. * + * For example: + * ```JavaScript + * await context.sendTraceActivity(`The following exception was thrown ${msg}`); + * ``` + * + * **See also** + * + * - [sendActivities](xref:botbuilder-core.TurnContext.sendActivities) + */ + public sendTraceActivity(name: string, value?: any, valueType?: string, label?: string): Promise { + const traceActivity: Partial = { + type: ActivityTypes.Trace, + timestamp: new Date(), + name: name, + value: value, + valueType: valueType, + label: label + }; + return this.sendActivity(traceActivity) + } + + /** + * Asynchronously sends an activity to the sender of the incoming activity. + * + * @param activityOrText The activity or text to send. + * @param speak Optional. The text to be spoken by your bot on a speech-enabled channel. + * @param inputHint Optional. Indicates whether your bot is accepting, expecting, or ignoring user + * input after the message is delivered to the client. One of: 'acceptingInput', 'ignoringInput', + * or 'expectingInput'. Default is 'acceptingInput'. + * + * @remarks + * If the activity is successfully sent, results in a + * [ResourceResponse](xref:botframework-schema.ResourceResponse) object containing the ID that the + * receiving channel assigned to the activity. + * + * See the channel's documentation for limits imposed upon the contents of the **activityOrText** parameter. + * + * To control various characteristics of your bot's speech such as voice, rate, volume, pronunciation, + * and pitch, specify **speak** in Speech Synthesis Markup Language (SSML) format. + * + * For example: * ```JavaScript * await context.sendActivity(`Hello World`); * ``` - * @param activityOrText Activity or text of a message to send the user. - * @param speak (Optional) SSML that should be spoken to the user for the message. - * @param inputHint (Optional) `InputHint` for the message sent to the user. Defaults to `acceptingInput`. + * + * **See also** + * + * - [sendActivities](xref:botbuilder-core.TurnContext.sendActivities) + * - [updateActivity](xref:botbuilder-core.TurnContext.updateActivity) + * - [deleteActivity](xref:botbuilder-core.TurnContext.deleteActivity) */ public sendActivity(activityOrText: string|Partial, speak?: string, inputHint?: string): Promise { let a: Partial; @@ -261,14 +418,19 @@ export class TurnContext { } /** - * Sends a set of activities to the user. An array of responses from the server will be returned. + * Asynchronously sends a set of activities to the sender of the incoming activity. * + * @param activities The activities to send. + * * @remarks - * Prior to delivery, the activities will be updated with information from the `ConversationReference` - * for the contexts [activity](#activity) and if any activities `type` field hasn't been set it will be - * set to a type of `message`. The array of activities will then be routed through any [onSendActivities()](#onsendactivities) - * handlers before being passed to `adapter.sendActivities()`. - * + * If the activities are successfully sent, results in an array of + * [ResourceResponse](xref:botframework-schema.ResourceResponse) objects containing the IDs that + * the receiving channel assigned to the activities. + * + * Before they are sent, the delivery information of each outbound activity is updated based on the + * delivery information of the inbound inbound activity. + * + * For example: * ```JavaScript * await context.sendActivities([ * { type: 'typing' }, @@ -276,7 +438,12 @@ export class TurnContext { * { type: 'message', text: 'Hello... How are you?' } * ]); * ``` - * @param activities One or more activities to send to the user. + * + * **See also** + * + * - [sendActivity](xref:botbuilder-core.TurnContext.sendActivity) + * - [updateActivity](xref:botbuilder-core.TurnContext.updateActivity) + * - [deleteActivity](xref:botbuilder-core.TurnContext.deleteActivity) */ public sendActivities(activities: Partial[]): Promise { let sentNonTraceActivity = false; @@ -285,7 +452,7 @@ export class TurnContext { const o: Partial = TurnContext.applyConversationReference({...a}, ref); if (!o.type) { o.type = ActivityTypes.Message; } if (o.type !== ActivityTypes.Trace) { sentNonTraceActivity = true; } - + if (o.id) { delete o.id; } return o; }); @@ -301,12 +468,15 @@ export class TurnContext { } /** - * Replaces an existing activity. + * Asynchronously updates a previously sent activity. * + * @param activity The replacement for the original activity. + * * @remarks - * The activity will be routed through any registered [onUpdateActivity](#onupdateactivity) handlers - * before being passed to `adapter.updateActivity()`. - * + * The [id](xref:botframework-schema.Activity.id) of the replacement activity indicates the activity + * in the conversation to replace. + * + * For example: * ```JavaScript * const matched = /approve (.*)/i.exec(context.activity.text); * if (matched) { @@ -314,19 +484,29 @@ export class TurnContext { * await context.updateActivity(update); * } * ``` - * @param activity New replacement activity. The activity should already have it's ID information populated. + * + * **See also** + * + * - [sendActivity](xref:botbuilder-core.TurnContext.sendActivity) + * - [deleteActivity](xref:botbuilder-core.TurnContext.deleteActivity) + * - [getReplyConversationReference](xref:botbuilder-core.TurnContext.getReplyConversationReference) */ public updateActivity(activity: Partial): Promise { - return this.emit(this._onUpdateActivity, activity, () => this.adapter.updateActivity(this, activity)); + const ref: Partial = TurnContext.getConversationReference(this.activity); + const a: Partial = TurnContext.applyConversationReference(activity, ref); + return this.emit(this._onUpdateActivity, a, () => this.adapter.updateActivity(this, a)); } /** - * Deletes an existing activity. + * Asynchronously deletes a previously sent activity. * + * @param idOrReference ID or conversation reference for the activity to delete. + * * @remarks - * The `ConversationReference` for the activity being deleted will be routed through any registered - * [onDeleteActivity](#ondeleteactivity) handlers before being passed to `adapter.deleteActivity()`. + * If an ID is specified, the conversation reference for the current request is used + * to get the rest of the information needed. * + * For example: * ```JavaScript * const matched = /approve (.*)/i.exec(context.activity.text); * if (matched) { @@ -334,7 +514,12 @@ export class TurnContext { * await context.deleteActivity(savedId); * } * ``` - * @param idOrReference ID or conversation of the activity being deleted. If an ID is specified the conversation reference information from the current request will be used to delete the activity. + * + * **See also** + * + * - [sendActivity](xref:botbuilder-core.TurnContext.sendActivity) + * - [updateActivity](xref:botbuilder-core.TurnContext.updateActivity) + * - [getReplyConversationReference](xref:botbuilder-core.TurnContext.getReplyConversationReference) */ public deleteActivity(idOrReference: string|Partial): Promise { let reference: Partial; @@ -349,21 +534,29 @@ export class TurnContext { } /** - * Registers a handler to be notified of, and potentially intercept, the sending of activities. + * Adds a response handler for send activity operations. * + * @param handler The handler to add to the context object. + * * @remarks - * This example shows how to listen for and logs outgoing `message` activities. + * This method returns a reference to the turn context object. + * + * When the [sendActivity](xref:botbuilder-core.TurnContext.sendActivity) or + * [sendActivities](xref:botbuilder-core.TurnContext.sendActivities) method is called, + * the registered handlers are called in the order in which they were added to the context object + * before the activities are sent. + * + * This example shows how to listen for and log outgoing `message` activities. * * ```JavaScript * context.onSendActivities(async (ctx, activities, next) => { - * // Deliver activities - * await next(); - * - * // Log sent messages + * // Log activities before sending them. * activities.filter(a => a.type === 'message').forEach(a => logSend(a)); + * + * // Allow the send process to continue. + * next(); * }); * ``` - * @param handler A function that will be called anytime [sendActivity()](#sendactivity) is called. The handler should call `next()` to continue sending of the activities. */ public onSendActivities(handler: SendActivitiesHandler): this { this._onSendActivities.push(handler); @@ -372,10 +565,18 @@ export class TurnContext { } /** - * Registers a handler to be notified of, and potentially intercept, an activity being updated. + * Adds a response handler for update activity operations. * + * @param handler The handler to add to the context object. + * * @remarks - * This example shows how to listen for and logs updated activities. + * This method returns a reference to the turn context object. + * + * When the [updateActivity](xref:botbuilder-core.TurnContext.updateActivity) method is called, + * the registered handlers are called in the order in which they were added to the context object + * before the activity is updated. + * + * This example shows how to listen for and log activity updates. * * ```JavaScript * context.onUpdateActivity(async (ctx, activity, next) => { @@ -386,7 +587,6 @@ export class TurnContext { * logUpdate(activity); * }); * ``` - * @param handler A function that will be called anytime [updateActivity()](#updateactivity) is called. The handler should call `next()` to continue sending of the replacement activity. */ public onUpdateActivity(handler: UpdateActivityHandler): this { this._onUpdateActivity.push(handler); @@ -395,10 +595,18 @@ export class TurnContext { } /** - * Registers a handler to be notified of, and potentially intercept, an activity being deleted. + * Adds a response handler for delete activity operations. * + * @param handler The handler to add to the context object. + * * @remarks - * This example shows how to listen for and logs deleted activities. + * This method returns a reference to the turn context object. + * + * When the [deleteActivity](xref:botbuilder-core.TurnContext.deleteActivity) method is called, + * the registered handlers are called in the order in which they were added to the context object + * before the activity is deleted. + * + * This example shows how to listen for and log activity deletions. * * ```JavaScript * context.onDeleteActivity(async (ctx, reference, next) => { @@ -409,7 +617,6 @@ export class TurnContext { * logDelete(activity); * }); * ``` - * @param handler A function that will be called anytime [deleteActivity()](#deleteactivity) is called. The handler should call `next()` to continue deletion of the activity. */ public onDeleteActivity(handler: DeleteActivityHandler): this { this._onDeleteActivity.push(handler); @@ -418,15 +625,18 @@ export class TurnContext { } /** - * Called when this TurnContext instance is passed into the constructor of a new TurnContext - * instance. + * Called when this turn context object is passed into the constructor for a new turn context. * + * @param context The new turn context object. + * * @remarks - * Can be overridden in derived classes to add additional fields that should be cloned. - * @param context The context object to copy private members to. Everything should be copied by reference. + * This copies private members from this object to the new object. + * All property values are copied by reference. + * + * Override this in a derived class to copy any additional members, as necessary. */ protected copyTo(context: TurnContext): void { - // Copy private member to other instance. + // Copy private members to other instance. [ '_adapter', '_activity', '_respondedRef', '_services', '_onSendActivities', '_onUpdateActivity', '_onDeleteActivity' @@ -434,27 +644,14 @@ export class TurnContext { } /** - * The adapter for this context. - * - * @remarks - * This example shows how to send a `typing` activity directly using the adapter. This approach - * bypasses any middleware which sometimes has its advantages. The calls to - * `getConversationReference()` and `applyConversationReference()` are needed to ensure that the - * outgoing activity is properly addressed: - * - * ```JavaScript - * // Send a typing indicator without going through an middleware listeners. - * const reference = TurnContext.getConversationReference(context.activity); - * const activity = TurnContext.applyConversationReference({ type: 'typing' }, reference); - * await context.adapter.sendActivities([activity]); - * ``` + * Gets the bot adapter that created this context object. */ public get adapter(): BotAdapter { return this._adapter as BotAdapter; } /** - * The received activity. + * Gets the activity associated with this turn. * * @remarks * This example shows how to get the users trimmed utterance from the activity: @@ -468,11 +665,15 @@ export class TurnContext { } /** - * If `true` at least one response has been sent for the current turn of conversation. + * Indicates whether the bot has replied to the user this turn. * * @remarks - * This is primarily useful for determining if a bot should run fallback routing logic: + * **true** if at least one response was sent for the current turn; otherwise, **false**. + * Use this to determine if your bot needs to run fallback logic after other normal processing. + * + * Trace activities do not set this flag. * + * for example: * ```JavaScript * await routeActivity(context); * if (!context.responded) { @@ -484,27 +685,38 @@ export class TurnContext { return this._respondedRef.responded; } + /** + * Sets the response flag on the current turn context. + * + * @remarks + * The [sendActivity](xref:botbuilder-core.TurnContext.sendActivity) and + * [sendActivities](xref:botbuilder-core.TurnContext.sendActivities) methods call this method to + * update the responded flag. You can call this method directly to indicate that your bot has + * responded appropriately to the incoming activity. + */ public set responded(value: boolean) { if (!value) { throw new Error(`TurnContext: cannot set 'responded' to a value of 'false'.`); } this._respondedRef.responded = true; } /** - * Map of services and other values cached for the lifetime of the turn. + * Gets the services registered on this context object. * * @remarks * Middleware, other components, and services will typically use this to cache information - * that could be asked for by a bot multiple times during a turn. The bots logic is free to - * use this to pass information between its own components. + * that could be asked for by a bot multiple times during a turn. You can use this cache to + * pass information between components of your bot. * + * For example: * ```JavaScript + * const cartKey = Symbol(); * const cart = await loadUsersShoppingCart(context); - * context.turnState.set('cart', cart); + * context.turnState.set(cartKey, cart); * ``` * * > [!TIP] - * > For middleware and third party components, consider using a `Symbol()` for your cache key - * > to avoid potential naming collisions with the bots caching and other components. + * > When creating middleware or a third-party component, use a unique symbol for your cache key + * > to avoid state naming collisions with the bot or other middleware or components. */ public get turnState(): Map { return this._turnState; @@ -531,5 +743,4 @@ export class TurnContext { return emitNext(0); } - } diff --git a/libraries/botbuilder-core/src/userTokenSettings.ts b/libraries/botbuilder-core/src/userTokenSettings.ts new file mode 100644 index 0000000000..64fb7c9143 --- /dev/null +++ b/libraries/botbuilder-core/src/userTokenSettings.ts @@ -0,0 +1,42 @@ +/** + * @module botbuilder + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Provides details for token polling. + */ +export interface TokenPollingSettings +{ + /** + * Polling timeout time in milliseconds. This is equivalent to login flow timeout. + */ + timeout?: number; + + /** + * Time Interval in milliseconds between token polling requests. + */ + interval?: number; +} + +/** + * TurnState key for the OAuth login timeout + */ +export const OAuthLoginTimeoutKey: string = 'loginTimeout'; + +/** + * Name of the token polling settings key. + */ +export const TokenPollingSettingsKey: string = "tokenPollingSettings"; + + +/** + * Default amount of time an OAuthCard will remain active (clickable and actively waiting for a token). + * After this time: + * (1) the OAuthCard will not allow the user to click on it. + * (2) any polling triggered by the OAuthCard will stop. + */ + export const OAuthLoginTimeoutMsValue: number = 900000; \ No newline at end of file diff --git a/libraries/botbuilder-core/tests/ActivityHandler.test.js b/libraries/botbuilder-core/tests/ActivityHandler.test.js index baac7f4763..4f6eb4dca3 100644 --- a/libraries/botbuilder-core/tests/ActivityHandler.test.js +++ b/libraries/botbuilder-core/tests/ActivityHandler.test.js @@ -5,9 +5,21 @@ describe('ActivityHandler', function() { const adapter = new TestAdapter(); - async function processActivity(activity, bot) { + async function processActivity(activity, bot, done) { + if (!activity) { + throw new Error('Missing activity'); + } + + if (!bot) { + throw new Error('Missing bot'); + } + + if (!done) { + throw new Error('Missing done'); + } const context = new TurnContext(adapter, activity); - await bot.run(context); + // Adding the catch with `done(error)` makes sure that the correct error is surfaced + await bot.run(context).catch(error => done(error)); } it(`should fire onTurn for any inbound activity`, async function (done) { @@ -20,7 +32,7 @@ describe('ActivityHandler', function() { await next(); }); - processActivity({type: 'any'}, bot); + processActivity({type: 'any'}, bot, done); }); it(`should fire onMessage for any message activities`, async function (done) { @@ -33,7 +45,7 @@ describe('ActivityHandler', function() { await next(); }); - processActivity({type: 'message'}, bot); + processActivity({type: 'message'}, bot, done); }); it(`calling next allows following events to firing`, async function (done) { @@ -51,7 +63,7 @@ describe('ActivityHandler', function() { await next(); }); - processActivity({type: 'message'}, bot); + processActivity({type: 'message'}, bot, done); }); it(`omitting call to next prevents following events from firing`, async function (done) { @@ -68,7 +80,7 @@ describe('ActivityHandler', function() { await next(); }); - processActivity({type: 'message'}, bot); + processActivity({type: 'message'}, bot, done); }); it(`binding 2 methods to the same event both fire`, async function (done) { @@ -91,7 +103,7 @@ describe('ActivityHandler', function() { await next(); }); - processActivity({type: 'message'}, bot); + processActivity({type: 'message'}, bot, done); }); it(`should fire onConversationUpdate`, async function (done) { @@ -104,7 +116,7 @@ describe('ActivityHandler', function() { await next(); }); - processActivity({type: ActivityTypes.ConversationUpdate}, bot); + processActivity({type: ActivityTypes.ConversationUpdate}, bot, done); }); it(`should fire onMembersAdded`, async function (done) { @@ -117,7 +129,7 @@ describe('ActivityHandler', function() { await next(); }); - processActivity({type: ActivityTypes.ConversationUpdate, membersAdded: [{id: 1}]}, bot); + processActivity({type: ActivityTypes.ConversationUpdate, membersAdded: [{id: 1}]}, bot, done); }); it(`should fire onMembersRemoved`, async function (done) { @@ -130,7 +142,7 @@ describe('ActivityHandler', function() { await next(); }); - processActivity({type: ActivityTypes.ConversationUpdate, membersRemoved: [{id: 1}]}, bot); + processActivity({type: ActivityTypes.ConversationUpdate, membersRemoved: [{id: 1}]}, bot, done); }); it(`should fire onMessageReaction`, async function (done) { @@ -143,7 +155,7 @@ describe('ActivityHandler', function() { await next(); }); - processActivity({type: ActivityTypes.MessageReaction}, bot); + processActivity({type: ActivityTypes.MessageReaction}, bot, done); }); it(`should fire onReactionsAdded`, async function (done) { @@ -156,7 +168,7 @@ describe('ActivityHandler', function() { await next(); }); - processActivity({type: ActivityTypes.MessageReaction, reactionsAdded: [{type: 'like'}]}, bot); + processActivity({type: ActivityTypes.MessageReaction, reactionsAdded: [{type: 'like'}]}, bot, done); }); it(`should fire onReactionsRemoved`, async function (done) { @@ -169,7 +181,7 @@ describe('ActivityHandler', function() { await next(); }); - processActivity({type: ActivityTypes.MessageReaction, reactionsRemoved: [{type: 'like'}]}, bot); + processActivity({type: ActivityTypes.MessageReaction, reactionsRemoved: [{type: 'like'}]}, bot, done); }); it(`should fire onEvent`, async function (done) { @@ -182,7 +194,7 @@ describe('ActivityHandler', function() { await next(); }); - processActivity({type: ActivityTypes.Event}, bot); + processActivity({type: ActivityTypes.Event}, bot, done); }); @@ -196,7 +208,7 @@ describe('ActivityHandler', function() { await next(); }); - processActivity({type: 'foo'}, bot); + processActivity({type: 'foo'}, bot, done); }); it(`should fire onDialog`, async function (done) { @@ -209,7 +221,243 @@ describe('ActivityHandler', function() { await next(); }); - processActivity({type: 'foo'}, bot); + processActivity({type: 'foo'}, bot, done); + }); + + describe('should by default', () => { + let onTurnCalled = false; + let onMessageCalled = false; + let onConversationUpdateCalled = false; + let onMembersAddedCalled = false; + let onMembersRemovedCalled = false; + let onMessageReactionCalled = false; + let onReactionsAddedCalled = false; + let onReactionsRemovedCalled = false; + let onEventCalled = false; + let onTokenResponseEventCalled = false; + let onUnrecognizedActivityTypeCalled = false; + let onDialogCalled = false; + + afterEach(function() { + onTurnCalled = false; + onMessageCalled = false; + onConversationUpdateCalled = false; + onMembersAddedCalled = false; + onMembersRemovedCalled = false; + onMessageReactionCalled = false; + onReactionsAddedCalled = false; + onReactionsRemovedCalled = false; + onEventCalled = false; + onTokenResponseEventCalled = false; + onUnrecognizedActivityTypeCalled = false; + onDialogCalled = false; + }); + + function assertContextAndNext(context, next) { + assert(context, 'context not found'); + assert(next, 'next not found'); + } + + function assertFalseFlag(flag, ...args) { + assert(!flag, `${args[0]}Called should not be true before the ${args.join(', ')} handlers are called.`); + } + + function assertTrueFlag(flag, ...args) { + assert(flag, `${args[0]}Called should be true after the ${args[0]} handlers are called.`); + } + + it('call "onTurn" handlers then dispatch by Activity Type "Message"', (done) => { + const bot = new ActivityHandler(); + bot.onTurn(async (context, next) => { + assertContextAndNext(context, next); + assertFalseFlag(onTurnCalled, 'onTurn'); + onTurnCalled = true; + assertFalseFlag(onConversationUpdateCalled, 'onMessage', 'onTurn'); + await next(); + }); + + bot.onMessage(async (context, next) => { + assertContextAndNext(context, next); + assertTrueFlag(onTurnCalled, 'onTurn'); + assertFalseFlag(onConversationUpdateCalled, 'onMessage', 'onTurn'); + assert(!onMessageCalled, 'onMessage should not be true before onTurn and onMessage handlers complete.'); + onMessageCalled = true; + await next(); + }); + + processActivity({type: ActivityTypes.Message}, bot, done); + assertTrueFlag(onTurnCalled, 'onTurn'); + assertTrueFlag(onMessageCalled, 'onMessage'); + done(); + }); + + it('call "onTurn" handlers then dispatch by Activity Type "ConversationUpdate"', (done) => { + const bot = new ActivityHandler(); + bot.onTurn(async (context, next) => { + assertContextAndNext(context, next); + assertFalseFlag(onTurnCalled, 'onTurn'); + onTurnCalled = true; + assertFalseFlag(onConversationUpdateCalled, 'onConversationUpdate', 'onTurn'); + await next(); + }); + + bot.onConversationUpdate(async (context, next) => { + assertContextAndNext(context, next); + assertTrueFlag(onTurnCalled, 'onTurn'); + assertFalseFlag(onConversationUpdateCalled, 'onConversationUpdate', 'onTurn'); + onConversationUpdateCalled = true; + await next(); + }); + + processActivity({type: ActivityTypes.ConversationUpdate}, bot, done); + assertTrueFlag(onTurnCalled, 'onTurn'); + assertTrueFlag(onConversationUpdateCalled, 'onConversationUpdate'); + done(); + }); + + it('call "onTurn" handlers then dispatch by Activity Type "ConversationUpdate"-subtype "MembersAdded"', (done) => { + const bot = new ActivityHandler(); + bot.onTurn(async (context, next) => { + assertContextAndNext(context, next); + assertFalseFlag(onTurnCalled, 'onTurn'); + onTurnCalled = true; + assertFalseFlag(onMembersAddedCalled, 'onMembersAdded', 'onTurn'); + await next(); + }); + + bot.onMembersAdded(async (context, next) => { + assertContextAndNext(context, next); + assertTrueFlag(onTurnCalled, 'onTurn'); + assertFalseFlag(onMembersAddedCalled, 'onMembersAdded', 'onTurn'); + onMembersAddedCalled = true; + await next(); + }); + + processActivity({type: ActivityTypes.ConversationUpdate, membersAdded: [{id: 1}]}, bot, done); + assertTrueFlag(onTurnCalled, 'onTurn', 'onMembersAdded'); + assertTrueFlag(onMembersAddedCalled, 'onMembersAdded', 'onTurn'); + done(); + }); + + it('call "onTurn" handlers then dispatch by Activity Type "ConversationUpdate"-subtype "MembersRemoved"', (done) => { + const bot = new ActivityHandler(); + bot.onTurn(async (context, next) => { + assertContextAndNext(context, next); + assertFalseFlag(onTurnCalled, 'onTurn'); + onTurnCalled = true; + assertFalseFlag(onMembersRemovedCalled, 'onMembersRemoved', 'onTurn'); + await next(); + }); + + bot.onMembersRemoved(async (context, next) => { + assertContextAndNext(context, next); + assertTrueFlag(onTurnCalled, 'onTurn'); + assertFalseFlag(onMembersRemovedCalled, 'onMembersRemoved', 'onTurn'); + onMembersRemovedCalled = true; + await next(); + }); + + processActivity({type: ActivityTypes.ConversationUpdate, membersRemoved: [{id: 1}]}, bot, done); + assertTrueFlag(onTurnCalled, 'onTurn', 'onMembersRemoved'); + assertTrueFlag(onMembersRemovedCalled, 'onMembersRemoved', 'onTurn'); + done(); + }); + + it('call "onTurn" handlers then dispatch by Activity Type "MessageReaction"', (done) => { + const bot = new ActivityHandler(); + bot.onTurn(async (context, next) => { + assertContextAndNext(context, next); + assertFalseFlag(onTurnCalled, 'onTurn'); + onTurnCalled = true; + assertFalseFlag(onMessageReactionCalled, 'onMessageReaction', 'onTurn'); + await next(); + }); + + bot.onMessageReaction(async (context, next) => { + assertContextAndNext(context, next); + assertTrueFlag(onTurnCalled, 'onTurn'); + assertFalseFlag(onMessageReactionCalled, 'onMessageReaction', 'onTurn'); + onMessageReactionCalled = true; + await next(); + }); + + processActivity({type: ActivityTypes.MessageReaction, reactionsRemoved: [{type: 'like'}]}, bot, done); + assertTrueFlag(onTurnCalled, 'onTurn', 'onMembersAdded'); + assertTrueFlag(onMessageReactionCalled, 'onMessageReaction', 'onTurn'); + done(); + }); + + it('call "onTurn" handlers then dispatch by Activity Type "MessageReaction"-subtype "ReactionsAdded"', (done) => { + const bot = new ActivityHandler(); + bot.onTurn(async (context, next) => { + assertContextAndNext(context, next); + assertFalseFlag(onTurnCalled, 'onTurn'); + onTurnCalled = true; + assertFalseFlag(onMessageReactionCalled, 'onMessageReaction', 'onTurn'); + assertFalseFlag(onReactionsRemovedCalled, 'onReactionsRemoved', 'onMessageReaction', 'onTurn'); + await next(); + }); + + bot.onMessageReaction(async (context, next) => { + assertContextAndNext(context, next); + assertTrueFlag(onTurnCalled, 'onTurn'); + assertFalseFlag(onMessageReactionCalled, 'onMessageReaction', 'onTurn'); + onMessageReactionCalled = true; + assertFalseFlag(onReactionsRemovedCalled, 'onReactionsRemoved', 'onMessageReaction', 'onTurn'); + await next(); + }); + + bot.onReactionsAdded(async (context, next) => { + assertContextAndNext(context, next); + assertTrueFlag(onTurnCalled, 'onTurn'); + assertTrueFlag(onMessageReactionCalled, 'onMessageReaction', 'onTurn'); + assertFalseFlag(onReactionsAddedCalled, 'onReactionsAdded', 'onMessageReaction', 'onTurn'); + onReactionsAddedCalled = true; + await next(); + }); + + processActivity({type: ActivityTypes.MessageReaction, reactionsAdded: [{type: 'like'}]}, bot, done); + assertTrueFlag(onTurnCalled, 'onTurn', 'onMembersAdded'); + assertTrueFlag(onMessageReactionCalled, 'onMessageReaction'); + assertTrueFlag(onReactionsAddedCalled, 'onReactionsAdded', 'onMessageReaction', 'onTurn'); + done(); + }); + + it('call "onTurn" handlers then dispatch by Activity Type "MessageReaction"-subtype "ReactionsRemoved"', (done) => { + const bot = new ActivityHandler(); + bot.onTurn(async (context, next) => { + assertContextAndNext(context, next); + assertFalseFlag(onTurnCalled, 'onTurn'); + onTurnCalled = true; + assertFalseFlag(onMessageReactionCalled, 'onMessageReaction', 'onTurn'); + assertFalseFlag(onReactionsRemovedCalled, 'onReactionsRemoved', 'onMessageReaction', 'onTurn'); + await next(); + }); + + bot.onMessageReaction(async (context, next) => { + assertContextAndNext(context, next); + assertTrueFlag(onTurnCalled, 'onTurn'); + assertFalseFlag(onMessageReactionCalled, 'onMessageReaction', 'onTurn'); + onMessageReactionCalled = true; + assertFalseFlag(onReactionsRemovedCalled, 'onReactionsRemoved', 'onMessageReaction', 'onTurn'); + await next(); + }); + + bot.onReactionsRemoved(async (context, next) => { + assertContextAndNext(context, next); + assertTrueFlag(onTurnCalled, 'onTurn'); + assertTrueFlag(onMessageReactionCalled, 'onMessageReaction', 'onTurn'); + assertFalseFlag(onReactionsRemovedCalled, 'onReactionsRemoved', 'onMessageReaction', 'onTurn'); + onReactionsRemovedCalled = true; + await next(); + }); + + processActivity({type: ActivityTypes.MessageReaction, reactionsRemoved: [{type: 'like'}]}, bot, done); + assertTrueFlag(onTurnCalled, 'onTurn', 'onMembersAdded'); + assertTrueFlag(onMessageReactionCalled, 'onMessageReaction'); + assertTrueFlag(onReactionsRemovedCalled, 'onReactionsRemoved'); + done(); + }); }); }); \ No newline at end of file diff --git a/libraries/botbuilder-core/tests/activityHandlerBase.test.js b/libraries/botbuilder-core/tests/activityHandlerBase.test.js new file mode 100644 index 0000000000..f6fd5a86dc --- /dev/null +++ b/libraries/botbuilder-core/tests/activityHandlerBase.test.js @@ -0,0 +1,304 @@ +const assert = require('assert'); +const { ActivityHandlerBase, ActivityTypes, TurnContext, TestAdapter } = require('../lib'); + +describe('ActivityHandlerBase', function() { + + const adapter = new TestAdapter(); + + async function processActivity(activity, bot, done) { + if (!activity) { + throw new Error('Missing activity'); + } + + if (!bot) { + throw new Error('Missing bot'); + } + + if (!done) { + throw new Error('Missing done'); + } + const context = new TurnContext(adapter, activity); + // Adding the catch with `done(error)` makes sure that the correct error is surfaced + await bot.run(context).catch(error => done(error)); + } + + let onTurnActivityCalled = false; + let onMessageCalled = false; + let onConversationUpdateActivityCalled = false; + let onMessageReactionCalled = false; + let onEventCalled = false; + let onUnrecognizedActivity = false; + + afterEach(function() { + onTurnActivityCalled = false; + onMessageCalled = false; + onConversationUpdateActivityCalled = false; + onMessageReactionCalled = false; + onEventCalled = false; + onUnrecognizedActivity = false; + }); + + it('should throw an error if context is not passed in', done => { + const bot = new ActivityHandlerBase(); + + bot.run().catch(error => { + if (error.message !== 'Missing TurnContext parameter') { + done(error); + } else { + done(); + } + }); + }); + + it('should throw an error if context.activity is falsey', done => { + const bot = new ActivityHandlerBase(); + + bot.run({}).catch(error => { + if (error.message !== 'TurnContext does not include an activity') { + done(error); + } else { + done(); + } + }); + }); + + it('should throw an error if context.activity.type is falsey', done => { + const bot = new ActivityHandlerBase(); + + bot.run({ activity: {} }).catch(error => { + if (error.message !== `Activity is missing it's type`) { + done(error); + } else { + done(); + } + }); + }); + + class OverrideOnTurnActivity extends ActivityHandlerBase { + async onTurnActivity(context) { + assert(context, 'context not found'); + super.onTurnActivity(context); + } + } + it('should call onActivity from run()', done => { + const bot = new OverrideOnTurnActivity(); + processActivity({ type: 'any' }, bot, done); + done(); + }); + + class UpdatedActivityHandler extends ActivityHandlerBase { + async onTurnActivity(context) { + assert(context, 'context not found'); + onTurnActivityCalled = true; + super.onTurnActivity(context); + } + + async onMessageActivity(context) { + assert(context, 'context not found'); + onMessageCalled = true; + } + + async onConversationUpdateActivity(context) { + assert(context, 'context not found'); + onConversationUpdateActivityCalled = true; + } + + async onMessageReactionActivity(context) { + assert(context, 'context not found'); + onMessageReactionCalled = true; + } + + async onEventActivity(context) { + assert(context, 'context not found'); + onEventCalled = true; + } + + async onUnrecognizedActivity(context) { + assert(context, 'context not found'); + onUnrecognizedActivity = true; + } + } + + it('should dispatch by ActivityType in onTurnActivity()', done => { + const bot = new UpdatedActivityHandler(); + + processActivity({ type: ActivityTypes.Message }, bot, done); + processActivity({type: ActivityTypes.ConversationUpdate}, bot, done); + processActivity({type: ActivityTypes.MessageReaction}, bot, done); + processActivity({type: ActivityTypes.Event}, bot, done); + processActivity({ type: 'unrecognized' }, bot, done); + + assert(onTurnActivityCalled, 'onTurnActivity was not called'); + assert(onMessageCalled, 'onMessageActivity was not called'); + assert(onConversationUpdateActivityCalled, 'onConversationUpdateActivity was not called'); + assert(onMessageReactionCalled, 'onMessageReactionActivity was not called'); + assert(onEventCalled, 'onEventActivity was not called'); + assert(onUnrecognizedActivity, 'onUnrecognizedActivity was not called'); + done(); + }); + + describe('onConversationUpdateActivity', () => { + class ConversationUpdateActivityHandler extends ActivityHandlerBase { + async onTurnActivity(context) { + assert(context, 'context not found'); + onTurnActivityCalled = true; + super.onTurnActivity(context); + } + + async onConversationUpdateActivity(context) { + assert(context, 'context not found'); + onConversationUpdateActivityCalled = true; + super.onConversationUpdateActivity(context); + } + + async onMembersAddedActivity(membersAdded, context) { + const value = context.activity.value; + if (value && value.skipSubtype) { + throw new Error('should not have reached onMembersAddedActivity'); + } + assert(context, 'context not found'); + assert(membersAdded, 'membersAdded not found'); + assert(membersAdded.length === 1, `unexpected number of membersAdded: ${membersAdded.length}`); + onMembersAddedActivityCalled = true; + } + + async onMembersRemovedActivity(membersRemoved, context) { + const value = context.activity.value; + if (value && value.skipSubtype) { + throw new Error('should not have reached onMembersRemovedActivity'); + } + assert(context, 'context not found'); + assert(membersRemoved, 'membersRemoved not found'); + assert(membersRemoved.length === 1, `unexpected number of membersRemoved: ${membersRemoved.length}`); + onMembersRemovedActivityCalled = true; + } + } + + let onTurnActivityCalled = false; + let onConversationUpdateActivityCalled = false; + let onMembersAddedActivityCalled = false; + let onMembersRemovedActivityCalled = false; + + afterEach(function() { + onTurnActivityCalled = false; + onConversationUpdateActivityCalled = false; + onMembersAddedActivityCalled = false; + onMembersRemovedActivityCalled = false; + }); + + function createConvUpdateActivity(recipientId, AddedOrRemoved, skipSubtype) { + const recipient = { id: recipientId }; + const activity = { type: ActivityTypes.ConversationUpdate, recipient, value: { }, ...AddedOrRemoved }; + if (skipSubtype) { + activity.value.skipSubtype = true; + } + return activity; + } + + it(`should call onMembersAddedActivity if the id of the member added does not match the recipient's id`, done => { + const bot = new ConversationUpdateActivityHandler(); + const activity = createConvUpdateActivity('bot', { membersAdded: [ { id: 'user' } ] }); + processActivity(activity, bot, done); + assert(onTurnActivityCalled, 'onTurnActivity was not called'); + assert(onConversationUpdateActivityCalled, 'onConversationUpdateActivity was not called'); + assert(onMembersAddedActivityCalled, 'onMembersAddedActivity was not called'); + done(); + }); + + it(`should call onMembersRemovedActivity if the id of the member removed does not match the recipient's id`, done => { + const bot = new ConversationUpdateActivityHandler(); + const activity = createConvUpdateActivity('bot', { membersRemoved: [ { id: 'user' } ] }); + processActivity(activity, bot, done); + assert(onTurnActivityCalled, 'onTurnActivity was not called'); + assert(onConversationUpdateActivityCalled, 'onConversationUpdateActivity was not called'); + assert(onMembersRemovedActivityCalled, 'onMembersRemovedActivity was not called'); + done(); + }); + + it(`should not call onMembersAddedActivity if the id of the member added matches the recipient's id`, done => { + const bot = new ConversationUpdateActivityHandler(); + const activity = createConvUpdateActivity('bot', { membersAdded: [ { id: 'bot' } ] }, true); + processActivity(activity, bot, done); + assert(onTurnActivityCalled, 'onTurnActivity was not called'); + assert(onConversationUpdateActivityCalled, 'onConversationUpdateActivity was not called'); + done(); + }); + + it(`should not call onMembersRemovedActivity if the id of the member removed matches the recipient's id`, done => { + const bot = new ConversationUpdateActivityHandler(); + const activity = createConvUpdateActivity('bot', { membersRemoved: [ { id: 'bot' } ] }, true); + processActivity(activity, bot, done); + assert(onTurnActivityCalled, 'onTurnActivity was not called'); + assert(onConversationUpdateActivityCalled, 'onConversationUpdateActivity was not called'); + done(); + }); + }); + + describe('onMessageReaction', () => { + class MessageReactionActivityHandler extends ActivityHandlerBase { + async onTurnActivity(context) { + assert(context, 'context not found'); + onTurnActivityCalled = true; + super.onTurnActivity(context); + } + + async onMessageReactionActivity(context) { + assert(context, 'context not found'); + onMessageReactionActivityCalled = true; + super.onMessageReactionActivity(context); + } + + async onReactionsAddedActivity(reactionsAdded, context) { + assert(context, 'context not found'); + assert(reactionsAdded, 'membersAdded not found'); + assert(reactionsAdded.length === 1, `unexpected number of reactionsAdded: ${reactionsAdded.length}`); + onReactionsAddedActivityCalled = true; + } + + async onReactionsRemovedActivity(reactionsRemoved, context) { + assert(context, 'context not found'); + assert(reactionsRemoved, 'reactionsRemoved not found'); + assert(reactionsRemoved.length === 1, `unexpected number of reactionsRemoved: ${reactionsRemoved.length}`); + onReactionsRemovedActivityCalled = true; + } + } + + let onTurnActivityCalled = false; + let onMessageReactionActivityCalled = false; + let onReactionsAddedActivityCalled = false; + let onReactionsRemovedActivityCalled = false; + + afterEach(function() { + onTurnActivityCalled = false; + onMessageReactionActivityCalled = false; + onReactionsAddedActivityCalled = false; + onReactionsRemovedActivityCalled = false; + }); + + function createMsgReactActivity(recipientId, AddedOrRemoved) { + const recipient = { id: recipientId }; + const activity = { type: ActivityTypes.MessageReaction, recipient, ...AddedOrRemoved }; + return activity; + } + + it(`should call onReactionsAddedActivity if reactionsAdded exists and reactionsAdded.length > 0`, done => { + const bot = new MessageReactionActivityHandler(); + const activity = createMsgReactActivity('bot', { reactionsAdded: [ { type: 'like' } ] }); + processActivity(activity, bot, done); + assert(onTurnActivityCalled, 'onTurnActivity was not called'); + assert(onMessageReactionActivityCalled, 'onMessageReactionActivity was not called'); + assert(onReactionsAddedActivityCalled, 'onReactionsAddedActivity was not called'); + done(); + }); + + it(`should call onReactionsRemovedActivity if reactionsRemoved exists and reactionsRemoved.length > 0`, done => { + const bot = new MessageReactionActivityHandler(); + const activity = createMsgReactActivity('bot', { reactionsRemoved: [ { type: 'like' } ] }); + processActivity(activity, bot, done); + assert(onTurnActivityCalled, 'onTurnActivity was not called'); + assert(onMessageReactionActivityCalled, 'onMessageReactionActivity was not called'); + assert(onReactionsRemovedActivityCalled, 'onReactionsRemovedActivity was not called'); + done(); + }); + }); +}); \ No newline at end of file diff --git a/libraries/botbuilder-core/tests/cardFactory.test.js b/libraries/botbuilder-core/tests/cardFactory.test.js index 047aab30c3..12126bdc38 100644 --- a/libraries/botbuilder-core/tests/cardFactory.test.js +++ b/libraries/botbuilder-core/tests/cardFactory.test.js @@ -343,4 +343,27 @@ describe(`CardFactory`, function () { assert(content.text === undefined, `text should not exist.`); assert(content.connectionName === 'ConnectionName', `wrong connectionName.`); }); + + it(`should create an o365ConnectorCard.`, function () { + const attachment = CardFactory.o365ConnectorCard( + { + "title": "card title", + "text": "card text", + "summary": "O365 card summary", + "themeColor": "#E67A9E", + "sections": [ + { + "title": "**section title**", + "text": "section text", + "activityTitle": "activity title", + } + ] + } + ); + assertAttachment(attachment, CardFactory.contentTypes.o365ConnectorCard); + const content = attachment.content; + assert(content.title === 'card title', `wrong title.`); + assert(content.sections.length === 1, `wrong sections count.`); + assert(content.potentialAction === undefined, `potentialAction should not exist.`); + }); }); diff --git a/libraries/botbuilder-core/tests/mention.test.js b/libraries/botbuilder-core/tests/mention.test.js new file mode 100644 index 0000000000..510fcb7c17 --- /dev/null +++ b/libraries/botbuilder-core/tests/mention.test.js @@ -0,0 +1,119 @@ +const assert = require('assert'); +const { MessageFactory, SkypeMentionNormalizeMiddleware, TurnContext } = require('../lib'); + +describe(`Mention`, function () { + this.timeout(5000); + + it('should not change activity text when entity type is not a mention', async function() { + const mentionJson = '{\"mentioned\": {\"id\": \"recipientid\"},\"text\": \"botname\"}'; + const entity = JSON.parse(mentionJson); + entity.type = 'test'; + + const activity = MessageFactory.text('botname sometext'); + activity.channelId = 'skype'; + activity.entities = [entity]; + + SkypeMentionNormalizeMiddleware.normalizeSkypeMentionText(activity); + TurnContext.removeMentionText(activity, 'recipientid'); + assert(activity.text === 'botname sometext'); + }); + + it('should not change activity text when mention text is empty', async function() { + const mentionJson = '{\"mentioned\": {\"id\": \"recipientid\"},\"text\": \""}'; + const entity = JSON.parse(mentionJson); + entity.type = 'mention'; + + const activity = MessageFactory.text('botname sometext'); + activity.channelId = 'skype'; + activity.entities = [entity]; + + SkypeMentionNormalizeMiddleware.normalizeSkypeMentionText(activity); + TurnContext.removeMentionText(activity, 'recipientid'); + assert(activity.text === 'botname sometext'); + }); + + it('should not change activity text when there is no matching mention', async function() { + const mentionJson = '{\"mentioned\": {\"id\": \"foo bar"},\"text\": \""}'; + const entity = JSON.parse(mentionJson); + entity.type = 'mention'; + + const activity = MessageFactory.text('botname sometext'); + activity.channelId = 'skype'; + activity.entities = [entity]; + + SkypeMentionNormalizeMiddleware.normalizeSkypeMentionText(activity); + TurnContext.removeMentionText(activity, 'recipientid'); + assert(activity.text === 'botname sometext'); + }); + + it(`should remove skype mention`, async function () { + const mentionJson = '{\"mentioned\": {\"id\": \"recipientid\"},\"text\": \"botname\"}'; + const entity = JSON.parse(mentionJson); + entity.type = 'mention'; + + const activity = MessageFactory.text('botname sometext'); + activity.channelId = 'skype'; + activity.entities = [entity]; + + SkypeMentionNormalizeMiddleware.normalizeSkypeMentionText(activity); + TurnContext.removeMentionText(activity, 'recipientid'); + + assert(activity.text === 'sometext'); + }); + + it(`should remove teams mention`, async function () { + const mentionJson = '{\"mentioned\": {\"id\": \"recipientid\"},\"text\": \"botname\"}'; + const entity = JSON.parse(mentionJson); + entity.type = 'mention'; + + const activity = MessageFactory.text('botname sometext'); + activity.channelId = 'teams'; + activity.entities = [entity]; + + TurnContext.removeMentionText(activity, 'recipientid'); + + assert(activity.text === 'sometext'); + }); + + it(`should remove slack mention`, async function () { + const mentionJson = '{\"mentioned\": {\"id\": \"recipientid\"},\"text\": \"@botname\"}'; + const entity = JSON.parse(mentionJson); + entity.type = 'mention'; + + const activity = MessageFactory.text('@botname sometext'); + activity.channelId = 'slack'; + activity.entities = [entity]; + + TurnContext.removeMentionText(activity, 'recipientid'); + + assert(activity.text === 'sometext'); + }); + + it(`should remove GroupMe mention`, async function () { + const mentionJson = '{\"mentioned\": {\"id\": \"recipientid\"},\"text\": \"@bot name\"}'; + const entity = JSON.parse(mentionJson); + entity.type = 'mention'; + + const activity = MessageFactory.text('@bot name sometext'); + activity.channelId = 'groupme'; + activity.entities = [entity]; + + TurnContext.removeMentionText(activity, 'recipientid'); + + assert(activity.text === 'sometext'); + }); + + it(`should remove Telegram mention`, async function () { + const mentionJson = '{\"mentioned\": {\"id\": \"recipientid\"},\"text\": \"botname\"}'; + const entity = JSON.parse(mentionJson); + entity.type = 'mention'; + + const activity = MessageFactory.text('botname sometext'); + activity.channelId = 'telegram'; + activity.entities = [entity]; + + TurnContext.removeMentionText(activity, 'recipientid'); + + assert(activity.text === 'sometext'); + }); +}); diff --git a/libraries/botbuilder-core/tests/transcriptMiddleware.test.js b/libraries/botbuilder-core/tests/transcriptMiddleware.test.js index d6e84b6f89..9c263b3603 100644 --- a/libraries/botbuilder-core/tests/transcriptMiddleware.test.js +++ b/libraries/botbuilder-core/tests/transcriptMiddleware.test.js @@ -196,8 +196,175 @@ describe(`TranscriptLoggerMiddleware`, function () { adapter.send('test'); }); }); + + it(`should add resource response id to activity when activity id is empty`, function (done) { + let conversationId = null; + const transcriptStore = new MemoryTranscriptStore(); + const adapter = new TestAdapter(async (context) => { + conversationId = context.activity.conversation.id; + + await context.sendActivity(`echo:${context.activity.text}`); + }).use(new TranscriptLoggerMiddleware(transcriptStore)); + + const fooActivity = { + type: ActivityTypes.Message, + id: 'testFooId', + text: 'foo' + }; + + adapter + .send(fooActivity) + // sent activities do not contain the id returned from the service, so it should be undefined here + .assertReply(activity => assert.equal(activity.id, undefined) && assert.equal(activity.text, 'echo:foo')) + .send('bar') + .assertReply(activity => assert.equal(activity.id, undefined) && assert.equal(activity.text, 'echo:bar')) + .then(() => { + transcriptStore.getTranscriptActivities('test', conversationId).then(pagedResult => { + assert.equal(pagedResult.items.length, 4); + assert.equal(pagedResult.items[0].text, 'foo'); + // Transcript activities should have the id present on the activity when received + assert.equal(pagedResult.items[0].id, 'testFooId'); + assert.equal(pagedResult.items[1].text, 'echo:foo'); + // Sent Activities in the transcript store should have the Id returned from Resource Response + // (the test adapter increments a number and uses this for the id) + assert.equal(pagedResult.items[1].id, '0'); + + assert.equal(pagedResult.items[2].text, 'bar'); + // Received activities also auto-add the incremental from the test adapter + assert.equal(pagedResult.items[2].id, '1'); + + assert.equal(pagedResult.items[3].text, 'echo:bar'); + assert.equal(pagedResult.items[3].id, '2'); + pagedResult.items.forEach(a => { + assert(a.timestamp); + assert(a.id); + }) + done(); + }); + }); + }); + + it(`should use outgoing activity's timestamp for activity id when activity id and resourceResponse is empty`, function(done) { + let conversationId, timestamp; + const transcriptStore = new MemoryTranscriptStore(); + + const adapter = createTestAdapterWithNoResourceResponseId(async (context) => { + conversationId = context.activity.conversation.id; + + timestamp = new Date(Date.now()); + await context.sendActivity({ + text: `echo:${ context.activity.text }`, + timestamp: timestamp, + type: ActivityTypes.Message + }); + }).use(new TranscriptLoggerMiddleware(transcriptStore)); + + const fooActivity = { + type: ActivityTypes.Message, + id: 'testFooId', + text: 'foo' + }; + + adapter + .send(fooActivity) + // sent activities do not contain the id returned from the service, so it should be undefined here + .assertReply(activity => { + assert.equal(activity.id, undefined); + assert.equal(activity.text, 'echo:foo'); + assert.equal(activity.timestamp, timestamp); + }) + .then(() => { + transcriptStore.getTranscriptActivities('test', conversationId).then(pagedResult => { + assert.equal(pagedResult.items.length, 2); + assert.equal(pagedResult.items[0].text, 'foo'); + // Transcript activities should have the id present on the activity when received + assert.equal(pagedResult.items[0].id, 'testFooId'); + + assert.equal(pagedResult.items[1].text, 'echo:foo'); + // Sent Activities in the transcript store should use the timestamp on the bot's outgoing activities + // to log the activity when the following cases are true: + // 1. The outgoing Activity.id is falsey + // 2. The ResourceResponse.id is falsey (some channels may not emit a ResourceResponse with an id value) + // 3. The outgoing Activity.timestamp exists + // Activity.Id ends with Activity.Timestamp and generated id starts with 'g_' + assert.ok(pagedResult.items[1].id.endsWith(timestamp.getTime().toString())); + assert.ok(pagedResult.items[1].id.startsWith('g_')); + //assert.equal(pagedResult.items[1].id, timestamp.getTime().toString()); + pagedResult.items.forEach(a => { + assert(a.timestamp); + }); + done(); + }); + }); + }); + + it(`should use current server time for activity id when activity and resourceResponse id is empty and no activity timestamp exists`, function(done) { + let conversationId; + const transcriptStore = new MemoryTranscriptStore(); + + const adapter = createTestAdapterWithNoResourceResponseId(async (context) => { + conversationId = context.activity.conversation.id; + + await context.sendActivity({ + text: `echo:${ context.activity.text }`, + type: ActivityTypes.Message + }); + }).use(new TranscriptLoggerMiddleware(transcriptStore)); + + const fooActivity = { + type: ActivityTypes.Message, + id: 'testFooId', + text: 'foo' + }; + + adapter + .send(fooActivity) + // sent activities do not contain the id returned from the service, so it should be undefined here + .assertReply(activity => { + assert.equal(activity.id, undefined); + assert.equal(activity.text, 'echo:foo'); + }) + .then(() => { + transcriptStore.getTranscriptActivities('test', conversationId).then(pagedResult => { + assert.equal(pagedResult.items.length, 2); + assert.equal(pagedResult.items[0].text, 'foo'); + // Transcript activities should have the id present on the activity when received + assert.equal(pagedResult.items[0].id, 'testFooId'); + + assert.equal(pagedResult.items[1].text, 'echo:foo'); + // Sent Activities in the transcript store should use the time the TranscriptLoggerMiddleware attempted + // to log the activity when the following cases are true: + // 1. The outgoing Activity.id is falsey + // 2. The ResourceResponse.id is falsey (some channels may not emit a ResourceResponse with an id value) + // 3. The outgoing Activity.timestamp is falsey + assert(pagedResult.items[1].id); + pagedResult.items.forEach(a => { + assert(a.timestamp); + }); + done(); + }); + }); + }); }); +function createTestAdapterWithNoResourceResponseId(logic) { + const modifiedAdapter = new TestAdapter(logic); + + function sendActivities(context, activities) { + const responses = activities + .filter((a) => this.sendTraceActivities || a.type !== 'trace') + .map((activity) => { + this.activityBuffer.push(activity); + return { }; + }); + + return Promise.resolve(responses); + } + modifiedAdapter.sendActivities = sendActivities.bind(modifiedAdapter); + + return modifiedAdapter; +} + function createReply(activity, text, locale = null) { return { diff --git a/libraries/botbuilder-core/tests/turnContext.test.js b/libraries/botbuilder-core/tests/turnContext.test.js index c5f6c2572a..9b30858fcd 100644 --- a/libraries/botbuilder-core/tests/turnContext.test.js +++ b/libraries/botbuilder-core/tests/turnContext.test.js @@ -1,5 +1,7 @@ const assert = require('assert'); -const { BotAdapter, TurnContext } = require('../'); +const { BotAdapter, MessageFactory, TurnContext, ActivityTypes } = require('../'); + +const activityId = `activity ID`; const testMessage = { type: 'message', @@ -48,6 +50,28 @@ class SimpleAdapter extends BotAdapter { } } +class SendAdapter extends BotAdapter { + sendActivities(context, activities) { + assert(context, `SendAdapter.sendActivities: missing context.`); + assert(activities, `SendAdapter.sendActivities: missing activities.`); + assert(Array.isArray(activities), `SendAdapter.sendActivities: activities not array.`); + assert(activities.length > 0, `SendAdapter.sendActivities: empty activities array.`); + return Promise.resolve(activities); + } + + updateActivity(context, activity) { + assert(context, `SendAdapter.updateActivity: missing context.`); + assert(activity, `SendAdapter.updateActivity: missing activity.`); + return Promise.resolve(); + } + + deleteActivity(context, reference) { + assert(context, `SendAdapter.deleteActivity: missing context.`); + assert(reference, `SendAdapter.deleteActivity: missing reference.`); + return Promise.resolve(); + } +} + describe(`TurnContext`, function () { this.timeout(5000); @@ -162,7 +186,23 @@ describe(`TurnContext`, function () { }); context.sendActivity('test', 'say test', 'ignoringInput').then(() => done()); }); - + + it(`should send a trace activity.`, function (done) { + const context = new TurnContext(new SimpleAdapter(), testMessage); + context.onSendActivities((ctx, activities, next) => { + assert(Array.isArray(activities), `activities not array.`); + assert(activities.length === 1, `invalid count of activities.`); + assert(activities[0].type === ActivityTypes.Trace, `type wrong.`); + assert(activities[0].name === 'name-text', `name wrong.`); + assert(activities[0].value === 'value-text', `value worng.`); + assert(activities[0].valueType === 'valueType-text', `valeuType wrong.`); + assert(activities[0].label === 'label-text', `label wrong.`); + return[]; + }); + context.sendTraceActivity('name-text', 'value-text', 'valueType-text', 'label-text').then(() => done()); + }); + + it(`should send multiple activities via sendActivities().`, function (done) { const context = new TurnContext(new SimpleAdapter(), testMessage); context.sendActivities([testMessage, testMessage, testMessage]).then((responses) => { @@ -214,6 +254,26 @@ describe(`TurnContext`, function () { done(); }); }); + + it(`should be able to update an activity with MessageFactory`, function (done) { + let called = false; + const context = new TurnContext(new SimpleAdapter(), testMessage); + context.onUpdateActivity((ctx, activity, next) => { + assert(ctx, `context not passed to hook`); + assert(activity, `activity not passed to hook`); + assert(activity.id === activityId, `wrong activity passed to hook`); + assert(activity.conversation.id === testMessage.conversation.id, `conversation ID not applied to activity`); + assert(activity.serviceUrl === testMessage.serviceUrl, `service URL not applied to activity`); + called = true; + return next(); + }); + const message = MessageFactory.text(`test text`); + message.id = activityId; + context.updateActivity(message).then((responses) => { + assert(called, `update hook not called.`); + done(); + }); + }); it(`should call onDeleteActivity() hook before delete by "id".`, function (done) { let called = false; @@ -372,4 +432,13 @@ describe(`TurnContext`, function () { assert(text,' test activity'); assert(activity.text,' test activity'); }); -}); \ No newline at end of file + + it(`should clear existing activity.id in context.sendActivity().`, function (done) { + const context = new TurnContext(new SendAdapter(), testMessage); + context.sendActivity(testMessage).then((response) => { + assert(response, `response is missing.`); + assert(response.id === undefined, `invalid response id of "${response.id}" sent back. Should be 'undefined'`); + done(); + }); + }); +}); diff --git a/libraries/botbuilder-core/tsconfig.json b/libraries/botbuilder-core/tsconfig.json index 3788218c0b..6c669e4c95 100644 --- a/libraries/botbuilder-core/tsconfig.json +++ b/libraries/botbuilder-core/tsconfig.json @@ -1,11 +1,13 @@ { "compilerOptions": { - "target": "ESNext", - "module": "commonjs", "declaration": true, + "esModuleInterop": true, "sourceMap": true, + "module": "commonjs", "outDir": "./lib", "rootDir": "./src", + "target": "es6", + "lib": [ "es2015", "dom" ], "types" : ["node"] }, "include": [ diff --git a/libraries/botbuilder-dialogs-adaptive/src/actions/beginDialog.ts b/libraries/botbuilder-dialogs-adaptive/src/actions/beginDialog.ts index 657acd941a..7102ab647d 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/actions/beginDialog.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/actions/beginDialog.ts @@ -5,61 +5,37 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { Dialog, DialogTurnResult, DialogConfiguration, DialogContext } from 'botbuilder-dialogs'; +import { Dialog, DialogTurnResult, DialogConfiguration, DialogContext, DialogReason } from 'botbuilder-dialogs'; export interface BeginDialogConfiguration extends DialogConfiguration { /** * ID of the dialog to call. */ - dialogId?: string; + dialogIdToCall?: string; /** * (Optional) static options to pass to called dialog. - * - * @remarks - * These options will be merged with any dynamic options configured as - * [inputBindings](#inputbindings). */ options?: object; - - /** - * (Optional) data binds the called dialogs input & output to the given state property. - * - * @remarks - * The bound properties current value will be passed to the called dialog as part of its - * options and will be accessible within the dialog via `dialog.options.value`. The result - * returned from the called dialog will then be copied to the bound property. - */ - property?: string; } export class BeginDialog extends Dialog { /** * Creates a new `BeginDialog` instance. - * @param dialogId ID of the dialog to call. - * @param property (Optional) property to bind the called dialogs value to. + * @param dialogIdToCall ID of the dialog to call. * @param options (Optional) static options to pass the called dialog. */ constructor(); - constructor(dialogId: string, options?: O); - constructor(dialogId: string, property: string, options?: O) - constructor(dialogId?: string, optionsOrProperty?: O|string, options?: O) { + constructor(dialogIdToCall: string, options?: O); + constructor(dialogIdToCall?: string, options?: O) { super(); - this.outputProperty = 'dialog.lastResult'; - - // Process args - if (typeof optionsOrProperty === 'object') { - options = optionsOrProperty; - optionsOrProperty = undefined; - } - if (dialogId) { this.dialogId = dialogId } - if (typeof optionsOrProperty == 'string') { this.property = optionsOrProperty } + if (dialogIdToCall) { this.dialogIdToCall = dialogIdToCall } if (options) { this.options = options } } - protected onComputeID(): string { - return `call[${this.hashedLabel(this.dialogId + ':' + this.bindingPath(false))}]`; + protected onComputeId(): string { + return `BeginDialog[${this.dialogIdToCall}]`; } public configure(config: BeginDialogConfiguration): this { @@ -69,36 +45,27 @@ export class BeginDialog extends Dialog { /** * ID of the dialog to call. */ - public dialogId: string; + public dialogIdToCall: string; /** * (Optional) static options to pass to the called dialog. - * - * @remarks - * These options will be merged with any dynamic options configured as - * [inputBindings](#inputbindings). */ public options?: object; /** - * (Optional) data binds the called dialogs input & output to the given property. - * - * @remarks - * The bound properties current value will be passed to the called dialog as part of its - * options and will be accessible within the dialog via `dialog.options.value`. The result - * returned from the called dialog will then be copied to the bound property. + * (Optional) property path to store the dialog result in. */ - public set property(value: string) { - this.inputProperties['value'] = value; - this.outputProperty = value; - } - - public get property(): string { - return this.inputProperties['value']; - } + public resultProperty?: string; public async beginDialog(dc: DialogContext, options?: O): Promise { options = Object.assign({}, options, this.options); - return await dc.beginDialog(this.dialogId, options); + return await dc.beginDialog(this.dialogIdToCall, options); + } + + public async resumeDialog(dc: DialogContext, reason: DialogReason, result: any = null): Promise { + if (this.resultProperty != null) { + dc.state.setValue(this.resultProperty, result); + } + return await dc.endDialog(result); } } \ No newline at end of file diff --git a/libraries/botbuilder-dialogs-adaptive/src/actions/cancelAllDialogs.ts b/libraries/botbuilder-dialogs-adaptive/src/actions/cancelAllDialogs.ts index e6577964a0..8cfe3485aa 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/actions/cancelAllDialogs.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/actions/cancelAllDialogs.ts @@ -16,32 +16,20 @@ export interface CancelAllDialogsConfiguration extends DialogConfiguration { export class CancelAllDialogs extends DialogCommand { constructor(); - constructor(eventName: string, eventValue?: string|object); - constructor(eventName?: string, eventValue?: string|object) { + constructor(eventName: string, eventValue?: string); + constructor(eventName?: string, eventValue?: string) { super(); this.eventName = eventName; - if (typeof eventValue == 'string') { - this.eventValueProperty = eventValue; - } else { - this.eventValue = eventValue - } + this.eventValue = eventValue } - protected onComputeID(): string { - return `cancelAllDialogs[${this.hashedLabel(this.eventName || '')}]`; + protected onComputeId(): string { + return `CancelAllDialogs[${this.eventName || ''}]`; } public eventName: string; - public eventValue: object; - - public set eventValueProperty(value: string) { - this.inputProperties['eventValue'] = value; - } - - public get eventValueProperty(): string { - return this.inputProperties['eventValue']; - } + public eventValue: string; public configure(config: CancelAllDialogsConfiguration): this { return super.configure(config); @@ -52,6 +40,6 @@ export class CancelAllDialogs extends DialogCommand { eventName: this.eventName, eventValue: this.eventValue }, options); - return await dc.cancelAllDialogs(opt.eventName, opt.eventValue); + return await dc.cancelAllDialogs(true, opt.eventName, opt.eventValue); } } \ No newline at end of file diff --git a/libraries/botbuilder-dialogs-adaptive/src/actions/codeAction.ts b/libraries/botbuilder-dialogs-adaptive/src/actions/codeAction.ts index b69e4926bd..28f9721e56 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/actions/codeAction.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/actions/codeAction.ts @@ -24,8 +24,8 @@ export class CodeAction extends Dialo this.handler = handler; } - protected onComputeID(): string { - return `codeAction[${this.hashedLabel(this.handler.toString())}]`; + protected onComputeId(): string { + return `CodeAction[${this.handler.toString()}]`; } protected async onRunCommand(context: T, options: object): Promise { diff --git a/libraries/botbuilder-dialogs-adaptive/src/actions/deleteProperty.ts b/libraries/botbuilder-dialogs-adaptive/src/actions/deleteProperty.ts index a4d4f393b5..0e9f706214 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/actions/deleteProperty.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/actions/deleteProperty.ts @@ -29,8 +29,8 @@ export class DeleteProperty extends DialogCommand { if (property) { this.property = property } } - protected onComputeID(): string { - return `delete[${this.hashedLabel(this.property)}]`; + protected onComputeId(): string { + return `DeleteProperty[${this.property}]`; } public configure(config: DeletePropertyConfiguration): this { diff --git a/libraries/botbuilder-dialogs-adaptive/src/actions/editActions.ts b/libraries/botbuilder-dialogs-adaptive/src/actions/editActions.ts index 0e00a7f2ed..b7e340619e 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/actions/editActions.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/actions/editActions.ts @@ -18,11 +18,6 @@ export interface EditActionsConfiguration extends DialogConfiguration { * The actions to update the dialog with. */ actions?: Dialog[]; - - /** - * Tags to insert actions before when [changeType](#changetype) is `ActionChangeType.insertActionsBeforeTags`. - */ - tags?: string[]; } export class EditActions extends DialogCommand { @@ -36,27 +31,22 @@ export class EditActions extends DialogCommand { */ public actions: Dialog[]; - /** - * Tags to insert actions before when [changeType](#changetype) is `PlanChangeType.doActionsBeforeTags`. - */ - public tags: string[]; - /** * Creates a new `EditActions` instance. * @param changeType The type of change to make to the dialogs list of actions. * @param actions The actions to update the dialog with. */ constructor(); - constructor(changeType: ActionChangeType, actions: Dialog[], tags?: string[]); - constructor(changeType?: ActionChangeType, actions?: Dialog[], tags?: string[]) { + constructor(changeType: ActionChangeType, actions: Dialog[]); + constructor(changeType?: ActionChangeType, actions?: Dialog[]) { super(); if (changeType !== undefined) { this.changeType = changeType } if (Array.isArray(actions)) { this.actions = actions } - if (Array.isArray(tags)) { this.tags = tags } } - protected onComputeID(): string { - return `editActions[${this.hashedLabel(this.changeType)}]`; + protected onComputeId(): string { + const idList = this.actions.map(action => action.id); + return `EditActions[${this.changeType}|${idList.join(',')}]`; } public configure(config: EditActionsConfiguration): this { @@ -69,8 +59,8 @@ export class EditActions extends DialogCommand { protected async onRunCommand(sequence: SequenceContext, options: object): Promise { // Ensure planning context and condition - if (!(sequence instanceof SequenceContext)) { throw new Error(`${this.id}: should only be used within a planning or sequence dialog.`) } - if (this.changeType == undefined) { throw new Error(`${this.id}: no 'changeType' specified.`) } + if (!(sequence instanceof SequenceContext)) { throw new Error(`EditAction should only be used in the context of an adaptive dialog.`) } + if (this.changeType == undefined) { throw new Error(`No 'changeType' specified.`) } // Queue changes const changes: ActionChangeList = { @@ -79,9 +69,6 @@ export class EditActions extends DialogCommand { return { dialogId: action.id, dialogStack: [] } as ActionState }) }; - if (this.changeType == ActionChangeType.insertActionsBeforeTags) { - changes.tags = this.tags; - } sequence.queueChanges(changes); return await sequence.endDialog(); diff --git a/libraries/botbuilder-dialogs-adaptive/src/actions/editArray.ts b/libraries/botbuilder-dialogs-adaptive/src/actions/editArray.ts index 4003b6b142..db4eb0a57d 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/actions/editArray.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/actions/editArray.ts @@ -34,8 +34,8 @@ export class EditArray extends DialogCommand { if (itemProperty) { this.itemProperty = itemProperty } } - protected onComputeID(): string { - return `array[${this.hashedLabel(this.changeType + ': ' + this.arrayProperty)}]`; + protected onComputeId(): string { + return `EditArray[${this.changeType}: ${this.arrayProperty}]`; } public changeType: ArrayChangeType; diff --git a/libraries/botbuilder-dialogs-adaptive/src/actions/emitEvent.ts b/libraries/botbuilder-dialogs-adaptive/src/actions/emitEvent.ts index e5455c88ce..de133e2496 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/actions/emitEvent.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/actions/emitEvent.ts @@ -10,7 +10,6 @@ import { DialogTurnResult, DialogCommand, DialogContext, DialogConfiguration, Di export interface EmitEventConfiguration extends DialogConfiguration { eventName?: string; eventValue?: string; - eventValueProperty?: string; bubbleEvent?: boolean; resultProperty?: string; } @@ -18,44 +17,24 @@ export interface EmitEventConfiguration extends DialogConfiguration { export class EmitEvent extends DialogCommand { constructor(); - constructor(eventName: string, eventValue?: string|object, bubbleEvent?: boolean); - constructor(eventName?: string, eventValue?: string|object, bubbleEvent = true) { + constructor(eventName: string, eventValue?: string, bubbleEvent?: boolean); + constructor(eventName?: string, eventValue?: string, bubbleEvent = true) { super(); this.eventName = eventName; - if (typeof eventValue == 'string') { - this.eventValueProperty = eventValue; - } else { - this.eventValue = eventValue - } + this.eventValue = eventValue; this.bubbleEvent = bubbleEvent; } - protected onComputeID(): string { - return `emitEvent[${this.hashedLabel(this.eventName || '')}]`; + protected onComputeId(): string { + return `EmitEvent[${this.eventName || ''}]`; } public eventName: string; - public eventValue: object; + public eventValue: string; public bubbleEvent: boolean; - public set eventValueProperty(value: string) { - this.inputProperties['eventValue'] = value; - } - - public get eventValueProperty(): string { - return this.inputProperties['eventValue']; - } - - public set resultProperty(value: string) { - this.outputProperty = value; - } - - public get resultProperty(): string { - return this.outputProperty; - } - public configure(config: EmitEventConfiguration): this { return super.configure(config); } diff --git a/libraries/botbuilder-dialogs-adaptive/src/actions/endDialog.ts b/libraries/botbuilder-dialogs-adaptive/src/actions/endDialog.ts index 34870e0340..8248d45ec1 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/actions/endDialog.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/actions/endDialog.ts @@ -30,8 +30,8 @@ export class EndDialog extends DialogCommand { return super.configure(config); } - protected onComputeID(): string { - return `end[${this.hashedLabel(this.resultProperty || '')}]`; + protected onComputeId(): string { + return `EndDialog[${this.resultProperty || ''}]`; } /** diff --git a/libraries/botbuilder-dialogs-adaptive/src/actions/endTurn.ts b/libraries/botbuilder-dialogs-adaptive/src/actions/endTurn.ts index bffcc36ac7..3ee389223a 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/actions/endTurn.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/actions/endTurn.ts @@ -10,8 +10,8 @@ import { ActivityTypes } from 'botbuilder-core'; export class EndTurn extends Dialog { - protected onComputeID(): string { - return `endTurn[]`; + protected onComputeId(): string { + return `EndTurn[]`; } public async beginDialog(dc: DialogContext): Promise { diff --git a/libraries/botbuilder-dialogs-adaptive/src/actions/forEach.ts b/libraries/botbuilder-dialogs-adaptive/src/actions/forEach.ts index 3f7f048124..09d4cce952 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/actions/forEach.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/actions/forEach.ts @@ -57,9 +57,9 @@ export class ForEach extends DialogCommand { if (actions) { this.actions = actions } } - protected onComputeID(): string { + protected onComputeId(): string { const label = this.list ? this.list.toString() : ''; - return `forEach[${this.hashedLabel(label)}]`; + return `ForEach[${label}]`; } /** @@ -111,7 +111,7 @@ export class ForEach extends DialogCommand { // Unpack options let { list, offset } = options; - if (list == undefined) { list = this.list.evaluate(this.id, sequence.state.toJSON()) } + if (list == undefined) { list = this.list.evaluate(this.id, sequence.state) } if (offset == undefined) { offset = 0 } // Get next page of items diff --git a/libraries/botbuilder-dialogs-adaptive/src/actions/forEachPage.ts b/libraries/botbuilder-dialogs-adaptive/src/actions/forEachPage.ts index c59c03f85e..214958486f 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/actions/forEachPage.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/actions/forEachPage.ts @@ -65,9 +65,9 @@ export class ForEachPage extends DialogCommand { if (actions) { this.actions = actions } } - protected onComputeID(): string { + protected onComputeId(): string { const label = this.list ? this.list.toString() : ''; - return `forEachPage[${this.hashedLabel(label)}]`; + return `ForEachPage[${label}]`; } /** @@ -120,7 +120,7 @@ export class ForEachPage extends DialogCommand { // Unpack options let { list, offset, pageSize } = options; - const memory = sequence.state.toJSON(); + const memory = sequence.state; if (list == undefined) { list = this.list.evaluate(this.id, memory) } if (pageSize == undefined) { pageSize = this.pageSize.evaluate(this.id, memory) } if (offset == undefined) { offset = 0 } diff --git a/libraries/botbuilder-dialogs-adaptive/src/actions/ifCondition.ts b/libraries/botbuilder-dialogs-adaptive/src/actions/ifCondition.ts index fddb4dac13..6836a3d0e3 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/actions/ifCondition.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/actions/ifCondition.ts @@ -53,9 +53,9 @@ export class IfCondition extends DialogCommand { if (Array.isArray(actions)) { this.actions = actions } } - protected onComputeID(): string { + protected onComputeId(): string { const label = this.condition ? this.condition.toString() : ''; - return `if[${this.hashedLabel(label)}]`; + return `If[${label}]`; } public configure(config: IfConditionConfiguration): this { @@ -91,7 +91,7 @@ export class IfCondition extends DialogCommand { if (!this.condition) { throw new Error(`${this.id}: no conditional expression specified.`) } // Evaluate expression - const memory = sequence.state.toJSON(); + const memory = sequence.state; const value = this.condition.evaluate(this.id, memory); // Check for truthy returned value diff --git a/libraries/botbuilder-dialogs-adaptive/src/actions/logAction.ts b/libraries/botbuilder-dialogs-adaptive/src/actions/logAction.ts index c330cd9439..077500d1f6 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/actions/logAction.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/actions/logAction.ts @@ -47,8 +47,8 @@ export class LogAction extends DialogCommand { this.sendTrace = sendTrace; } - protected onComputeID(): string { - return `logAction[${this.hashedLabel(this.template)}]`; + protected onComputeId(): string { + return `LogAction[${this.template}]`; } public configure(config: LogActionConfiguration): this { @@ -61,7 +61,7 @@ export class LogAction extends DialogCommand { // Format message const data = Object.assign({ utterance: dc.context.activity.text || '' - }, dc.state.toJSON(), options); + }, dc.state, options); const msg = format(this.template, data); // Log to console and send trace if needed diff --git a/libraries/botbuilder-dialogs-adaptive/src/actions/repeatDialog.ts b/libraries/botbuilder-dialogs-adaptive/src/actions/repeatDialog.ts index e3ed24b8ee..a481ab45ae 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/actions/repeatDialog.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/actions/repeatDialog.ts @@ -29,8 +29,8 @@ export class RepeatDialog extends DialogCommand { if (options) { this.options = options } } - protected onComputeID(): string { - return `repeat[${this.bindingPath()}]`; + protected onComputeId(): string { + return `RepeatDialog[]`; } /** @@ -47,7 +47,7 @@ export class RepeatDialog extends DialogCommand { } protected async onRunCommand(dc: DialogContext, options?: object): Promise { - const originalOptions = dc.state.dialog.get('options'); + const originalOptions = dc.state.getValue('options'); options = Object.assign({}, originalOptions, options, this.options); return await this.repeatParentDialog(dc, options); } diff --git a/libraries/botbuilder-dialogs-adaptive/src/actions/replaceDialog.ts b/libraries/botbuilder-dialogs-adaptive/src/actions/replaceDialog.ts index 10de971e61..4fcb666a13 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/actions/replaceDialog.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/actions/replaceDialog.ts @@ -5,7 +5,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { DialogTurnResult, DialogConfiguration, Dialog, DialogCommand, DialogContext } from 'botbuilder-dialogs'; +import { DialogTurnResult, DialogConfiguration, DialogCommand, DialogContext } from 'botbuilder-dialogs'; export interface ReplaceDialogConfiguration extends DialogConfiguration { /** @@ -38,8 +38,8 @@ export class ReplaceDialog extends DialogCommand { if (options) { this.options = options } } - protected onComputeID(): string { - return `replace[${this.hashedLabel(this.dialogId)}]`; + protected onComputeId(): string { + return `ReplaceDialog[${this.dialogId}]`; } /** diff --git a/libraries/botbuilder-dialogs-adaptive/src/actions/sendActivity.ts b/libraries/botbuilder-dialogs-adaptive/src/actions/sendActivity.ts index ce317de117..b41df47dbb 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/actions/sendActivity.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/actions/sendActivity.ts @@ -5,7 +5,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { DialogTurnResult, DialogConfiguration, Dialog, DialogCommand, DialogContext } from 'botbuilder-dialogs'; +import { DialogTurnResult, DialogConfiguration, DialogCommand, DialogContext } from 'botbuilder-dialogs'; import { Activity, InputHints } from 'botbuilder-core'; import { ActivityProperty } from '../activityProperty'; @@ -24,14 +24,6 @@ export interface SendActivityConfiguration extends DialogConfiguration { * (Optional) input hint for the message. Defaults to a value of `InputHints.acceptingInput`. */ inputHint?: InputHints; - - /** - * (Optional) in-memory state property that the result of the send should be saved to. - * - * @remarks - * This is just a convenience property for setting the dialogs [outputBinding](#outputbinding). - */ - resultProperty?: string; } export class SendActivity extends DialogCommand { @@ -51,8 +43,8 @@ export class SendActivity extends DialogCommand { this.activityProperty.inputHint = inputHint || InputHints.AcceptingInput; } - protected onComputeID(): string { - return `send[${this.hashedLabel(this.activityProperty.displayLabel)}]`; + protected onComputeId(): string { + return `SendActivity[${this.activityProperty.displayLabel}]`; } /** @@ -77,13 +69,6 @@ export class SendActivity extends DialogCommand { * @remarks * This is just a convenience property for setting the dialogs [outputBinding](#outputbinding). */ - public set resultProperty(value: string) { - this.outputProperty = value; - } - - public get resultProperty(): string { - return this.outputProperty; - } public configure(config: SendActivityConfiguration): this { return super.configure(config); @@ -91,15 +76,14 @@ export class SendActivity extends DialogCommand { protected async onRunCommand(dc: DialogContext, options: object): Promise { if (!this.activityProperty.hasValue()) { - throw new Error(`SendActivity: no activity assigned for action '${this.id}'.`) + // throw new Error(`SendActivity: no activity assigned for action '${this.id}'.`) + throw new Error(`SendActivity: no activity assigned for action.`) } // Send activity and return result - // - If `resultProperty` has been set, the returned result will be saved to the requested - // memory location. const data = Object.assign({ utterance: dc.context.activity.text || '' - }, dc.state.toJSON(), options); + }, dc.state, options); const activity = this.activityProperty.format(dc, data); const result = await dc.context.sendActivity(activity); return await dc.endDialog(result); diff --git a/libraries/botbuilder-dialogs-adaptive/src/actions/sendList.ts b/libraries/botbuilder-dialogs-adaptive/src/actions/sendList.ts index 9a7352f9d0..e4cbbf7894 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/actions/sendList.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/actions/sendList.ts @@ -44,8 +44,8 @@ export class SendList extends DialogCommand { if (itemTemplate) { this.itemTemplate = itemTemplate } } - protected onComputeID(): string { - return `sendList[${this.bindingPath()}]`; + protected onComputeId(): string { + return `SendList[]`; } /** diff --git a/libraries/botbuilder-dialogs-adaptive/src/actions/setProperty.ts b/libraries/botbuilder-dialogs-adaptive/src/actions/setProperty.ts index f1cfa5259d..12b4501c37 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/actions/setProperty.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/actions/setProperty.ts @@ -44,9 +44,9 @@ export class SetProperty extends DialogCommand { if (value) { this.value = new ExpressionProperty(value) } } - protected onComputeID(): string { + protected onComputeId(): string { const label = this.value ? this.value.toString() : ''; - return `setProperty[${this.hashedLabel(label)}]`; + return `SetProperty[${label}]`; } public configure(config: SetPropertyConfiguration): this { @@ -74,7 +74,7 @@ export class SetProperty extends DialogCommand { if (!this.value) { throw new Error(`${this.id}: no 'value' expression specified.`) } // Evaluate expression and save value - const memory = dc.state.toJSON(); + const memory = dc.state; const value = this.value.evaluate(this.id, memory); dc.state.setValue(this.property, value); diff --git a/libraries/botbuilder-dialogs-adaptive/src/activityProperty.ts b/libraries/botbuilder-dialogs-adaptive/src/activityProperty.ts index 378af1ff3f..33bb4d2b5b 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/activityProperty.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/activityProperty.ts @@ -65,7 +65,7 @@ export class ActivityProperty { public format(dc: DialogContext, extraData?: object, override?: Partial|string): Partial { // Format basic activity - const data = Object.assign({}, dc.state.toJSON(), extraData); + const data = Object.assign({}, dc.state, extraData); let activity: Partial; if (override) { activity = this.formatOverride(override, data); diff --git a/libraries/botbuilder-dialogs-adaptive/src/adaptiveDialog.ts b/libraries/botbuilder-dialogs-adaptive/src/adaptiveDialog.ts index ab43b911d4..f33a8a9009 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/adaptiveDialog.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/adaptiveDialog.ts @@ -11,7 +11,7 @@ import { } from 'botbuilder-core'; import { Dialog, DialogInstance, DialogReason, DialogTurnResult, DialogTurnStatus, DialogEvent, - DialogContext, StateMap, DialogConfiguration, DialogContainer + DialogContext, DialogConfiguration, DialogContainer } from 'botbuilder-dialogs'; import { AdaptiveEventNames, SequenceContext, ActionChangeList, ActionChangeType, AdaptiveDialogState @@ -96,8 +96,8 @@ export class AdaptiveDialog extends DialogContainer { // Base Dialog Overrides //--------------------------------------------------------------------------------------------- - protected onComputeID(): string { - return `adaptive[${this.bindingPath()}]`; + protected onComputeId(): string { + return `AdaptiveDialog[]`; } public async beginDialog(dc: DialogContext, options?: O): Promise { @@ -161,7 +161,7 @@ export class AdaptiveDialog extends DialogContainer { const state = instance.state as AdaptiveDialogState; if (state.actions && state.actions.length > 0) { // We need to mockup a DialogContext so that we can call repromptDialog() for the active action - const actionDC: DialogContext = new DialogContext(this.dialogs, context, state.actions[0], new StateMap({}), new StateMap({})); + const actionDC: DialogContext = new DialogContext(this.dialogs, context, state.actions[0]); await actionDC.repromptDialog(); } } diff --git a/libraries/botbuilder-dialogs-adaptive/src/conditions/onIntent.ts b/libraries/botbuilder-dialogs-adaptive/src/conditions/onIntent.ts index 9e541202dc..09cea36f10 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/conditions/onIntent.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/conditions/onIntent.ts @@ -31,10 +31,10 @@ export class OnIntent extends OnDialogEvent { this.matches = Array.isArray(matches) ? matches : (matches !== undefined ? [matches] : []); } - protected async onIsTriggered(sequence: SequenceContext, event: DialogEvent): Promise { + protected async onIsTriggered(sequence: SequenceContext, event: DialogEvent): Promise { // Ensure all intents, entities, and properties exist. - const memory = sequence.state.toJSON(); + const memory = sequence.state; for(let i = 0; i < this.matches.length; i++) { const value = DialogContextState.queryMemory(memory, this.matches[i], 1); if (!Array.isArray(value) || value.length == 0 || value[0] == undefined) { diff --git a/libraries/botbuilder-dialogs-adaptive/src/expressionProperty.ts b/libraries/botbuilder-dialogs-adaptive/src/expressionProperty.ts index d7df8e8e1d..da84f32eba 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/expressionProperty.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/expressionProperty.ts @@ -6,7 +6,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { DialogContextVisibleState } from "botbuilder-dialogs"; +import { DialogContextVisibleState, DialogStateManager } from "botbuilder-dialogs"; import { ExpressionEngine, Expression } from 'botframework-expressions'; export type ExpressionDelegate = (state: DialogContextVisibleState) => T; @@ -54,7 +54,7 @@ export class ExpressionProperty { return this._expression; } - public evaluate(stepId: string, state: DialogContextVisibleState): T { + public evaluate(stepId: string, state: DialogStateManager): T { // Parse expression let expression: Expression; try { diff --git a/libraries/botbuilder-dialogs-adaptive/src/input/attachmentInput.ts b/libraries/botbuilder-dialogs-adaptive/src/input/attachmentInput.ts index c8305f85a0..8fffbff93e 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/input/attachmentInput.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/input/attachmentInput.ts @@ -43,8 +43,8 @@ export class AttachmentInput extends InputDialog { return super.configure(config); } - protected onComputeID(): string { - return `textInput[${this.bindingPath()}]`; + protected onComputeId(): string { + return `AttachmentInput[]`; } protected getDefaultInput(dc: DialogContext): any { diff --git a/libraries/botbuilder-dialogs-adaptive/src/input/choiceInput.ts b/libraries/botbuilder-dialogs-adaptive/src/input/choiceInput.ts index 7596cecace..89e62a0cd5 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/input/choiceInput.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/input/choiceInput.ts @@ -117,15 +117,15 @@ export class ChoiceInput extends InputDialog { return this; } - protected onComputeID(): string { - return `choiceInput[${this.bindingPath()}]`; + protected onComputeId(): string { + return `ChoiceInput[]`; } protected onInitializeOptions(dc: DialogContext, options: ChoiceInputOptions): ChoiceInputOptions { if (!Array.isArray(options.choices)) { if (this.choices) { // Compute list of choices - const memory = dc.state.toJSON(); + const memory = dc.state; const value = this.choices.evaluate(this.id, memory); if (Array.isArray(value)) { options.choices = value; diff --git a/libraries/botbuilder-dialogs-adaptive/src/input/confirmInput.ts b/libraries/botbuilder-dialogs-adaptive/src/input/confirmInput.ts index bca792919f..a52df5ee2e 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/input/confirmInput.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/input/confirmInput.ts @@ -79,8 +79,8 @@ export class ConfirmInput extends InputDialog { return super.configure(config); } - protected onComputeID(): string { - return `ConfirmInput[${this.bindingPath()}]`; + protected onComputeId(): string { + return `ConfirmInput[]`; } protected async onRecognizeInput(dc: DialogContext, consultation: boolean): Promise { diff --git a/libraries/botbuilder-dialogs-adaptive/src/input/inputDialog.ts b/libraries/botbuilder-dialogs-adaptive/src/input/inputDialog.ts index 21ab05f018..0c383d9a57 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/input/inputDialog.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/input/inputDialog.ts @@ -63,9 +63,6 @@ export abstract class InputDialog extends Dialog extends Dialog extends Dialog { // Initialize and persist options @@ -139,7 +139,7 @@ export abstract class InputDialog extends Dialog extends Dialog extends Dialog { return super.configure(config); } - protected onComputeID(): string { - return `NumberInput[${this.bindingPath()}]`; + protected onComputeId(): string { + return `NumberInput[]`; } protected async onRecognizeInput(dc: DialogContext, consultation: boolean): Promise { diff --git a/libraries/botbuilder-dialogs-adaptive/src/input/textInput.ts b/libraries/botbuilder-dialogs-adaptive/src/input/textInput.ts index 92f965c394..da0e4f75c7 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/input/textInput.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/input/textInput.ts @@ -44,8 +44,8 @@ export class TextInput extends InputDialog { return super.configure(config); } - protected onComputeID(): string { - return `textInput[${this.bindingPath()}]`; + protected onComputeId(): string { + return `TextInput[]`; } protected async onRecognizeInput(dc: DialogContext, consultation: boolean): Promise { diff --git a/libraries/botbuilder-dialogs-adaptive/src/sequenceContext.ts b/libraries/botbuilder-dialogs-adaptive/src/sequenceContext.ts index 14b23d1b48..b709982daa 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/sequenceContext.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/sequenceContext.ts @@ -26,7 +26,6 @@ export interface ActionChangeList { export enum ActionChangeType { insertActions = 'insertActions', - insertActionsBeforeTags = 'insertActionsBeforeTags', appendActions = 'appendActions', endSequence = 'endSequence', replaceSequence = 'replaceSequence' @@ -50,7 +49,7 @@ export class SequenceContext extends DialogContext { * Creates a new `SequenceContext` instance. */ constructor(dialogs: DialogSet, dc: DialogContext, state: DialogState, actions: ActionState[], changeKey: symbol) { - super(dialogs, dc.context, state, dc.state.user, dc.state.conversation); + super(dialogs, dc.context, state); this.actions = actions; this.changeKey = changeKey; } @@ -99,7 +98,6 @@ export class SequenceContext extends DialogContext { const change = queue[i]; switch (change.changeType) { case ActionChangeType.insertActions: - case ActionChangeType.insertActionsBeforeTags: case ActionChangeType.appendActions: await this.updateSequence(change); break; @@ -131,11 +129,6 @@ export class SequenceContext extends DialogContext { return this; } - public insertActionsBeforeTags(tags: string[], actions: ActionState[]): this { - this.queueChanges({ changeType: ActionChangeType.insertActionsBeforeTags, actions: actions, tags: tags }); - return this; - } - public appendActions(actions: ActionState[]): this { this.queueChanges({ changeType: ActionChangeType.appendActions, actions: actions }); return this; @@ -160,28 +153,6 @@ export class SequenceContext extends DialogContext { case ActionChangeType.insertActions: Array.prototype.unshift.apply(this.actions, change.actions); break; - case ActionChangeType.insertActionsBeforeTags: - let inserted = false; - if (Array.isArray(change.tags)) { - // Walk list of actions to find point at which to insert new actions - for (let i = 0; i < this.actions.length; i++) { - // Does action have one of the tags we're looking for? - if (this.actionHasTags(this.actions[i], change.tags)) { - // Insert actions before the current action. - const args = ([i, 0] as any[]).concat(change.actions); - Array.prototype.splice.apply(this.actions, args); - inserted = true; - break; - } - } - } - - // If we didn't find any of the tags we were looking for then just append - // the actions to the end of the current sequence. - if (!inserted) { - Array.prototype.push.apply(this.actions, change.actions); - } - break; case ActionChangeType.appendActions: Array.prototype.push.apply(this.actions, change.actions); break; @@ -192,17 +163,4 @@ export class SequenceContext extends DialogContext { await this.emitEvent(AdaptiveEventNames.sequenceStarted, undefined, false); } } - - private actionHasTags(action: ActionState, tags: string[]): boolean { - const dialog = this.findDialog(action.dialogId); - if (dialog && dialog.tags.length > 0) { - for (let i = 0; i < dialog.tags.length; i++) { - if (tags.indexOf(dialog.tags[i]) >= 0) { - return true; - } - } - } - - return false; - } } diff --git a/libraries/botbuilder-dialogs-adaptive/tests/adaptiveDialog.test.js b/libraries/botbuilder-dialogs-adaptive/tests/adaptiveDialog.test.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libraries/botbuilder-dialogs-declarative/tests/fileResource.test.js b/libraries/botbuilder-dialogs-declarative/tests/fileResource.test.js index 35d3bc18db..d859be032e 100644 --- a/libraries/botbuilder-dialogs-declarative/tests/fileResource.test.js +++ b/libraries/botbuilder-dialogs-declarative/tests/fileResource.test.js @@ -10,14 +10,14 @@ describe('FileResource', function () { this.timeout(5000); it('FileResource load existing file relative path', async () => { - const fileResource = new FileResource('libraries/botbuilder-dialogs-declarative/tests/resources/00 - TextPrompt/SimplePrompt.main.dialog'); + const fileResource = new FileResource(`${__dirname}/resources/00 - TextPrompt/SimplePrompt.main.dialog`); assert.equal(fileResource.id(), 'SimplePrompt.main.dialog'); const text = await fileResource.readText(); assert.equal(text[0], '{'); }); it('FileResource load existing file absolute path', async () => { - const absolutePath = path.resolve('libraries/botbuilder-dialogs-declarative/tests/resources/00 - TextPrompt/SimplePrompt.main.dialog'); + const absolutePath = path.resolve(`${__dirname}/resources/00 - TextPrompt/SimplePrompt.main.dialog`); const fileResource = new FileResource(absolutePath); assert.equal(fileResource.id(), 'SimplePrompt.main.dialog'); const text = await fileResource.readText(); diff --git a/libraries/botbuilder-dialogs-declarative/tests/resourceProvider.test.js b/libraries/botbuilder-dialogs-declarative/tests/resourceProvider.test.js index a864985e15..720edcdb3f 100644 --- a/libraries/botbuilder-dialogs-declarative/tests/resourceProvider.test.js +++ b/libraries/botbuilder-dialogs-declarative/tests/resourceProvider.test.js @@ -11,7 +11,7 @@ describe('FileResourceProvider', function () { it('FileResourceProvider load multi-level directory, get by id', async () => { let resourceProvider = new FileResourceProvider(); - resourceProvider.registerDirectory('libraries/botbuilder-dialogs-declarative/tests/resources'); + resourceProvider.registerDirectory(`${__dirname}/resources`); let simplePromptResource = await resourceProvider.getResource('SimplePrompt.main.dialog'); assert.equal(simplePromptResource.id(), 'SimplePrompt.main.dialog'); @@ -22,7 +22,7 @@ describe('FileResourceProvider', function () { it('FileResourceProvider load single-level directory, get by id', async () => { let resourceProvider = new FileResourceProvider(); - resourceProvider.registerDirectory('libraries/botbuilder-dialogs-declarative/tests/resources/07 - BeginDialog'); + resourceProvider.registerDirectory(`${__dirname}/resources/07 - BeginDialog`); let simplePromptResource = await resourceProvider.getResource('BeginDialog.main.dialog'); assert.equal(simplePromptResource.id(), 'BeginDialog.main.dialog'); @@ -33,7 +33,7 @@ describe('FileResourceProvider', function () { it('FileResourceProvider load multi-level directory, get by type', async () => { let resourceProvider = new FileResourceProvider(); - resourceProvider.registerDirectory('libraries/botbuilder-dialogs-declarative/tests/resources'); + resourceProvider.registerDirectory(`${__dirname}/resources`); let dialogResources = await resourceProvider.getResources('dialog'); diff --git a/libraries/botbuilder-dialogs-declarative/tests/typeLoader.test.js b/libraries/botbuilder-dialogs-declarative/tests/typeLoader.test.js index 1c77834686..c05e2b2856 100644 --- a/libraries/botbuilder-dialogs-declarative/tests/typeLoader.test.js +++ b/libraries/botbuilder-dialogs-declarative/tests/typeLoader.test.js @@ -10,44 +10,44 @@ describe('TypeLoader', function () { this.timeout(5000); it('TypeLoader TextPrompt: simple textprompt dialog should prompt for text', async function () { - declarativeTestCase('00 - TextPrompt/SimplePrompt.main.dialog', null, async function(adapter){ - await adapter.send('hi') + const adapter = await declarativeTestCase('00 - TextPrompt/SimplePrompt.main.dialog', null); + + await adapter.send('hi') .assertReply('Hello, I\'m Zoidberg. What is your name?'); - }); }); it('TypeLoader Steps: should be ran when no other rules', async function () { - declarativeTestCase('01 - Steps/Steps.main.dialog', null, async function(adapter){ - await adapter.send('hi') + const adapter = await declarativeTestCase('01 - Steps/Steps.main.dialog', null); + + await adapter.send('hi') .assertReply('Step 1') .assertReply('Step 2') .assertReply('Step 3'); - }); }); it('TypeLoader EndTurn: end turn should wait for user input', async function () { - declarativeTestCase('02 - EndTurn/EndTurn.main.dialog', null, async function(adapter){ - await adapter + const adapter = await declarativeTestCase('02 - EndTurn/EndTurn.main.dialog', null); + + await adapter .send('hi') .assertReply('What\'s up?') .send('not much homie') .assertReply('Oh I see!'); - }); }); it('TypeLoader TextInput: should prompt for name and then use name in response. Next time prompt should not ask for property that is already in memory', async function () { - declarativeTestCase('04 - TextInput\\TextInput.main.dialog', null, async function(adapter){ - await adapter + const adapter = await declarativeTestCase('04 - TextInput\\TextInput.main.dialog', null); + + await adapter .send('hi') .assertReply('Hello, I\'m Zoidberg. What is your name?') .send('Carlos') .assertReply('Hello Carlos, nice to talk to you!') - }); }); it('TypeLoader IntentRule: intent rule should route according to intent defined in recognizer', async function () { - declarativeTestCase('07 - BeginDialog/BeginDialog.main.dialog', '07 - BeginDialog', async function(adapter){ - await adapter + const adapter = await declarativeTestCase('07 - BeginDialog/BeginDialog.main.dialog', '07 - BeginDialog'); + await adapter .send('hi') .assertReply('Hello, I\'m Zoidberg. What is your name?') .send('Bender') @@ -60,47 +60,41 @@ describe('TypeLoader', function () { .assertReply('Seeing into the future...') .assertReply('I see great things in your future...') .assertReply('Potentially a successful demo') - }); }); }); -function declarativeTestCase(path, resourcesFolder, callback) { - - readPackageJson(`tests/resources/${path}`, - async function (err, json) { - if (err) { - assert.fail(err); - return; - } - let loader = new TypeLoader(); +async function declarativeTestCase(path, resourcesFolder) { + const json = await readPackageJson(`tests/resources/${path}`); + let loader = new TypeLoader(); - if (resourcesFolder) { - let resourceProvider = new FileResourceProvider(); - resourceProvider.registerDirectory(`tests/resources`); - loader = new TypeLoader(null, resourceProvider) - } - - const dialog = await loader.load(json); + if (resourcesFolder) { + let resourceProvider = new FileResourceProvider(); + resourceProvider.registerDirectory(`tests/resources`); + loader = new TypeLoader(null, resourceProvider) + } + + const dialog = await loader.load(json); - // Create bots DialogManager and bind to state storage - const bot = new DialogManager(); - bot.storage = new MemoryStorage(); + // Create bots DialogManager and bind to state storage + const bot = new DialogManager(); + bot.storage = new MemoryStorage(); - const adapter = new TestAdapter(async turnContext => { - // Route activity to bot. - await bot.onTurn(turnContext); - }); + const adapter = new TestAdapter(async turnContext => { + // Route activity to bot. + await bot.onTurn(turnContext); + }); - bot.rootDialog = dialog; + bot.rootDialog = dialog; - await callback(adapter); - }); + return adapter; } -function readPackageJson(path, done) { - fs.readFile(path, function (err, buffer) { - if (err) { return done(err); } - var json = JSON.parse(buffer.toString().trim()); - return done(null, json); +function readPackageJson(path) { + return new Promise((resolve, reject) => { + fs.readFile(path, function (err, buffer) { + if (err) { reject(err); } + var json = JSON.parse(buffer.toString().trim()); + resolve(json); + }); }); } diff --git a/libraries/botbuilder-dialogs/.nycrc b/libraries/botbuilder-dialogs/.nycrc index 5e26d54160..a1fc7f3a77 100644 --- a/libraries/botbuilder-dialogs/.nycrc +++ b/libraries/botbuilder-dialogs/.nycrc @@ -9,7 +9,9 @@ "**/node_modules/**", "**/tests/**", "**/coverage/**", - "**/*.d.ts" + "**/*.d.ts", + "lib/choices/modelResult.js", + "lib/memory/pathResolvers/pathResolver.js" ], "reporter": [ "html" diff --git a/libraries/botbuilder-dialogs/package-lock.json b/libraries/botbuilder-dialogs/package-lock.json index bf0110872c..97408462fc 100644 --- a/libraries/botbuilder-dialogs/package-lock.json +++ b/libraries/botbuilder-dialogs/package-lock.json @@ -4,6 +4,183 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@microsoft/recognizers-text-choice": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-choice/-/recognizers-text-choice-1.1.4.tgz", + "integrity": "sha512-4CddwFe4RVhZeJgW65ocBrEdeukBMghK8pgI0K0Qy2eA5ysPZQpeZ7BGSDz5QMQei5LPY+QaAQ3CHU+ORHoO7A==", + "requires": { + "@microsoft/recognizers-text": "~1.1.4", + "grapheme-splitter": "^1.0.2" + }, + "dependencies": { + "@microsoft/recognizers-text": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text/-/recognizers-text-1.1.4.tgz", + "integrity": "sha512-hlSVXcaX5i8JcjuUJpVxmy2Z/GxvFXarF0KVySCFop57wNEnrLWMHe4I4DjP866G19VyIKRw+vPA32pkGhZgTg==" + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" + } + } + }, + "@microsoft/recognizers-text-date-time": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-date-time/-/recognizers-text-date-time-1.1.4.tgz", + "integrity": "sha512-leMnjN+KYNwNvRD5T4G0ORUzkjlek/BBZDvQIjAujtyrd/pkViUnuouWIPkFT/dbSOxXML8et54CSk2KfHiWIA==", + "requires": { + "@microsoft/recognizers-text": "~1.1.4", + "@microsoft/recognizers-text-number": "~1.1.4", + "@microsoft/recognizers-text-number-with-unit": "~1.1.4", + "lodash.isequal": "^4.5.0", + "lodash.tonumber": "^4.0.3" + }, + "dependencies": { + "@microsoft/recognizers-text": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text/-/recognizers-text-1.1.4.tgz", + "integrity": "sha512-hlSVXcaX5i8JcjuUJpVxmy2Z/GxvFXarF0KVySCFop57wNEnrLWMHe4I4DjP866G19VyIKRw+vPA32pkGhZgTg==" + }, + "@microsoft/recognizers-text-number-with-unit": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-number-with-unit/-/recognizers-text-number-with-unit-1.1.4.tgz", + "integrity": "sha512-zl+CfmfWK0x/x+iSgaBAevKTYO0F4+z7SYHAHztaaaGuX8FERw2jmUjSgVetm5KA3EveyCx0XYGU1mRNY8p7Eg==", + "requires": { + "@microsoft/recognizers-text": "~1.1.4", + "@microsoft/recognizers-text-number": "~1.1.4", + "lodash.escaperegexp": "^4.1.2", + "lodash.last": "^3.0.0", + "lodash.max": "^4.0.1" + } + }, + "lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, + "lodash.last": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash.last/-/lodash.last-3.0.0.tgz", + "integrity": "sha1-JC9mMRLdTG5jcoxgo8kJ0b2tvUw=" + }, + "lodash.max": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.max/-/lodash.max-4.0.1.tgz", + "integrity": "sha1-hzVWbGGLNan3YFILSHrnllivE2o=" + }, + "lodash.tonumber": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/lodash.tonumber/-/lodash.tonumber-4.0.3.tgz", + "integrity": "sha1-C5azGzVnJ5Prf1pj7nkfG56QJdk=" + } + } + }, + "@microsoft/recognizers-text-number": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-number/-/recognizers-text-number-1.1.4.tgz", + "integrity": "sha512-6EmlR+HR+eJBIX7sQby1vs6LJB64wxLowHaGpIU9OCXFvZ5Nb0QT8qh10rC40v3Mtrz4DpScXfSXr9tWkIO5MQ==", + "requires": { + "@microsoft/recognizers-text": "~1.1.4", + "bignumber.js": "^7.2.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.sortby": "^4.7.0", + "lodash.trimend": "^4.5.1" + }, + "dependencies": { + "@microsoft/recognizers-text": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text/-/recognizers-text-1.1.4.tgz", + "integrity": "sha512-hlSVXcaX5i8JcjuUJpVxmy2Z/GxvFXarF0KVySCFop57wNEnrLWMHe4I4DjP866G19VyIKRw+vPA32pkGhZgTg==" + }, + "bignumber.js": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", + "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==" + }, + "lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + }, + "lodash.trimend": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/lodash.trimend/-/lodash.trimend-4.5.1.tgz", + "integrity": "sha1-EoBENyhrmMrYmWt5QU4RMAEUCC8=" + } + } + }, + "@microsoft/recognizers-text-suite": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-suite/-/recognizers-text-suite-1.1.4.tgz", + "integrity": "sha512-hNIaR4M2G0nNeI9WZxt9C0KYh/1vhjeKzX5Ds8XDdT0pxF7zwCSo19WNcPjrVK6aCOeZTw/ULofsAjdu9gSkcA==", + "requires": { + "@microsoft/recognizers-text": "~1.1.4", + "@microsoft/recognizers-text-choice": "~1.1.4", + "@microsoft/recognizers-text-date-time": "~1.1.4", + "@microsoft/recognizers-text-number": "~1.1.4", + "@microsoft/recognizers-text-number-with-unit": "~1.1.4", + "@microsoft/recognizers-text-sequence": "~1.1.4" + }, + "dependencies": { + "@microsoft/recognizers-text": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text/-/recognizers-text-1.1.4.tgz", + "integrity": "sha512-hlSVXcaX5i8JcjuUJpVxmy2Z/GxvFXarF0KVySCFop57wNEnrLWMHe4I4DjP866G19VyIKRw+vPA32pkGhZgTg==" + }, + "@microsoft/recognizers-text-number-with-unit": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-number-with-unit/-/recognizers-text-number-with-unit-1.1.4.tgz", + "integrity": "sha512-zl+CfmfWK0x/x+iSgaBAevKTYO0F4+z7SYHAHztaaaGuX8FERw2jmUjSgVetm5KA3EveyCx0XYGU1mRNY8p7Eg==", + "requires": { + "@microsoft/recognizers-text": "~1.1.4", + "@microsoft/recognizers-text-number": "~1.1.4", + "lodash.escaperegexp": "^4.1.2", + "lodash.last": "^3.0.0", + "lodash.max": "^4.0.1" + } + }, + "@microsoft/recognizers-text-sequence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-sequence/-/recognizers-text-sequence-1.1.4.tgz", + "integrity": "sha512-rb5j8/aE7HSOdIxaVfCGFrj0wWPpSq0CuykFg/A/iJNPP+FnAU71bgP5HexrwQcpCsDinauisX7u0DKIChrHRA==", + "requires": { + "@microsoft/recognizers-text": "~1.1.4", + "grapheme-splitter": "^1.0.2" + } + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" + }, + "lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" + }, + "lodash.last": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash.last/-/lodash.last-3.0.0.tgz", + "integrity": "sha1-JC9mMRLdTG5jcoxgo8kJ0b2tvUw=" + }, + "lodash.max": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.max/-/lodash.max-4.0.1.tgz", + "integrity": "sha1-hzVWbGGLNan3YFILSHrnllivE2o=" + } + } + }, "mocha": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", diff --git a/libraries/botbuilder-dialogs/package.json b/libraries/botbuilder-dialogs/package.json index 03fecdb682..5d575a6f23 100644 --- a/libraries/botbuilder-dialogs/package.json +++ b/libraries/botbuilder-dialogs/package.json @@ -20,14 +20,14 @@ "main": "./lib/index.js", "typings": "./lib/index.d.ts", "dependencies": { - "@microsoft/recognizers-text-choice": "1.1.2", - "@microsoft/recognizers-text-date-time": "1.1.2", - "@microsoft/recognizers-text-number": "1.1.2", - "@microsoft/recognizers-text-suite": "1.1.2", - "@types/jsonpath": "^0.2.0", + "@microsoft/recognizers-text-choice": "1.1.4", + "@microsoft/recognizers-text-date-time": "1.1.4", + "@microsoft/recognizers-text-number": "1.1.4", + "@microsoft/recognizers-text-suite": "1.1.4", "@types/node": "^10.12.18", "botbuilder-core": "4.1.6", - "jsonpath": "^1.0.0" + "cldr-data": "^35.1.0", + "globalize": "^1.4.2" }, "devDependencies": { "@types/mocha": "^2.2.47", @@ -46,6 +46,6 @@ }, "files": [ "/lib", - "/src" + "/src" ] } diff --git a/libraries/botbuilder-dialogs/src/choices/choiceFactory.ts b/libraries/botbuilder-dialogs/src/choices/choiceFactory.ts index 07a3904c22..a22cc26c35 100644 --- a/libraries/botbuilder-dialogs/src/choices/choiceFactory.ts +++ b/libraries/botbuilder-dialogs/src/choices/choiceFactory.ts @@ -135,15 +135,15 @@ export class ChoiceFactory { } } - public static heroCard(choices: Choice[] = [], text = '', speak = ''): Activity { - const buttons: CardAction[] = choices.map(choice => ({ + public static heroCard(choices: (string | Choice)[] = [], text = '', speak = ''): Activity { + const buttons: CardAction[] = ChoiceFactory.toChoices(choices).map(choice => ({ title: choice.value, type: ActionTypes.ImBack, value: choice.value } as CardAction)); - const attachment = CardFactory.heroCard(null, text, null, buttons); + const attachment = CardFactory.heroCard(undefined, text, undefined, buttons); - return MessageFactory.attachment(attachment, null, speak, InputHints.ExpectingInput) as Activity; + return MessageFactory.attachment(attachment, undefined, speak, InputHints.ExpectingInput) as Activity; } diff --git a/libraries/botbuilder-dialogs/src/componentDialog.ts b/libraries/botbuilder-dialogs/src/componentDialog.ts index 45d556d670..e146348bbf 100644 --- a/libraries/botbuilder-dialogs/src/componentDialog.ts +++ b/libraries/botbuilder-dialogs/src/componentDialog.ts @@ -7,10 +7,8 @@ */ import { TurnContext, BotTelemetryClient, NullTelemetryClient } from 'botbuilder-core'; import { Dialog, DialogInstance, DialogReason, DialogTurnResult, DialogTurnStatus } from './dialog'; -import { DialogContext, DialogState } from './dialogContext'; -import { DialogSet } from './dialogSet'; +import { DialogContext } from './dialogContext'; import { DialogContainer } from './dialogContainer'; -import { StateMap } from './stateMap'; const PERSISTED_DIALOG_STATE = 'dialogs'; @@ -80,13 +78,9 @@ export class ComponentDialog extends DialogContainer { */ protected initialDialogId: string; - protected onComputeID(): string { - return `component[${this.bindingPath()}]`; - } - public async beginDialog(outerDC: DialogContext, options?: O): Promise { // Start the inner dialog. - const innerDC = this.createChildContext(outerDC); + const innerDC: DialogContext = this.createChildContext(outerDC) const turnResult: DialogTurnResult = await this.onBeginDialog(innerDC, options); // Check for end of inner dialog @@ -105,22 +99,17 @@ export class ComponentDialog extends DialogContainer { public async continueDialog(outerDC: DialogContext): Promise { // Continue execution of inner dialog. - const innerDC = this.createChildContext(outerDC); + const innerDC: DialogContext = this.createChildContext(outerDC) const turnResult: DialogTurnResult = await this.onContinueDialog(innerDC); // Check for end of inner dialog if (turnResult.status !== DialogTurnStatus.waiting) { - if (turnResult.status === DialogTurnStatus.cancelled) { - await this.endComponent(outerDC, turnResult.result); - const cancelledTurnResult: DialogTurnResult = { status: DialogTurnStatus.cancelled, result: turnResult.result } - return cancelledTurnResult; - } // Return result to calling dialog return await this.endComponent(outerDC, turnResult.result); - } else { - // Just signal end of turn - return Dialog.EndOfTurn; } + + // Just signal end of turn + return Dialog.EndOfTurn; } public async resumeDialog(dc: DialogContext, reason: DialogReason, result?: any): Promise { @@ -136,7 +125,7 @@ export class ComponentDialog extends DialogContainer { public async repromptDialog(context: TurnContext, instance: DialogInstance): Promise { // Forward to inner dialogs - const innerDC = this.createInnerDC(context, instance); + const innerDC: DialogContext = this.createInnerDC(context, instance); await innerDC.repromptDialog(); // Notify component. @@ -146,7 +135,7 @@ export class ComponentDialog extends DialogContainer { public async endDialog(context: TurnContext, instance: DialogInstance, reason: DialogReason): Promise { // Forward cancel to inner dialogs if (reason === DialogReason.cancelCalled) { - const innerDC = this.createInnerDC(context, instance); + const innerDC: DialogContext = this.createInnerDC(context, instance); await innerDC.cancelAllDialogs(); } @@ -154,12 +143,6 @@ export class ComponentDialog extends DialogContainer { await this.onEndDialog(context, instance, reason); } - public createChildContext(dc: DialogContext): DialogContext | undefined { - const childDC = this.createInnerDC(dc.context, dc.activeDialog, dc.state.user, dc.state.conversation); - childDC.parent = dc; - return childDC.stack.length > 0 ? childDC : undefined; - } - /** * Adds a child dialog or prompt to the components internal `DialogSet`. * @@ -169,12 +152,23 @@ export class ComponentDialog extends DialogContainer { * @param dialog The child dialog or prompt to add. */ public addDialog(dialog: Dialog): this { - super.addDialog(dialog); + this.dialogs.add(dialog); if (this.initialDialogId === undefined) { this.initialDialogId = dialog.id; } return this; } + /** + * Creates the inner dialog context + * @param outerDC the outer dialog context + */ + public createChildContext(outerDC: DialogContext) { + const innerDC = this.createInnerDC(outerDC.context, outerDC.activeDialog); + innerDC.parent = outerDC; + + return innerDC; + } + /** * Called anytime an instance of the component has been started. * @@ -241,6 +235,14 @@ export class ComponentDialog extends DialogContainer { return outerDC.endDialog(result); } + private createInnerDC(context: TurnContext, instance: DialogInstance) { + const dialogState = instance.state[PERSISTED_DIALOG_STATE] || { dialogStack: [] }; + instance.state[PERSISTED_DIALOG_STATE] = dialogState + const innerDC: DialogContext = new DialogContext(this.dialogs, context, dialogState); + + return innerDC + } + /** * Set the telemetry client, and also apply it to all child dialogs. * Future dialogs added to the component will also inherit this client. @@ -256,11 +258,4 @@ export class ComponentDialog extends DialogContainer { public get telemetryClient(): BotTelemetryClient { return this._telemetryClient; } - - private createInnerDC(context: TurnContext, instance: DialogInstance, userState?: StateMap, conversationState?: StateMap): DialogContext { - const state: DialogState = instance.state[PERSISTED_DIALOG_STATE]; - if (!Array.isArray(state.dialogStack)) { state.dialogStack = [] } - const innerDC: DialogContext = new DialogContext(this.dialogs, context, state, userState || new StateMap({}), conversationState || new StateMap({})); - return innerDC; - } } diff --git a/libraries/botbuilder-dialogs/src/dialog.ts b/libraries/botbuilder-dialogs/src/dialog.ts index 98e314a299..230629a304 100644 --- a/libraries/botbuilder-dialogs/src/dialog.ts +++ b/libraries/botbuilder-dialogs/src/dialog.ts @@ -12,7 +12,7 @@ import { Configurable } from './configurable'; /** * Tracking information persisted for an instance of a dialog on the stack. */ -export interface DialogInstance { +export interface DialogInstance { /** * ID of the dialog this instance is for. */ @@ -25,7 +25,7 @@ export interface DialogInstance { * When the dialog referenced by [id](#id) derives from `DialogCommand`, the state field will * contain the stack index of the state object that should be inherited by the command. */ - state: any; + state: T; } /** @@ -90,6 +90,35 @@ export enum DialogTurnStatus { cancelled = 'cancelled' } +export interface DialogEvent { + /** + * Flag indicating whether the event will be bubbled to the parent `DialogContext`. + */ + bubble: boolean; + + /** + * Name of the event being raised. + */ + name: string; + + /** + * Optional. Value associated with the event. + */ + value?: any; +} + +export interface DialogConfiguration { + /** + * Static id of the dialog. + */ + id?: string; + + /** + * Telemetry client the dialog should use. + */ + telemetryClient?: BotTelemetryClient; +} + /** * Returned by `Dialog.continueDialog()` and `DialogContext.beginDialog()` to indicate whether a * dialog is still active after the turn has been processed by the dialog. @@ -137,7 +166,7 @@ export interface DialogTurnResult { parentEnded?: boolean; } -export interface DialogEvent { +export interface DialogEvent { /** * If `true` the event will be bubbled to the parent `DialogContext` if not handled by the * current dialog. @@ -152,7 +181,7 @@ export interface DialogEvent { /** * (Optional) value associated with the event. */ - value?: T; + value?: any; } export interface DialogConfiguration { @@ -160,7 +189,7 @@ export interface DialogConfiguration { tags?: string[]; - inputBindings?: { [option:string]: string; }; + inputBindings?: { [option: string]: string; }; outputBinding?: string; @@ -178,41 +207,19 @@ export abstract class Dialog extends Configurable { */ public static EndOfTurn: DialogTurnResult = { status: DialogTurnStatus.waiting }; - /** - * (Optional) set of tags assigned to the dialog. - */ - public readonly tags: string[] = []; - - /** - * (Optional) if true, the dialog will inherit its parents state. - */ - public inheritState = false; - - /** - * (Optional) JSONPath expression for the memory slots to bind the dialogs options to on a - * call to `beginDialog()`. - */ - public readonly inputProperties: { [option:string]: string; } = {}; - - /** - * (Optional) JSONPath expression for the memory slot to bind the dialogs result to when - * `endDialog()` is called. - */ - public outputProperty: string; - /** * The telemetry client for logging events. * Default this to the NullTelemetryClient, which does nothing. */ - protected _telemetryClient: BotTelemetryClient = new NullTelemetryClient(); + protected _telemetryClient: BotTelemetryClient = new NullTelemetryClient(); /** * Creates a new Dialog instance. - * @param dialogId (Optional) unique ID to assign to the dialog. + * @param dialogId Optional. unique ID of the dialog. */ constructor(dialogId?: string) { super(); - this._id = dialogId; + this.id = dialogId; } /** @@ -223,7 +230,7 @@ export abstract class Dialog extends Configurable { */ public get id(): string { if (this._id === undefined) { - this._id = this.onComputeID(); + this._id = this.onComputeId(); } return this._id; } @@ -294,34 +301,6 @@ export abstract class Dialog extends Configurable { return dc.endDialog(result); } - /** - * Called when an event has been raised, using `DialogContext.emitEvent()`. - * - * @remarks - * The dialog is responsible for bubbling the event up to its parent dialog. In most cases - * developers should override `onPreBubbleEvent()` or `onPostBubbleEvent()` versus - * overriding `onDialogEvent()` directly. - * @param dc The dialog context for the current turn of conversation. - * @param event The event being raised. - * @returns `true` if the event is handled by the current dialog and bubbling should stop. - */ - public async onDialogEvent(dc: DialogContext, event: DialogEvent): Promise { - // Before bubble - let handled = await this.onPreBubbleEvent(dc, event); - - // Bubble as needed - if (!handled && event.bubble && dc.parent) { - handled = await dc.parent.emitEvent(event.name, event.value, true); - } - - // Post bubble - if (!handled) { - handled = await this.onPostBubbleEvent(dc, event); - } - - return handled; - } - /** * Called when the dialog has been requested to re-prompt the user for input. * @@ -349,15 +328,28 @@ export abstract class Dialog extends Configurable { // No-op by default } - /** - * Called when a unique ID needs to be computed for a dialog. - * - * @remarks - * SHOULD be overridden to provide a more contextually relevant ID. The default implementation - * returns `dialog[${this.bindingPath()}]`. - */ - protected onComputeID(): string { - return `dialog[${this.bindingPath()}]`; + /// + /// Called when an event has been raised, using `DialogContext.emitEvent()`, by either the current dialog or a dialog that the current dialog started. + /// + /// The dialog context for the current turn of conversation. + /// The event being raised. + /// The cancellation token. + /// True if the event is handled by the current dialog and bubbling should stop. + public async onDialogEvent(dc: DialogContext, e: DialogEvent): Promise { + // Before bubble + let handled = await this.onPreBubbleEventAsync(dc, e); + + // Bubble as needed + if (!handled && e.bubble && dc.parent != undefined) { + handled = await dc.parent.emitEvent(e.name, e.value, true, false); + } + + // Post bubble + if (!handled) { + handled = await this.onPostBubbleEventAsync(dc, e); + } + + return handled; } /** @@ -368,10 +360,10 @@ export abstract class Dialog extends Configurable { * any further bubbling of the event to the dialogs parents and will also prevent any child * dialogs from performing their default processing. * @param dc The dialog context for the current turn of conversation. - * @param event The event being raised. - * @returns `true` if the event is handled by the current dialog and further processing should stop. + * @param e The event being raised. + * @returns Whether the event is handled by the current dialog and further processing should stop. */ - protected async onPreBubbleEvent(dc: DialogContext, event: DialogEvent): Promise { + protected async onPreBubbleEventAsync(dc: DialogContext, e: DialogEvent): Promise { return false; } @@ -382,27 +374,22 @@ export abstract class Dialog extends Configurable { * This is a good place to perform default processing logic for an event. Returning `true` will * prevent any processing of the event by child dialogs. * @param dc The dialog context for the current turn of conversation. - * @param event The event being raised. - * @returns `true` if the event is handled by the current dialog and further processing should stop. + * @param e The event being raised. + * @returns Whether the event is handled by the current dialog and further processing should stop. */ - protected async onPostBubbleEvent(dc: DialogContext, event: DialogEvent): Promise { + protected async onPostBubbleEventAsync(dc: DialogContext, e: DialogEvent): Promise { return false; } /** - * Aids in computing a unique ID for a dialog by returning the current input or output property - * the dialog is bound to. - * @param hashOutput (Optional) if true the output will be hashed to less than 15 characters before returning. + * Called when a unique ID needs to be computed for a dialog. + * + * @remarks + * SHOULD be overridden to provide a more contextually relevant ID. The preferred pattern for + * ID's is `(this.hashedLabel(''))`. */ - protected bindingPath(hashOutput = true): string { - let output = ''; - if (this.inputProperties.hasOwnProperty('value') && this.inputProperties['value']) { - output = this.inputProperties['value']; - } else if (this.outputProperty && this.outputProperty.length) { - output = this.outputProperty; - } - - return hashOutput ? this.hashedLabel(output) : output; + protected onComputeId(): string { + throw new Error(`Dialog.onComputeId(): not implemented.`) } /** @@ -418,12 +405,11 @@ export abstract class Dialog extends Configurable { */ protected hashedLabel(label: string): string { const l = label.length; - if (label.length > 15) - { + if (label.length > 15) { let hash = 0; for (let i = 0; i < l; i++) { const chr = label.charCodeAt(i); - hash = ((hash << 5) - hash) + chr; + hash = ((hash << 5) - hash) + chr; hash |= 0; // Convert to 32 bit integer } label = `${label.substr(0, 5)}${hash.toString()}`; diff --git a/libraries/botbuilder-dialogs/src/dialogCommand.ts b/libraries/botbuilder-dialogs/src/dialogCommand.ts index 99f333c9e2..e16810a218 100644 --- a/libraries/botbuilder-dialogs/src/dialogCommand.ts +++ b/libraries/botbuilder-dialogs/src/dialogCommand.ts @@ -14,12 +14,15 @@ export abstract class DialogCommand extends Dialog imp constructor(dialogId?: string) { super(dialogId); - this.inheritState = true; } public getDependencies(): Dialog[] { return []; } + + protected onComputeId(): string { + return `${typeof(this)}[${this.id}]`; + } public beginDialog(dc: DialogContext, options?: O): Promise { return this.onRunCommand(dc, options); diff --git a/libraries/botbuilder-dialogs/src/dialogContainer.ts b/libraries/botbuilder-dialogs/src/dialogContainer.ts index 05da581942..4e7132f106 100644 --- a/libraries/botbuilder-dialogs/src/dialogContainer.ts +++ b/libraries/botbuilder-dialogs/src/dialogContainer.ts @@ -6,37 +6,27 @@ * Licensed under the MIT License. */ import { Dialog } from './dialog'; -import { DialogContext } from './dialogContext'; import { DialogSet } from './dialogSet'; +import { DialogContext } from './dialogContext'; export abstract class DialogContainer extends Dialog { - protected readonly dialogs: DialogSet = new DialogSet(null); - /** - * Creates a new `DialogContext` for the containers active child. - * - * @remarks - * Returns `undefined` if the container has no active children. - * @param dc Dialog context for the active container instance. + * The containers dialog set. */ - public abstract createChildContext(dc: DialogContext): DialogContext | undefined; + protected readonly dialogs = new DialogSet(undefined); /** - * Adds a child dialog or prompt to the containers internal `DialogSet`. - * @param dialog The child dialog or prompt to add. + * Creates an inner dialog context for the containers active child. + * @param dc Parents dialog context. + * @returns A new dialog context for the active child or `undefined` if there is no active child. */ - public addDialog(dialog: Dialog): this { - this.dialogs.add(dialog); - return this; - } + public abstract createChildContext(dc: DialogContext): DialogContext | undefined; /** - * Finds a child dialog that was previously added to the container using - * [addDialog()](#adddialog). - * @param dialogId ID of the dialog or prompt to lookup. + * Finds a child dialog that was previously added to the container. + * @param dialogId ID of the dialog to lookup. */ public findDialog(dialogId: string): Dialog | undefined { return this.dialogs.find(dialogId); } - } \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/dialogContext.ts b/libraries/botbuilder-dialogs/src/dialogContext.ts index 4efe888983..e8fe5afadf 100644 --- a/libraries/botbuilder-dialogs/src/dialogContext.ts +++ b/libraries/botbuilder-dialogs/src/dialogContext.ts @@ -1,6 +1,3 @@ -/** - * @module botbuilder-dialogs - */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. @@ -10,131 +7,109 @@ import { Choice } from './choices'; import { Dialog, DialogInstance, DialogReason, DialogTurnResult, DialogTurnStatus, DialogEvent } from './dialog'; import { DialogSet } from './dialogSet'; import { PromptOptions } from './prompts'; -import { StateMap } from './stateMap'; -import { DialogContextState } from './dialogContextState'; -import { DialogCommand } from './dialogCommand'; +import { DialogStateManager } from './memory'; import { DialogContainer } from './dialogContainer'; +import { DialogEvents } from './dialogEvents'; /** - * State information persisted by a `DialogSet`. + * @private + */ +const ACTIVITY_RECEIVED_EMITTED = Symbol('ActivityReceivedEmitted'); + +/** + * Contains dialog state, information about the state of the dialog stack, for a specific [DialogSet](xref:botbuilder-dialogs.DialogSet). + * + * @remarks + * State is read from and saved to storage each turn, and state cache for the turn is managed through + * the [TurnContext](xref:botbuilder-core.TurnContext). + * + * For more information, see the articles on + * [Managing state](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-state) and + * [Dialogs library](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-dialog). */ export interface DialogState { /** - * The dialog stack being persisted. + * Contains state information for each [Dialog](xref:botbuilder-dialogs.Dialog) on the stack. */ dialogStack: DialogInstance[]; - - /** - * (optional) values that are persisted for the lifetime of the conversation. - * - * @remarks - * These values are intended to be transient and may automatically expire after some timeout - * period. - */ - conversationState?: object; - - /** - * (Optional) values that are persisted across all interactions with the current user. - */ - userState?: object; } /** - * A context object used to start and stop dialogs within a `DialogContainer`. + * The context for the current dialog turn with respect to a specific [DialogSet](xref:botbuilder-dialogs.DialogSet). * * @remarks - * Every active DialogContainer instance has its own DialogContext which can be used to start and - * stop dialogs within that container. The [parent](#parent) and [child](#child) properties can - * be used to navigate the bots stack of active dialog containers. + * This includes the turn context, information about the dialog set, and the state of the dialog stack. + * + * Use a dialog set's [createContext](xref:botbuilder-dialogs.DialogSet.createContext) method to create the dialog context. + * Use the methods of the dialog context to manage the progression of dialogs in the set. + * + * For example: + * ```JavaScript + * const dc = await dialogs.createContext(turnContext); + * const result = await dc.continueDialog(); + * ``` */ export class DialogContext { /** - * Set of dialogs that can be called from this dialog context. + * Gets the dialogs that can be called directly from this context. */ public readonly dialogs: DialogSet; /** - * Context for the current turn of conversation. + * Gets the context object for the turn. */ public readonly context: TurnContext; /** - * Current dialog stack. + * Gets the current dialog stack. */ public readonly stack: DialogInstance[]; + public readonly state: DialogStateManager; + /** - * In-memory properties that are currently visible to the active dialog. + * The parent dialog context for this dialog context, or `undefined` if this context doesn't have a parent. + * + * @remarks + * When it attempts to start a dialog, the dialog context searches for the [Dialog.id](xref:botbuilder-dialogs.Dialog.id) + * in its [dialogs](xref:botbuilder-dialogs.DialogContext.dialogs). If the dialog to start is not found + * in this dialog context, it searches in its parent dialog context, and so on. */ - public readonly state: DialogContextState; + public parent: DialogContext|undefined; /** - * Creates a new DialogContext instance. - * @param dialogs Parent dialog set. - * @param context Context for the current turn of conversation with the user. - * @param state State object being used to persist the dialog stack. - * @param userState (Optional) user values to bind context to. If not specified, a new set of user values will be persisted off the passed in `state` property. - * @param conversationState (Optional) conversation values to bind context to. If not specified, a new set of conversation values will be persisted off the passed in `state` property. - */ - constructor(dialogs: DialogSet, context: TurnContext, state: DialogState, userState?: StateMap, conversationState?: StateMap) { + * Creates an new instance of the [DialogContext](xref:botbuilder-dialogs.DialogContext) class. + * + * @param dialogs The dialog set for which to create the dialog context. + * @param context The context object for the current turn of the bot. + * @param state The state object to use to read and write dialog state to storage. + */ + constructor(dialogs: DialogSet, context: TurnContext, state: DialogState) { if (!Array.isArray(state.dialogStack)) { state.dialogStack = []; } this.dialogs = dialogs; this.context = context; this.stack = state.dialogStack; - if (!conversationState) { - // Create a new session state map - if (typeof state.conversationState !== 'object') { state.conversationState = {}; } - conversationState = new StateMap(state.conversationState); - } - if (!userState) { - // Create a new session state map - if (typeof state.userState !== 'object') { state.userState = {}; } - userState = new StateMap(state.userState); - } - this.state = new DialogContextState(this, userState, conversationState); + this.state = new DialogStateManager(this); } /** - * Returns the persisted instance of the active dialog on the top of the stack or `undefined` if + * Returns the state information for the dialog on the top of the dialog stack, or `undefined` if * the stack is empty. - * - * @remarks - * Dialogs can use this to persist custom state in between conversation turns: */ public get activeDialog(): DialogInstance|undefined { - let instance: DialogInstance; - if (this.stack.length > 0) { - // For DialogCommand instances we need to return the inherited state. - const frame = this.stack[this.stack.length - 1]; - instance = { - id: frame.id, - state: this.getActiveDialogState(this, frame.state) - }; - } - return instance; + return this.stack.length > 0 ? this.stack[this.stack.length - 1] : undefined; } /** - * A DialogContext for manipulating the stack of current containers parent. - * - * @remarks - * Returns `undefined` if the current container is the [rootParent](#rootparent). - */ - public parent: DialogContext|undefined; - - /** - * A DialogContext for manipulating the stack of a child container. - * - * @remarks - * Returns `undefined` if the [activeDialog](#activedialog) isn't an instance of a - * DialogContainer or the container has no active child dialogs. + * Returns dialog context for child if the active dialog is a container. */ public get child(): DialogContext|undefined { - const instance = this.activeDialog; - if (instance) { + var instance = this.activeDialog; + if (instance != undefined) { + // Is active dialog a container? const dialog = this.findDialog(instance.id); if (dialog instanceof DialogContainer) { - return (dialog as DialogContainer).createChildContext(this); + return dialog.createChildContext(this); } } @@ -142,66 +117,42 @@ export class DialogContext { } /** - * Pushes a new dialog onto the dialog stack. + * Starts a dialog instance and pushes it onto the dialog stack. + * Creates a new instance of the dialog and pushes it onto the stack. * + * @param dialogId ID of the dialog to start. + * @param options Optional. Arguments to pass into the dialog when it starts. + * * @remarks - * If there's already an active dialog on the stack, that dialog will be paused until the new - * dialog calls [endDialog()](#enddialog). + * If there's already an active dialog on the stack, that dialog will be paused until + * it is again the top dialog on the stack. + * + * The [status](xref:botbuilder-dialogs.DialogTurnResult.status) of returned object describes + * the status of the dialog stack after this method completes. * + * This method throws an exception if the requested dialog can't be found in this dialog context + * or any of its ancestors. + * + * For example: * ```JavaScript - * return await dc.beginDialog('greeting', { name: user.name }); + * const result = await dc.beginDialog('greeting', { name: user.name }); * ``` - * - * The `DialogTurnResult.status` returned can be: - * - `DialogTurnStatus.active` if the dialog started was a multi-turn dialog. - * - `DialogTurnStatus.completed` if the dialog started was a single-turn dialog. - * @param dialogId ID of the dialog to start. - * @param options (Optional) additional argument(s) to pass to the dialog being started. + * + * **See also** + * - [endDialog](xref:botbuilder-dialogs.DialogContext.endDialog) + * - [prompt](xref:botbuilder-dialogs.DialogContext.prompt) + * - [replaceDialog](xref:botbuilder-dialogs.DialogContext.replaceDialog) + * - [Dialog.beginDialog](xref:botbuilder-dialogs.Dialog.beginDialog) */ public async beginDialog(dialogId: string, options?: object): Promise { // Lookup dialog const dialog: Dialog<{}> = this.findDialog(dialogId); if (!dialog) { throw new Error(`DialogContext.beginDialog(): A dialog with an id of '${ dialogId }' wasn't found.`); } - // Process dialogs input bindings - // - If the stack is empty, any `dialog.` bindings will be pulled from the active dialog on - // the parents stack. - options = options || {}; - for(const option in dialog.inputProperties) { - if (dialog.inputProperties.hasOwnProperty(option)) { - const binding = dialog.inputProperties[option]; - if (binding) { - const value = this.state.getValue(binding); - options[option] = Array.isArray(value) || typeof value === 'object' ? JSON.parse(JSON.stringify(value)) : value; - } - } - } - - // Check for inherited state - // - Local stack references are positive numbers and negative numbers are references on the - // parents stack. - let state: number|object; - if (dialog.inheritState) { - if (this.stack.length > 0) { - state = this.stack.length - 1; - } else if (this.parent) { - // We can't use -0 so index 0 in the parents stack is encoded as -1. - state = 0 - this.parent.stack.length; - } - // Find stack entry to inherit - for (let i = this.stack.length - 1; i >= 0; i--) { - if (typeof this.stack[i] === 'object') { - state = i; - break; - } - } - } - if (state == undefined) { state = {} } - // Push new instance onto stack. - const instance: DialogInstance = { + const instance: DialogInstance = { id: dialogId, - state: state + state: {} }; this.stack.push(instance); @@ -210,87 +161,73 @@ export class DialogContext { } /** - * Cancels any dialogs on the stack resulting in an empty stack. - * @param eventName (Optional) name of event to bubble up as dialogs are cancelled. Defaults to `cancelDialog`. - * @param eventValue (Optional) value to pass with event. + * Cancels all dialogs on the dialog stack, and clears stack. + * + * @param cancelParents Optional. If `true` all parent dialogs will be cancelled as well. + * @param eventName Optional. Name of a custom event to raise as dialogs are cancelled. This defaults to [cancelDialog](xref:botbuilder-dialogs.DialogEvents.cancelDialog). + * @param eventValue Optional. Value to pass along with custom cancellation event. + * + * @remarks + * This calls each dialog's [Dialog.endDialog](xref:botbuilder-dialogs.Dialog.endDialog) method before + * removing the dialog from the stack. + * + * If there were any dialogs on the stack initially, the [status](xref:botbuilder-dialogs.DialogTurnResult.status) + * of the return value is [cancelled](xref:botbuilder-dialogs.DialogTurnStatus.cancelled); otherwise, it's + * [empty](xref:botbuilder-dialogs.DialogTurnStatus.empty). + * + * This example clears a dialog stack, `dc`, before starting a 'bookFlight' dialog. + * ```JavaScript + * await dc.cancelAllDialogs(); + * return await dc.beginDialog('bookFlight'); + * ``` + * + * **See also** + * - [endDialog](xref:botbuilder-dialogs.DialogContext.endDialog) */ - public async cancelAllDialogs(eventName = 'cancelDialog', eventValue?: any): Promise { - if (this.stack.length > 0 || this.parent) { + public async cancelAllDialogs(cancelParents = false, eventName?: string, eventValue?: any): Promise { + eventName = eventName || DialogEvents.cancelDialog; + if (this.stack.length > 0 || this.parent != undefined) { // Cancel all local and parent dialogs while checking for interception let notify = false; let dc: DialogContext = this; - while (dc) { - if (dc.stack.length > 0) - { + while (dc != undefined) { + if (dc.stack.length > 0) { // Check to see if the dialog wants to handle the event - if (notify && await dc.emitEvent(eventName, eventValue, false)) { - // Event handled so stop canceling dialogs - break; + // - We skip notifying the first dialog which actually called cancelAllDialogs() + if (notify) { + const handled = await dc.emitEvent(eventName, eventValue, false, false); + if (handled) { + break; + } } // End the active dialog await dc.endActiveDialog(DialogReason.cancelCalled); } else { - dc = dc.parent; + dc = cancelParents ? dc.parent : undefined; } + notify = true; } return { status: DialogTurnStatus.cancelled }; } else { - // Stack was empty and no parent return { status: DialogTurnStatus.empty }; } } /** - * Emits a named event for the current dialog, or someone who started it, to handle. - * @param name Name of the event to raise. - * @param value (Optional) value to send along with the event. - * @param bubble (Optional) flag to control whether the event should be bubbled to its parent if not handled locally. Defaults to a value of `true`. - * @param fromLeaf (Optional) if `true` the event will be emitted starting with the leaf most child dialog. Defaults to a value of `false`. - */ - public async emitEvent(name: string, value?: any, bubble = true, fromLeaf = false): Promise { - // Initialize event - const event: DialogEvent = { - bubble: bubble, - name: name, - value: value - }; - - // Find starting dialog - let dc: DialogContext = this; - if (fromLeaf) { - while (true) { - const childDC = dc.child; - if (childDC) { - dc = childDC; - } else { - break; - } - } - } - - // Dispatch to active dialog - // - The dialog is responsible for bubbling the event to its parent - const instance = dc.activeDialog; - if (instance) { - const dialog = dc.findDialog(instance.id); - if (dialog) { - return await dialog.onDialogEvent(dc, event); - } - } - - return false; - } - - /** - * Searches for a dialog to begin or replace. - * - * @remarks - * If the dialog cannot be found within the current `DialogSet`, the parent `DialogContext` - * will be searched as well. + * Searches for a dialog with a given ID. + * * @param dialogId ID of the dialog to search for. + * + * @remarks + * If the dialog to start is not found in the [DialogSet](xref:botbuilder-dialogs.DialogSet) associated + * with this dialog context, it attempts to find the dialog in its parent dialog context. + * + * **See also** + * - [dialogs](xref:botbuilder-dialogs.DialogContext.dialogs) + * - [parent](xref:botbuilder-dialogs.DialogContext.parent) */ public findDialog(dialogId: string): Dialog|undefined { let dialog = this.dialogs.find(dialogId); @@ -301,18 +238,22 @@ export class DialogContext { } /** - * Helper function to simplify formatting the options for calling a `Prompt` based dialog. - * + * Helper function to simplify formatting the options for calling a prompt dialog. + * + * @param dialogId ID of the prompt dialog to start. + * @param promptOrOptions The text of the initial prompt to send the user, + * the activity to send as the initial prompt, or + * the object with which to format the prompt dialog. + * @param choices Optional. Array of choices for the user to choose from, + * for use with a [ChoicePrompt](xref:botbuilder-dialogs.ChoicePrompt). + * * @remarks - * This is a lightweight wrapper abound [beginDialog()](#begindialog). It fills in a - * `PromptOptions` structure and then passes it through to `dc.beginDialog(dialogId, options)`. + * This helper method formats the object to use as the `options` parameter, and then calls + * [beginDialog](xref:botbuilder-dialogs.DialogContext.beginDialog) to start the specified prompt dialog. * * ```JavaScript * return await dc.prompt('confirmPrompt', `Are you sure you'd like to quit?`); * ``` - * @param dialogId ID of the prompt to start. - * @param promptOrOptions Initial prompt to send the user or a set of options to configure the prompt with. - * @param choices (Optional) array of choices associated with the prompt. */ public async prompt(dialogId: string, promptOrOptions: string | Partial | PromptOptions): Promise; public async prompt(dialogId: string, promptOrOptions: string | Partial | PromptOptions, choices: (string | Choice)[]): Promise; @@ -332,21 +273,28 @@ export class DialogContext { options = { ...promptOrOptions as PromptOptions }; } - if (choices) + if (choices) { options.choices = choices; } - + return this.beginDialog(dialogId, options); } /** - * Continues execution of the active multi-turn dialog, if there is one. + * Continues execution of the active dialog, if there is one, by passing this dialog context to its + * [Dialog.continueDialog](xref:botbuilder-dialogs.Dialog.continueDialog) method. * * @remarks - * The stack will be inspected and the active dialog will be retrieved using `DialogSet.find()`. - * The dialog will then have its `Dialog.continueDialog()` method called. + * After the call completes, you can check the turn context's [responded](xref:botbuilder-core.TurnContext.responded) + * property to determine if the dialog sent a reply to the user. + * + * The [status](xref:botbuilder-dialogs.DialogTurnResult.status) of returned object describes + * the status of the dialog stack after this method completes. * + * Typically, you would call this from within your bot's turn handler. + * + * For example: * ```JavaScript * const result = await dc.continueDialog(); * if (result.status == DialogTurnStatus.empty && dc.context.activity.type == ActivityTypes.message) { @@ -354,20 +302,25 @@ export class DialogContext { * await dc.context.sendActivity(`I'm sorry. I didn't understand.`); * } * ``` - * - * The `DialogTurnResult.status` returned can be: - * - `DialogTurnStatus.active` if there's still one or more dialogs on the stack. - * - `DialogTurnStatus.completed` if the last dialog on the stack just ended. - * - `DialogTurnStatus.empty` if the stack was empty. */ public async continueDialog(): Promise { + // if we are continuing and haven't emitted the activityReceived event, emit it + // NOTE: This is backward compatible way for activity received to be fired even if you have legacy dialog loop + if (!this.context.turnState.has(ACTIVITY_RECEIVED_EMITTED)) { + this.context.turnState.set(ACTIVITY_RECEIVED_EMITTED, true); + + // Dispatch "activityReceived" event + // - This fired from teh leaf and will queue up any interruptions. + await this.emitEvent(DialogEvents.activityReceived, this.context.activity, true, true); + } + // Check for a dialog on the stack - const instance: DialogInstance = this.activeDialog; + const instance: DialogInstance = this.activeDialog; if (instance) { // Lookup dialog const dialog: Dialog<{}> = this.findDialog(instance.id); if (!dialog) { - throw new Error(`DialogContext.continue(): Can't continue dialog. A dialog with an id of '${ instance.id }' wasn't found.`); + throw new Error(`DialogContext.continueDialog(): Can't continue dialog. A dialog with an id of '${ instance.id }' wasn't found.`); } // Continue execution of dialog @@ -378,36 +331,46 @@ export class DialogContext { } /** - * Ends a dialog by popping it off the stack and returns an optional result to the dialogs - * parent. + * Ends a dialog and pops it off the stack. Returns an optional result to the dialog's parent. * + * @param result Optional. A result to pass to the parent logic. This might be the next dialog + * on the stack, or it might be the bot's turn handler, if this was the last dialog on the stack. + * * @remarks - * The parent dialog is the dialog the started the one being ended via a call to either - * [beginDialog()](#begindialog) or [prompt()](#prompt). The parent dialog will have its - * `Dialog.resumeDialog()` method called with any returned `result`. If there is no parent - * dialog then turn will end and the result will be passed to the bot via - * `DialogTurnResult.result`. + * The _parent_ dialog is the next dialog on the dialog stack, if there is one. This method + * calls the parent's [Dialog.resumeDialog](xref:botbuilder-dialogs.Dialog.resumeDialog) method, + * passing the result returned by the ending dialog. If there is no parent dialog, the turn ends + * and the result is available to the bot through the returned object's + * [result](xref:botbuilder-dialogs.DialogTurnResult.result) property. * + * The [status](xref:botbuilder-dialogs.DialogTurnResult.status) of returned object describes + * the status of the dialog stack after this method completes. + * + * Typically, you would call this from within the logic for a specific dialog to signal back to + * the dialog context that the dialog has completed, the dialog should be removed from the stack, + * and the parent dialog should resume. + * + * For example: * ```JavaScript - * return await dc.endDialog(); + * return await dc.endDialog(returnValue); * ``` - * - * The `DialogTurnResult.status` returned can be: - * - `DialogTurnStatus.active` the parent dialog was resumed and is still active. - * - `DialogTurnStatus.completed` the parent dialog completed or there was no parent dialog to resume. - * @param result (Optional) result to pass to the parent dialogs `Dialog.resume()` method. + * + * **See also** + * - [beginDialog](xref:botbuilder-dialogs.DialogContext.beginDialog) + * - [replaceDialog](xref:botbuilder-dialogs.DialogContext.replaceDialog) + * - [Dialog.endDialog](xref:botbuilder-dialogs.Dialog.endDialog) */ public async endDialog(result?: any): Promise { // End the active dialog - await this.endActiveDialog(DialogReason.endCalled, result); + await this.endActiveDialog(DialogReason.endCalled); // Resume parent dialog - const instance: DialogInstance = this.activeDialog; + const instance: DialogInstance = this.activeDialog; if (instance) { // Lookup dialog const dialog: Dialog<{}> = this.findDialog(instance.id); if (!dialog) { - throw new Error(`DialogContext.endDialog(): Can't resume previous dialog. A dialog with an id of '${instance.id}' wasn't found.`); + throw new Error(`DialogContext.endDialog(): Can't resume previous dialog. A dialog with an id of '${ instance.id }' wasn't found.`); } // Return result to previous dialog @@ -421,33 +384,21 @@ export class DialogContext { /** * Ends the active dialog and starts a new dialog in its place. * + * @param dialogId ID of the dialog to start. + * @param options Optional. Arguments to pass into the new dialog when it starts. + * * @remarks - * This method is conceptually equivalent to calling [endDialog()](#enddialog) and then - * immediately calling [beginDialog()](#begindialog). The difference is that the parent - * dialog is not resumed or otherwise notified that the dialog it started has been replaced. - * - * This method is particularly useful for creating conversational loops within your bot: + * This is particularly useful for creating a loop or redirecting to another dialog. * - * ```JavaScript - * this.addDialog(new WaterfallDialog('randomNumber', [ - * async (step) => { - * const { min, max } = step.options; - * const num = min + Math.floor((max - min) * Math.random()); - * return await step.prompt('continuePrompt', `Here's a number between ${min} and ${max}: ${num}. Should I pick another one?`); - * }, - * async (step) { - * if (step.result) { - * return await step.replaceDialog(this.id, step.options); - * } else { - * return await step.endDialog(); - * } - * } - * ])); + * The [status](xref:botbuilder-dialogs.DialogTurnResult.status) of returned object describes + * the status of the dialog stack after this method completes. + * + * This method is similar to ending the current dialog and immediately beginning the new one. + * However, the parent dialog is neither resumed nor otherwise notified. * - * this.addDialog(new ConfirmPrompt('continuePrompt')); - * ``` - * @param dialogId ID of the new dialog to start. - * @param options (Optional) additional argument(s) to pass to the new dialog. + * **See also** + * - [beginDialog](xref:botbuilder-dialogs.DialogContext.beginDialog) + * - [endDialog](xref:botbuilder-dialogs.DialogContext.endDialog) */ public async replaceDialog(dialogId: string, options?: object): Promise { // End the active dialog @@ -458,26 +409,27 @@ export class DialogContext { } /** - * Requests the [activeDialog](#activeDialog) to re-prompt the user for input. + * Requests the active dialog to re-prompt the user for input. * * @remarks - * The active dialogs `Dialog.repromptDialog()` method will be called. + * This calls the active dialog's [repromptDialog](xref:botbuilder-dialogs.Dialog.repromptDialog) method. * + * For example: * ```JavaScript * await dc.repromptDialog(); * ``` */ public async repromptDialog(): Promise { - // Emit 'repromptDialog' event first - const handled = await this.emitEvent('repromptDialog', undefined, false); + // Try raising event first + const handled = await this.emitEvent(DialogEvents.repromptDialog, undefined, false, false); if (!handled) { // Check for a dialog on the stack - const instance: DialogInstance = this.activeDialog; + const instance: DialogInstance = this.activeDialog; if (instance) { // Lookup dialog const dialog: Dialog<{}> = this.findDialog(instance.id); if (!dialog) { - throw new Error(`DialogSet.reprompt(): Can't find A dialog with an id of '${instance.id}'.`); + throw new Error(`DialogSet.reprompt(): Can't find A dialog with an id of '${ instance.id }'.`); } // Ask dialog to re-prompt if supported @@ -486,8 +438,52 @@ export class DialogContext { } } - private async endActiveDialog(reason: DialogReason, result?: any): Promise { - const instance: DialogInstance = this.activeDialog; + /// + /// Searches for a dialog with a given ID. + /// Emits a named event for the current dialog, or someone who started it, to handle. + /// + /// Name of the event to raise. + /// Value to send along with the event. + /// Flag to control whether the event should be bubbled to its parent if not handled locally. Defaults to a value of `true`. + /// Whether the event is emitted from a leaf node. + /// The cancellation token. + /// True if the event was handled. + public async emitEvent(name: string, value?: any, bubble = true, fromLeaf = false): Promise { + // Initialize event + const dialogEvent: DialogEvent = { + bubble: bubble, + name: name, + value: value, + }; + + // Find starting dialog + let dc: DialogContext = this; + if (fromLeaf) { + while (true) { + const childDc = dc.child; + if (childDc != undefined) { + dc = childDc; + } else { + break; + } + } + } + + // Dispatch to active dialog first + // - The active dialog will decide if it should bubble the event to its parent. + const instance = dc.activeDialog; + if (instance != undefined) { + const dialog = dc.findDialog(instance.id); + if (dialog != undefined) { + return await dialog.onDialogEvent(dc, dialogEvent); + } + } + + return false; + } + + private async endActiveDialog(reason: DialogReason): Promise { + const instance: DialogInstance = this.activeDialog; if (instance) { // Lookup dialog const dialog: Dialog<{}> = this.findDialog(instance.id); @@ -498,34 +494,6 @@ export class DialogContext { // Pop dialog off stack this.stack.pop(); - - // Process dialogs output binding - // - if the stack is empty, any `dialog.` bindings will be applied to the active dialog - // on the parents stack. - if (dialog && dialog.outputProperty && result !== undefined) { - this.state.setValue(dialog.outputProperty, result); - } } } - - private getActiveDialogState(dc: DialogContext, state: object|number): object { - if (typeof state === 'number') { - // Positive values are indexes within the current DC and negative values are indexes in - // the parent DC. - if (state >= 0) { - if (state < dc.stack.length) { - return this.getActiveDialogState(dc, dc.stack[state].state); - } else { - throw new Error(`DialogContext.activeDialog: Can't find inherited state. Index out of range.`); - } - } else if (dc.parent) { - // Parent stack index of 0 is encoded as -1 so we need to make positive and then subtract 1 - return this.getActiveDialogState(dc.parent, (0 - state) - 1); - } else { - throw new Error(`DialogContext.activeDialog: Can't find inherited state. No parent DialogContext.`); - } - } else { - return state; - } - } -} +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/dialogEvents.ts b/libraries/botbuilder-dialogs/src/dialogEvents.ts new file mode 100644 index 0000000000..38b16ceb69 --- /dev/null +++ b/libraries/botbuilder-dialogs/src/dialogEvents.ts @@ -0,0 +1,15 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export class DialogEvents { + static readonly beginDialog = "beginDialog"; + static readonly repromptDialog = "repromptDialog"; + static readonly cancelDialog = "cancelDialog"; + static readonly activityReceived = "activityReceived"; + static readonly error = "error"; +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/dialogManager.ts b/libraries/botbuilder-dialogs/src/dialogManager.ts index adea9ecb22..c4e364830f 100644 --- a/libraries/botbuilder-dialogs/src/dialogManager.ts +++ b/libraries/botbuilder-dialogs/src/dialogManager.ts @@ -11,7 +11,6 @@ import { DialogState, DialogContext } from './dialogContext'; import { DialogTurnResult, Dialog, DialogTurnStatus } from './dialog'; import { Configurable } from './configurable'; import { DialogSet } from './dialogSet'; -import { StateMap } from './stateMap'; export interface PersistedState { userState: { @@ -123,9 +122,7 @@ export class DialogManager extends Configurable { } // Create DialogContext - const userState = new StateMap(newState.userState); - const conversationState = new StateMap(newState.conversationState); - const dc = new DialogContext(this.main, context, newState.conversationState._dialogs, userState, conversationState); + const dc = new DialogContext(this.main, context, newState.conversationState._dialogs); // Dispatch "activityReceived" event // - This will queue up any interruptions. diff --git a/libraries/botbuilder-dialogs/src/dialogSet.ts b/libraries/botbuilder-dialogs/src/dialogSet.ts index 9ddc43be5a..39b5d44593 100644 --- a/libraries/botbuilder-dialogs/src/dialogSet.ts +++ b/libraries/botbuilder-dialogs/src/dialogSet.ts @@ -8,12 +8,18 @@ import { BotTelemetryClient, StatePropertyAccessor, TurnContext } from 'botbuilder-core'; import { Dialog } from './dialog'; import { DialogContext, DialogState } from './dialogContext'; -import { StateMap } from './stateMap'; export interface DialogDependencies { getDependencies(): Dialog[]; } +export interface DialogDependencies { + /** + * Returns a dialogs child dialog dependencies so they can be added to a containers dialog set. + */ + getDependencies(): Dialog[]; +} + /** * A related set of dialogs that can all call each other. * @@ -95,12 +101,13 @@ export class DialogSet { public add(dialog: T): this { if (!(dialog instanceof Dialog)) { throw new Error(`DialogSet.add(): Invalid dialog being added.`); } - // Ensure dialog has a unique ID. + // ENsure dialogs ID is unique. if (this.dialogs.hasOwnProperty(dialog.id)) { let nextSuffix = 2; while (true) { - if (!this.dialogs.hasOwnProperty(dialog.id + nextSuffix.toString())) { - dialog.id = dialog.id + nextSuffix.toString(); + const suffixId = dialog.id + nextSuffix.toString(); + if (!this.hasOwnProperty(suffixId)) { + dialog.id = suffixId; break; } else { nextSuffix++; @@ -112,16 +119,15 @@ export class DialogSet { if (this._telemetryClient) { dialog.telemetryClient = this._telemetryClient; } - + + // Save dialog reference this.dialogs[dialog.id] = dialog; - // Automatically add any dependencies the dialog might have. - if (typeof (dialog as any).getDependencies == 'function') { - const dependencies = (dialog as any).getDependencies(); - if (Array.isArray(dependencies)) { - dependencies.forEach((dialog) => this.add(dialog)); - } + // Automatically add any child dependencies the dialog might have + if (typeof ((dialog as any) as DialogDependencies).getDependencies == 'function') { + ((dialog as any) as DialogDependencies).getDependencies().forEach((child) => this.add(child)); } + return this; } @@ -129,15 +135,13 @@ export class DialogSet { * Creates a dialog context which can be used to work with the dialogs in the set. * @param context Context for the current turn of conversation with the user. */ - public async createContext(context: TurnContext, conversationState?: object, userState?: object): Promise { + public async createContext(context: TurnContext): Promise { if (!this.dialogState) { throw new Error(`DialogSet.createContextAsync(): the dialog set was not bound to a stateProperty when constructed.`); } const state: DialogState = await this.dialogState.get(context, { dialogStack: [] } as DialogState); - const conversation = conversationState ? new StateMap(conversationState) : undefined; - const user = userState ? new StateMap(userState) : undefined; - return new DialogContext(this, context, state, conversation, user); + return new DialogContext(this, context, state); } /** diff --git a/libraries/botbuilder-dialogs/src/index.ts b/libraries/botbuilder-dialogs/src/index.ts index 9fd97a44ac..cbb5243dec 100644 --- a/libraries/botbuilder-dialogs/src/index.ts +++ b/libraries/botbuilder-dialogs/src/index.ts @@ -6,6 +6,7 @@ * Licensed under the MIT License. */ export * from './choices'; +export * from './memory'; export * from './prompts'; export * from './componentDialog'; export * from './configurable'; @@ -15,6 +16,7 @@ export * from './dialogContainer'; export * from './dialogContext'; export * from './dialogContextState'; export * from './dialogManager'; +export * from './dialogEvents'; export * from './dialogSet'; export * from './stateMap'; export * from './waterfallDialog'; diff --git a/libraries/botbuilder-dialogs/src/memory/dialogStateManager.ts b/libraries/botbuilder-dialogs/src/memory/dialogStateManager.ts new file mode 100644 index 0000000000..420382ca2a --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/dialogStateManager.ts @@ -0,0 +1,522 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { PathResolver, DollarPathResolver, HashPathResolver, AtAtPathResolver, AtPathResolver, PercentPathResolver } from './pathResolvers'; +import { MemoryScope, ScopePath, SettingsMemoryScope, DialogMemoryScope, ClassMemoryScope, ThisMemoryScope } from './scopes'; +import { DialogContext } from '../dialogContext'; +import { ConversationState, UserState } from 'botbuilder-core'; +import { ConversationMemoryScope } from './scopes/conversationMemoryScope'; +import { TurnMemoryScope } from './scopes/turnMemoryScope'; +import { UserMemoryScope } from './scopes/userMemoryScope'; + +export interface DialogStateManagerConfiguration { + /** + * List of path resolvers used to evaluate memory paths. + */ + readonly pathResolvers: PathResolver[]; + + /** + * List of the supported memory scopes. + */ + readonly memoryScopes: MemoryScope[]; +} + +/** + * The DialogStateManager manages memory scopes and path resolvers. + * + * @remarks + * MemoryScopes are named root level objects, which can exist either in the dialog context or off + * of turn state. Path resolvers allow for shortcut behavior for mapping things like + * $foo -> dialog.foo + */ +export class DialogStateManager { + private readonly dialogContext: DialogContext; + private _config: DialogStateManagerConfiguration; + + constructor(dc: DialogContext) { + this.dialogContext = dc; + } + + /** + * Gets or sets the configured path resolvers and memory scopes for the dialog state manager. + * + * @remarks + * There is a single set of configuration information for a given chain of dialog contexts. + * Assigning a new configuration to any DialogStateManager within the chain will update the + * configuration for the entire chain. + */ + public get configuration(): DialogStateManagerConfiguration { + if (this.dialogContext.parent) { + return this.dialogContext.parent.state.configuration; + } else { + if (this._config == undefined) { + this._config = DialogStateManager.createStandardConfiguration(); + } + + return this._config; + } + } + + public set configuration(value: DialogStateManagerConfiguration) { + if (this.dialogContext.parent) { + this.dialogContext.parent.state.configuration = value; + } else { + this._config = value; + } + } + + /** + * Get the value from memory using path expression. + * + * @remarks + * This always returns a CLONE of the memory, any modifications to the result will not affect memory. + * @param T The value type to return. + * @param pathExpression Path expression to use. + * @param defaultValue (Optional) default value to use if the path isn't found. May be a function that returns the default value to use. + * @returns The found value or undefined if not found and no `defaultValue` specified. + */ + public getValue(pathExpression: string, defaultValue?: T|(() => T)): T { + function returnDefault(): T { + return typeof defaultValue == 'function' ? (defaultValue as Function)() : defaultValue; + } + + // Get path segments + const segments = this.parsePath(this.transformPath(pathExpression)); + if (segments.length < 1) { return returnDefault() } + + // Get memory scope to search over + const scope = this.getMemoryScope(segments[0].toString()); + if (scope == undefined) { throw new Error(`DialogStateManager.getValue: a scope of '${segments[0]}' wasn't found.`) } + + // Search over path + const memory = this.resolveSegments(scope.getMemory(this.dialogContext), segments, false); + + // Return default value if nothing found + return memory != undefined ? memory : returnDefault(); + } + + /** + * Set memory to value. + * @param pathExpression Path to memory. + * @param value Value to set. + */ + public setValue(pathExpression: string, value: any): void { + // Get path segments + const segments = this.parsePath(this.transformPath(pathExpression)); + if (segments.length < 1) { throw new Error(`DialogStateManager.setValue: path wasn't specified.`) } + + // Get memory scope to update + const scope = this.getMemoryScope(segments[0].toString()); + if (scope == undefined) { throw new Error(`DialogStateManager.setValue: a scope of '${segments[0]}' wasn't found.`) } + + // Update memory + if (segments.length > 1) { + // Find value up to last key + // - Missing paths will be populated as needed + let memory = scope.getMemory(this.dialogContext); + memory = this.resolveSegments(memory, segments, true); + + // Update value + let key = segments[segments.length - 1]; + if (key === 'first()') { key = 0 }; + if (typeof key == 'number' && Array.isArray(memory)) { + // Only allow positive indexes + if (key < 0) { throw new Error(`DialogStateManager.setValue: unable to update value for '${pathExpression}'. Negative indexes aren't allowed.`) } + + // Expand array as needed and update array + const l = key + 1; + while (memory.length < l) { + memory.push(undefined); + } + memory[key] = value; + } else if (typeof key == 'string' && key.length > 0 && typeof memory == 'object' && !Array.isArray(memory)) { + // Find key to use and update object + key = this.findObjectKey(memory, key) || key; + memory[key] = value; + } else { + throw new Error(`DialogStateManager.setValue: unable to update value for '${pathExpression}'.`); + } + } else { + // Just update memory scope + scope.setMemory(this.dialogContext, value); + } + } + + /** + * Delete property from memory + * @param path The leaf property to remove. + */ + public deleteValue(pathExpression: string): void { + // Get path segments + const segments = this.parsePath(this.transformPath(pathExpression)); + if (segments.length < 2) { throw new Error(`DialogStateManager.deleteValue: invalid path of '${pathExpression}'.`) } + + // Get memory scope to update + const scope = this.getMemoryScope(segments[0].toString()); + if (scope == undefined) { throw new Error(`DialogStateManager.deleteValue: a scope of '${segments[0]}' wasn't found.`) } + + // Find value up to last key + let key = segments.pop(); + const memory = this.resolveSegments(scope.getMemory(this.dialogContext), segments, false); + + // Update value + if (typeof key == 'number' && Array.isArray(memory)) { + if (key < memory.length) { + memory.splice(key, 1); + } + } else if (typeof key == 'string' && key.length > 0 && typeof memory == 'object' && !Array.isArray(memory)) { + const found = this.findObjectKey(memory, key); + if (found) { + delete memory[found]; + } + } + } + + /** + * Ensures that all memory scopes have been loaded for the current turn. + * + * @remarks + * This should be called at the beginning of the turn. + */ + public async loadAllScopes(): Promise { + const scopes = this.configuration.memoryScopes; + for (let i = 0; i < scopes.length; i++) { + await scopes[i].load(this.dialogContext); + } + } + + /** + * Saves any changes made to memory scopes. + * + * @remarks + * This should be called at the end of the turn. + */ + public async saveAllChanges(): Promise { + const scopes = this.configuration.memoryScopes; + for (let i = 0; i < scopes.length; i++) { + await scopes[i].saveChanges(this.dialogContext); + } + } + + /** + * Deletes all of the backing memory for a given scope. + */ + public async deleteScopesMemory(name: string): Promise { + name = name.toLowerCase(); + const scopes = this.configuration.memoryScopes; + for (let i = 0; i < scopes.length; i++) { + const scope = scopes[i]; + if (scope.name.toLowerCase() == name) { + await scope.delete(this.dialogContext); + break; + } + } + } + + /** + * Normalizes the path segments of a passed in path. + * + * @remarks + * A path of `profile.address[0]` will be normalized to `profile.address.0`. + * @param pathExpression The path to normalize. + * @returns The normalized path. + */ + public parsePath(pathExpression: string): (string|number)[] { + // Expand path segments + let segment = ''; + let depth = 0; + let quote = ''; + const output: (string|number)[] = []; + for (let i = 0; i < pathExpression.length; i++) { + const c = pathExpression[i]; + if (depth > 0) { + // We're in a set of brackets + if (quote.length) { + // We're in a string + switch (c) { + case '\\': + // Escape code detected + i++; + segment += pathExpression[i]; + break; + default: + segment += c; + if (c == quote) { + quote = ''; + } + break; + } + } else { + // We're in a bracket + switch (c) { + case '[': + depth++; + segment += c; + break; + case ']': + depth--; + if (depth > 0) { segment += c } + break; + case "'": + case '"': + quote = c; + segment += c; + break; + default: + segment += c; + break; + } + + // Are we out of the brackets + if (depth == 0) { + if (isQuoted(segment)) { + // Quoted segment + output.push(segment.length > 2 ? segment.substr(1, segment.length - 2) : ''); + } else if (isIndex(segment)) { + // Array index + output.push(parseInt(segment)); + } else { + // Resolve nested value + const val = this.getValue(segment); + const t = typeof val; + output.push(t == 'string' || t == 'number' ? val : ''); + } + segment = ''; + } + } + } else { + // We're parsing the outer path + switch (c) { + case '[': + if (segment.length > 0) { + output.push(segment); + segment = ''; + } + depth++; + break; + case '.': + if (segment.length > 0) { + output.push(segment); + segment = ''; + } else if (i == 0 || i == (pathExpression.length - 1)) { + // Special case a "." at beginning or end of path + output.push(''); + } else if (pathExpression[i - 1] == '.') { + // Special case ".." + output.push(''); + } + break; + default: + if (isValidPathChar(c)) { + segment += c; + } else { + throw new Error(`DialogStateManager.normalizePath: Invalid path detected - ${pathExpression}`); + } + break; + } + } + } + if (depth > 0) { + throw new Error(`DialogStateManager.normalizePath: Invalid path detected - ${pathExpression}`); + } else if (segment.length > 0) { + output.push(segment); + } + + return output; + } + + /** + * Transform the path using the registered path transformers. + * @param pathExpression The path to transform. + * @returns The transformed path. + */ + public transformPath(pathExpression: string): string { + // Run path through registered resolvers. + const resolvers = this.configuration.pathResolvers; + for (let i = 0; i < resolvers.length; i++) { + pathExpression = resolvers[i].transformPath(pathExpression); + } + + return pathExpression; + } + + /** + * Gets all memory scopes suitable for logging. + * @returns Object which represents all memory scopes. + */ + public getMemorySnapshot(): object { + const output = {}; + this.configuration.memoryScopes.forEach((scope) => { + if (scope.includeInSnapshot) { + output[scope.name] = scope.getMemory(this.dialogContext); + } + }); + + return output; + } + + private resolveSegments(memory: object, segments: (string|number)[], assignment?: boolean): any { + let value: any = memory; + const l = assignment ? segments.length - 1 : segments.length; + for (let i = 1; i < l && value != undefined; i++) { + let key = segments[i]; + if (typeof key == 'number') { + // Key is an array index + if (Array.isArray(value)) { + value = value[key]; + } else { + value = undefined; + } + } else if (key === 'first()') { + // Special case returning the first entity in an array of entities. + if (Array.isArray(value) && value.length > 0) { + value = value[0]; + if (Array.isArray(value)) { + // Nested array detected + if (value.length > 0) { + value = value[0]; + } else { + value = undefined; + } + } + } else { + value = undefined; + } + } else if (typeof key == 'string' && key.length > 0) { + // Key is an object index + if (typeof value == 'object' && !Array.isArray(value)) { + // Case-insensitive search for prop + let found = this.findObjectKey(value, key); + + // Ensure path exists as needed + if (assignment) { + const nextKey = segments[i + 1]; + if (typeof nextKey == 'number' || nextKey === 'first()') { + // Ensure prop contains an array + if (found) { + if (value[found] == undefined) { + value[found] = []; + } + } else { + found = key; + value[found] = []; + } + } else if (typeof nextKey == 'string' && nextKey.length > 0) { + // Ensure prop contains an object + if (found) { + if (value[found] == undefined) { + value[found] = {}; + } + } else { + found = key; + value[found] = {}; + } + } else { + // We can't determine type so return undefined + found = undefined; + } + } + + value = found ? value[found] : undefined; + } else { + value = undefined; + } + } else { + // Key is missing + value = undefined; + } + } + + return value; + } + + private findObjectKey(obj: object, key: string): string|undefined { + const k = key.toLowerCase(); + for (const prop in obj) { + if (prop.toLowerCase() == k) { + return prop; + } + } + + return undefined; + } + + private getMemoryScope(name: string): MemoryScope | undefined { + const key = name.toLowerCase(); + const scopes = this.configuration.memoryScopes; + for (let i = 0; i < scopes.length; i++) { + const scope = scopes[i]; + if (scope.name.toLowerCase() == key) { + return scope; + } + } + + return undefined; + } + + static createStandardConfiguration(conversationState?: ConversationState, userState?: UserState): DialogStateManagerConfiguration { + const config: DialogStateManagerConfiguration = { + pathResolvers: [ + new DollarPathResolver(), + new HashPathResolver(), + new AtAtPathResolver(), + new AtPathResolver(), + new PercentPathResolver() + ], + memoryScopes: [ + new TurnMemoryScope(), + new SettingsMemoryScope(), + new DialogMemoryScope(), + new ClassMemoryScope(), + new ThisMemoryScope() + ] + }; + + // Add optional scopes + if (conversationState) { + config.memoryScopes.push(new ConversationMemoryScope(conversationState)); + } + if (userState) { + config.memoryScopes.push(new UserMemoryScope(userState)); + } + + return config; + } +} + +/** + * @private + */ +function isIndex(segment: string): boolean { + const digits = '0123456789'; + for (let i = 0; i < segment.length; i++) { + const c= segment[i]; + if (digits.indexOf(c) < 0) { + // Check for negative sign + if (c != '-' || i > 0 || segment.length < 2) { + return false; + } + } + } + + return segment.length > 0; +} + +/** + * @private + */ +function isQuoted(segment: string): boolean { + return segment.length > 1 && (segment.startsWith("'") && segment.endsWith("'")) || + (segment.startsWith('"') && segment.endsWith('"')); +} + +/** + * @private + */ +function isValidPathChar(c: string): boolean { + return '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-()'.indexOf(c) >= 0; +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/memory/index.ts b/libraries/botbuilder-dialogs/src/memory/index.ts new file mode 100644 index 0000000000..ccd79df686 --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/index.ts @@ -0,0 +1,10 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +export * from './pathResolvers'; +export * from './scopes'; +export * from './dialogStateManager'; \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/memory/pathResolvers/aliasPathResolver.ts b/libraries/botbuilder-dialogs/src/memory/pathResolvers/aliasPathResolver.ts new file mode 100644 index 0000000000..f6a44a1b29 --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/pathResolvers/aliasPathResolver.ts @@ -0,0 +1,37 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { PathResolver } from './pathResolver'; + +/** + * Maps aliasXXX -> path.xxx ($foo => dialog.foo). + */ +export class AliasPathResolver implements PathResolver { + private readonly alias: string; + private readonly prefix: string; + private readonly postfix: string; + + constructor(alias: string, prefix: string, postfix?: string) + { + this.alias = alias.trim(); + this.prefix = prefix.trim(); + this.postfix = postfix ? postfix.trim() : ''; + } + + public transformPath(path: string): string { + const start = path.indexOf(this.alias); + if (start >= 0) { + // $xxx -> path.xxx + path = `${this.prefix}${path.substr(start + this.alias.length)}${this.postfix}`; + if (path.endsWith('.')) { + path = path.substr(0, path.length - 1); + } + } + + return path; + } +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/memory/pathResolvers/atAtPathResolver.ts b/libraries/botbuilder-dialogs/src/memory/pathResolvers/atAtPathResolver.ts new file mode 100644 index 0000000000..ff1735f8f8 --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/pathResolvers/atAtPathResolver.ts @@ -0,0 +1,18 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { AliasPathResolver } from './aliasPathResolver'; + +/** + * Maps @@ => turn.recognized.entitites.xxx array. + */ +export class AtAtPathResolver extends AliasPathResolver { + + constructor() { + super('@@', 'turn.recognized.entities.'); + } +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/memory/pathResolvers/atPathResolver.ts b/libraries/botbuilder-dialogs/src/memory/pathResolvers/atPathResolver.ts new file mode 100644 index 0000000000..601a5c5228 --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/pathResolvers/atPathResolver.ts @@ -0,0 +1,28 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { AliasPathResolver } from './aliasPathResolver'; + +/** + * Maps @@ => turn.recognized.entitites.xxx[0] + */ +export class AtPathResolver extends AliasPathResolver { + + constructor() { + super('@', 'turn.recognized.entities.', '.first()'); + } + + public transformPath(path: string): string { + // override to make sure it doesn't match @@ + path = path.trim(); + if (path.startsWith('@') && !path.startsWith('@@')) { + return super.transformPath(path); + } + + return path; + } +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/memory/pathResolvers/dollarPathResolver.ts b/libraries/botbuilder-dialogs/src/memory/pathResolvers/dollarPathResolver.ts new file mode 100644 index 0000000000..936794bc90 --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/pathResolvers/dollarPathResolver.ts @@ -0,0 +1,18 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { AliasPathResolver } from './aliasPathResolver'; + +/** + * Maps $xxx => dialog.xxx + */ +export class DollarPathResolver extends AliasPathResolver { + + constructor() { + super('$', 'dialog.'); + } +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/memory/pathResolvers/hashPathResolver.ts b/libraries/botbuilder-dialogs/src/memory/pathResolvers/hashPathResolver.ts new file mode 100644 index 0000000000..f583d64dbe --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/pathResolvers/hashPathResolver.ts @@ -0,0 +1,18 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { AliasPathResolver } from './aliasPathResolver'; + +/** + * Maps #xxx => turn.recognized.intents.xxx + */ +export class HashPathResolver extends AliasPathResolver { + + constructor() { + super('#', 'turn.recognized.intents.'); + } +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/memory/pathResolvers/index.ts b/libraries/botbuilder-dialogs/src/memory/pathResolvers/index.ts new file mode 100644 index 0000000000..3a7ec66050 --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/pathResolvers/index.ts @@ -0,0 +1,14 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +export * from './aliasPathResolver'; +export * from './atAtPathResolver'; +export * from './atPathResolver'; +export * from './dollarPathResolver'; +export * from './hashPathResolver'; +export * from './pathResolver'; +export * from './percentPathResolver'; diff --git a/libraries/botbuilder-dialogs/src/memory/pathResolvers/pathResolver.ts b/libraries/botbuilder-dialogs/src/memory/pathResolvers/pathResolver.ts new file mode 100644 index 0000000000..4ee7da446b --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/pathResolvers/pathResolver.ts @@ -0,0 +1,16 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export interface PathResolver { + /** + * Transform the path + * @param path Path to inspect. + * @returns Transformed path + */ + transformPath(path: string): string; +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/memory/pathResolvers/percentPathResolver.ts b/libraries/botbuilder-dialogs/src/memory/pathResolvers/percentPathResolver.ts new file mode 100644 index 0000000000..c77b56bab3 --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/pathResolvers/percentPathResolver.ts @@ -0,0 +1,18 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { AliasPathResolver } from './aliasPathResolver'; + +/** + * Maps %xxx => class.xxx (aka activeDialog.properties.xxx) + */ +export class PercentPathResolver extends AliasPathResolver { + + constructor() { + super('%', 'class.'); + } +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/memory/scopes/botStateMemoryScope.ts b/libraries/botbuilder-dialogs/src/memory/scopes/botStateMemoryScope.ts new file mode 100644 index 0000000000..6e44cda63d --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/scopes/botStateMemoryScope.ts @@ -0,0 +1,61 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { MemoryScope } from "./memoryScope"; +import { DialogContext } from "../../dialogContext"; +import { BotState } from 'botbuilder-core'; + +/** + * Base class for memory scopes based on BotState. + */ +export class BotStateMemoryScope extends MemoryScope { + private readonly _state: BotState; + private readonly _propertyName: string; + + constructor(name: string, botState: BotState, propertyName?: string) { + super(name, true); + + // Create property accessor + this._state = botState; + this._propertyName = propertyName || name; + } + + public getMemory(dc: DialogContext): object { + // Get state + const state = this._state.get(dc.context); + if (state == undefined) { throw new Error(`BotStateMemory.getMemory: load() should be called before retrieving memory.`) } + + // Ensure memory initialized + let memory = state[this._propertyName]; + if (typeof memory !== "object") { + state[this._propertyName] = memory = {}; + } + + // Return memory + return memory; + } + + public setMemory(dc: DialogContext, memory: object): void { + this._state.get(dc.context)[this._propertyName] = memory; + } + + public async load(dc: DialogContext): Promise { + await this._state.load(dc.context); + } + + public async saveChanges(dc: DialogContext): Promise { + await this._state.saveChanges(dc.context); + } + + public async delete(dc: DialogContext): Promise { + await this._state.delete(dc.context); + + // The state cache is cleared after deletion so we should re-load to + // avoid potential errors from the bot touching memory after a delete. + await this._state.load(dc.context); + } +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/memory/scopes/classMemoryScope.ts b/libraries/botbuilder-dialogs/src/memory/scopes/classMemoryScope.ts new file mode 100644 index 0000000000..8b7a711286 --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/scopes/classMemoryScope.ts @@ -0,0 +1,39 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { MemoryScope } from "./memoryScope"; +import { ScopePath } from "./scopePath"; +import { DialogContext } from "../../dialogContext"; + +/** + * ClassMemoryScope maps "class" -> dc.activeDialog.properties + */ +export class ClassMemoryScope extends MemoryScope { + constructor() { + super(ScopePath.CLASS, false); + } + + public getMemory(dc: DialogContext): object { + // if active dialog is a container dialog then "dialog" binds to it + if (dc.activeDialog) { + var dialog = dc.findDialog(dc.activeDialog.id); + if (dialog != undefined) { + // Clone properties + const clone: object = {}; + for (const key in dialog) { + if (dialog.hasOwnProperty(key) && typeof dialog[key] != 'function') { + clone[key] = dialog[key]; + } + } + + return clone; + } + } + + return undefined; + } +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/memory/scopes/conversationMemoryScope.ts b/libraries/botbuilder-dialogs/src/memory/scopes/conversationMemoryScope.ts new file mode 100644 index 0000000000..2586792108 --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/scopes/conversationMemoryScope.ts @@ -0,0 +1,19 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { BotStateMemoryScope } from './botStateMemoryScope'; +import { ConversationState } from 'botbuilder-core'; +import { ScopePath } from './scopePath'; + +/** + * Memory that's scoped to the current conversation. + */ +export class ConversationMemoryScope extends BotStateMemoryScope { + constructor(conversationState: ConversationState, propertyName?: string) { + super(ScopePath.CONVERSATION, conversationState, propertyName); + } +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/memory/scopes/dialogMemoryScope.ts b/libraries/botbuilder-dialogs/src/memory/scopes/dialogMemoryScope.ts new file mode 100644 index 0000000000..56327b9f51 --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/scopes/dialogMemoryScope.ts @@ -0,0 +1,65 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { MemoryScope } from "./memoryScope"; +import { ScopePath } from "./scopePath"; +import { DialogContext } from "../../dialogContext"; +import { DialogContainer } from "../../dialogContainer"; + +/** + * DialogMemoryScope maps "dialog" -> dc.parent.activeDialog.state || dc.activeDialog.state + */ +export class DialogMemoryScope extends MemoryScope { + constructor() { + super(ScopePath.DIALOG); + } + + public getMemory(dc: DialogContext): object { + // If active dialog is a container dialog then "dialog" binds to it. + // Otherwise the "dialog" will bind to the dialogs parent assuming it + // is a container. + let parent: DialogContext = dc; + if (!this.isContainer(parent) && this.isContainer(parent.parent)) { + parent = parent.parent; + } + + // If there's no active dialog then throw an error. + if (!parent.activeDialog) { throw new Error(`DialogMemoryScope.getMemory: no active dialog found.`) } + + return parent.activeDialog.state; + } + + public setMemory(dc: DialogContext, memory: object): void { + if (memory == undefined) { + throw new Error(`DialogMemoryScope.setMemory: undefined memory object passed in.`); + } + + // If active dialog is a container dialog then "dialog" binds to it. + // Otherwise the "dialog" will bind to the dialogs parent assuming it + // is a container. + let parent: DialogContext = dc; + if (!this.isContainer(parent) && this.isContainer(parent.parent)) { + parent = parent.parent; + } + + // If there's no active dialog then throw an error. + if (!parent.activeDialog) { throw new Error(`DialogMemoryScope.setMemory: no active dialog found.`) } + + parent.activeDialog.state = memory; + } + + private isContainer(dc: DialogContext): boolean { + if (dc != undefined && dc.activeDialog != undefined) { + var dialog = dc.findDialog(dc.activeDialog.id); + if (dialog instanceof DialogContainer) { + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/memory/scopes/index.ts b/libraries/botbuilder-dialogs/src/memory/scopes/index.ts new file mode 100644 index 0000000000..16ba85bb62 --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/scopes/index.ts @@ -0,0 +1,16 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +export * from './classMemoryScope'; +export * from './conversationMemoryScope'; +export * from './dialogMemoryScope'; +export * from './memoryScope'; +export * from './scopePath'; +export * from './settingsMemoryScope'; +export * from './thisMemoryScope'; +export * from './turnMemoryScope'; +export * from './userMemoryScope'; \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/memory/scopes/memoryScope.ts b/libraries/botbuilder-dialogs/src/memory/scopes/memoryScope.ts new file mode 100644 index 0000000000..d294e2c7d4 --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/scopes/memoryScope.ts @@ -0,0 +1,69 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { DialogContext } from "../../dialogContext"; + +/** + * Abstract base class for all memory scopes. + */ +export abstract class MemoryScope { + constructor(name: string, includeInSnapshot = true) + { + this.includeInSnapshot = includeInSnapshot; + this.name = name; + } + + /** + * Gets or sets name of the scope + */ + public readonly name: string; + + /** + * Gets a value indicating whether this memory should be included in snapshot. + */ + public readonly includeInSnapshot: boolean; + + /** + * Get the backing memory for this scope + * @param dc Current dialog context. + * @returns memory for the scope + */ + public abstract getMemory(dc: DialogContext): object; + + /** + * Changes the backing object for the memory scope. + * @param dc Current dialog context + * @param memory memory to assign + */ + public setMemory(dc: DialogContext, memory: object): void { + throw new Error(`MemoryScope.setMemory: The '${this.name}' memory scope is read-only.`); + } + + /** + * Loads a scopes backing memory at the start of a turn. + * @param dc Current dialog context. + */ + public async load(dc: DialogContext): Promise { + // No initialization by default. + } + + /** + * Saves a scopes backing memory at the end of a turn. + * @param dc Current dialog context. + */ + public async saveChanges(dc: DialogContext): Promise { + // No initialization by default. + } + + /** + * Deletes the backing memory for a scope. + * @param dc Current dialog context. + */ + public async delete(dc: DialogContext): Promise { + throw new Error(`MemoryScope.delete: The '${this.name}' memory scope can't be deleted.`); + } +} diff --git a/libraries/botbuilder-dialogs/src/memory/scopes/scopePath.ts b/libraries/botbuilder-dialogs/src/memory/scopes/scopePath.ts new file mode 100644 index 0000000000..412ab3bb24 --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/scopes/scopePath.ts @@ -0,0 +1,17 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export class ScopePath { + static readonly USER = "user"; + static readonly CONVERSATION = "conversation"; + static readonly DIALOG = "dialog"; + static readonly THIS = "this"; + static readonly CLASS = "class"; + static readonly SETTINGS = "settings"; + static readonly TURN = "turn"; +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/memory/scopes/settingsMemoryScope.ts b/libraries/botbuilder-dialogs/src/memory/scopes/settingsMemoryScope.ts new file mode 100644 index 0000000000..d92d70abf1 --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/scopes/settingsMemoryScope.ts @@ -0,0 +1,31 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { MemoryScope } from "./memoryScope"; +import { ScopePath } from "./scopePath"; +import { DialogContext } from "../../dialogContext"; + +/** + * SettingsMemoryScope maps "settings" -> process.env + */ +export class SettingsMemoryScope extends MemoryScope { + constructor() { + super(ScopePath.SETTINGS, false); + } + + public getMemory(dc: DialogContext): object { + // Clone strings from env + const settings: object = {}; + for (const key in process.env) { + if (typeof process.env[key] == 'string') { + settings[key] = process.env[key]; + } + } + + return settings; + } +} diff --git a/libraries/botbuilder-dialogs/src/memory/scopes/thisMemoryScope.ts b/libraries/botbuilder-dialogs/src/memory/scopes/thisMemoryScope.ts new file mode 100644 index 0000000000..96ba9d16cf --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/scopes/thisMemoryScope.ts @@ -0,0 +1,34 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { MemoryScope } from "./memoryScope"; +import { ScopePath } from "./scopePath"; +import { DialogContext } from "../../dialogContext"; + +/** + * ThisMemoryScope maps "this" -> dc.activeDialog.state + */ +export class ThisMemoryScope extends MemoryScope { + constructor() { + super(ScopePath.THIS); + } + + public getMemory(dc: DialogContext): object { + if (!dc.activeDialog) { throw new Error(`ThisMemoryScope.getMemory: no active dialog found.`) } + return dc.activeDialog.state; + } + + public setMemory(dc: DialogContext, memory: object): void { + if (memory == undefined) { + throw new Error(`ThisMemoryScope.setMemory: undefined memory object passed in.`); + } + + if (!dc.activeDialog) { throw new Error(`ThisMemoryScope.setMemory: no active dialog found.`) } + + dc.activeDialog.state = memory; + } +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/memory/scopes/turnMemoryScope.ts b/libraries/botbuilder-dialogs/src/memory/scopes/turnMemoryScope.ts new file mode 100644 index 0000000000..1ba1269405 --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/scopes/turnMemoryScope.ts @@ -0,0 +1,42 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { MemoryScope } from "./memoryScope"; +import { ScopePath } from "./scopePath"; +import { DialogContext } from "../../dialogContext"; + +/** + * TurnMemoryScope represents memory scoped to the current turn. + */ +export class TurnMemoryScope extends MemoryScope { + constructor() { + super(ScopePath.TURN); + } + + public getMemory(dc: DialogContext): object { + let memory = dc.context.turnState.get(TURN_STATE); + if (typeof memory != 'object') { + memory = {}; + dc.context.turnState.set(TURN_STATE, memory); + } + + return memory; + } + + public setMemory(dc: DialogContext, memory: object): void { + if (memory == undefined) { + throw new Error(`TurnMemoryScope.setMemory: undefined memory object passed in.`); + } + + dc.context.turnState.set(TURN_STATE, memory); + } +} + +/** + * @private + */ +const TURN_STATE = Symbol('turn'); \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/memory/scopes/userMemoryScope.ts b/libraries/botbuilder-dialogs/src/memory/scopes/userMemoryScope.ts new file mode 100644 index 0000000000..389d04c8d5 --- /dev/null +++ b/libraries/botbuilder-dialogs/src/memory/scopes/userMemoryScope.ts @@ -0,0 +1,19 @@ +/** + * @module botbuilder-dialogs + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { BotStateMemoryScope } from './botStateMemoryScope'; +import { UserState } from 'botbuilder-core'; +import { ScopePath } from './scopePath'; + +/** + * Memory that's scoped to the current user. + */ +export class UserMemoryScope extends BotStateMemoryScope { + constructor(userState: UserState, propertyName?: string) { + super(ScopePath.USER, userState, propertyName); + } +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/prompts/attachmentPrompt.ts b/libraries/botbuilder-dialogs/src/prompts/attachmentPrompt.ts index 68607aef22..539df40af9 100644 --- a/libraries/botbuilder-dialogs/src/prompts/attachmentPrompt.ts +++ b/libraries/botbuilder-dialogs/src/prompts/attachmentPrompt.ts @@ -25,10 +25,6 @@ export class AttachmentPrompt extends Prompt { super(dialogId, validator); } - protected onComputeID(): string { - return `attachmentPrompt[${this.bindingPath()}]`; - } - protected async onPrompt(context: TurnContext, state: any, options: PromptOptions, isRetry: boolean): Promise { if (isRetry && options.retryPrompt) { await context.sendActivity(options.retryPrompt, undefined, InputHints.ExpectingInput); diff --git a/libraries/botbuilder-dialogs/src/prompts/choicePrompt.ts b/libraries/botbuilder-dialogs/src/prompts/choicePrompt.ts index 8cbc61c9cc..00157ceacd 100644 --- a/libraries/botbuilder-dialogs/src/prompts/choicePrompt.ts +++ b/libraries/botbuilder-dialogs/src/prompts/choicePrompt.ts @@ -8,6 +8,10 @@ import { Activity, TurnContext } from 'botbuilder-core'; import { ChoiceFactory, ChoiceFactoryOptions, FindChoicesOptions, FoundChoice, recognizeChoices } from '../choices'; import { ListStyle, Prompt, PromptOptions, PromptRecognizerResult, PromptValidator } from './prompt'; +import { PromptCultureModels } from './promptCultureModels'; + +// Need ChoiceDefaultsProperty so we can set choiceDefaults dynamically with lambda +interface ChoiceDefaultsChoicePrompt { [locale: string]: ChoiceFactoryOptions }; /** * Prompts a user to select from a list of choices. @@ -19,18 +23,10 @@ import { ListStyle, Prompt, PromptOptions, PromptRecognizerResult, PromptValidat export class ChoicePrompt extends Prompt { /** - * Default options for rendering the choices to the user based on locale. + * A dictionary of Default Choices based on [[PromptCultureModels.getSupportedCultures()]]. + * Can be replaced by user using the constructor that contains choiceDefaults. */ - private static defaultChoiceOptions: { [locale: string]: ChoiceFactoryOptions } = { - 'es-es': { inlineSeparator: ', ', inlineOr: ' o ', inlineOrMore: ', o ', includeNumbers: true }, - 'nl-nl': { inlineSeparator: ', ', inlineOr: ' of ', inlineOrMore: ', of ', includeNumbers: true }, - 'en-us': { inlineSeparator: ', ', inlineOr: ' or ', inlineOrMore: ', or ', includeNumbers: true }, - 'fr-fr': { inlineSeparator: ', ', inlineOr: ' ou ', inlineOrMore: ', ou ', includeNumbers: true }, - 'de-de': { inlineSeparator: ', ', inlineOr: ' oder ', inlineOrMore: ', oder ', includeNumbers: true }, - 'ja-jp': { inlineSeparator: '、 ', inlineOr: ' または ', inlineOrMore: '、 または ', includeNumbers: true }, - 'pt-br': { inlineSeparator: ', ', inlineOr: ' ou ', inlineOrMore: ', ou ', includeNumbers: true }, - 'zh-cn': { inlineSeparator: ', ', inlineOr: ' 要么 ', inlineOrMore: ', 要么 ', includeNumbers: true } - }; + private choiceDefaults: ChoiceDefaultsChoicePrompt; /** * The prompts default locale that should be recognized. @@ -61,21 +57,34 @@ export class ChoicePrompt extends Prompt { * @param dialogId (Optional) unique ID of the dialog within its parent `DialogSet`. * @param validator (Optional) validator that will be called each time the user responds to the prompt. If the validator replies with a message no additional retry prompt will be sent. * @param defaultLocale (Optional) locale to use if `dc.context.activity.locale` not specified. Defaults to a value of `en-us`. + * @param choiceDefaults (Optional) Overrides the dictionary of Bot Framework SDK-supported _choiceDefaults (for prompt localization). + * Must be passed in to each ConfirmPrompt that needs the custom choice defaults. */ - constructor(dialogId?: string, validator?: PromptValidator, defaultLocale?: string) { + public constructor(dialogId: string, validator?: PromptValidator, defaultLocale?: string, choiceDefaults?: ChoiceDefaultsChoicePrompt) { super(dialogId, validator); this.style = ListStyle.auto; this.defaultLocale = defaultLocale; - } - - protected onComputeID(): string { - return `choicePrompt[${this.bindingPath()}]`; + + if (choiceDefaults == undefined) { + const supported: ChoiceDefaultsChoicePrompt = {}; + PromptCultureModels.getSupportedCultures().forEach((culture): void => { + supported[culture.locale] = { + inlineSeparator: culture.separator, + inlineOr: culture.inlineOr, + inlineOrMore: culture.inlineOrMore, + includeNumbers: true + }; + }); + this.choiceDefaults = supported; + } else { + this.choiceDefaults = choiceDefaults; + } } protected async onPrompt(context: TurnContext, state: any, options: PromptOptions, isRetry: boolean): Promise { // Determine locale - let locale: string = context.activity.locale || this.defaultLocale; - if (!locale || !ChoicePrompt.defaultChoiceOptions.hasOwnProperty(locale)) { + let locale: string = PromptCultureModels.mapToNearestLanguage(context.activity.locale || this.defaultLocale); + if (!locale || !this.choiceDefaults.hasOwnProperty(locale)) { locale = 'en-us'; } @@ -83,7 +92,7 @@ export class ChoicePrompt extends Prompt { let prompt: Partial; const choices: any[] = (this.style === ListStyle.suggestedAction ? ChoiceFactory.toChoices(options.choices) : options.choices) || []; const channelId: string = context.activity.channelId; - const choiceOptions: ChoiceFactoryOptions = this.choiceOptions || ChoicePrompt.defaultChoiceOptions[locale]; + const choiceOptions: ChoiceFactoryOptions = this.choiceOptions || this.choiceDefaults[locale]; const choiceStyle: ListStyle = options.style || this.style; if (isRetry && options.retryPrompt) { prompt = this.appendChoices(options.retryPrompt, channelId, choices, choiceStyle, choiceOptions); @@ -101,7 +110,7 @@ export class ChoicePrompt extends Prompt { const activity: Activity = context.activity; const utterance: string = activity.text; const choices: any[] = (this.style === ListStyle.suggestedAction ? ChoiceFactory.toChoices(options.choices) : options.choices)|| []; - const opt: FindChoicesOptions = this.recognizerOptions || {} as FindChoicesOptions; + const opt: FindChoicesOptions = this.recognizerOptions || {}; opt.locale = activity.locale || opt.locale || this.defaultLocale || 'en-us'; const results: any[] = recognizeChoices(utterance, choices, opt); if (Array.isArray(results) && results.length > 0) { diff --git a/libraries/botbuilder-dialogs/src/prompts/confirmPrompt.ts b/libraries/botbuilder-dialogs/src/prompts/confirmPrompt.ts index 2ed03ab544..5a11d10d8a 100644 --- a/libraries/botbuilder-dialogs/src/prompts/confirmPrompt.ts +++ b/libraries/botbuilder-dialogs/src/prompts/confirmPrompt.ts @@ -9,6 +9,10 @@ import * as Recognizers from '@microsoft/recognizers-text-choice'; import { Activity, TurnContext } from 'botbuilder-core'; import { Choice, ChoiceFactoryOptions, recognizeChoices } from '../choices'; import { ListStyle, Prompt, PromptOptions, PromptRecognizerResult, PromptValidator } from './prompt'; +import { PromptCultureModels } from './promptCultureModels'; + +// Need ChoiceDefaultsProperty so we can set choiceDefaults dynamically with lambda +interface ChoiceDefaultsConfirmPrompt { [locale: string]: { choices: (string|Choice)[]; options: ChoiceFactoryOptions }}; /** * Prompts a user to confirm something with a "yes" or "no" response. @@ -20,33 +24,11 @@ import { ListStyle, Prompt, PromptOptions, PromptRecognizerResult, PromptValidat export class ConfirmPrompt extends Prompt { /** - * Default confirm choices for a range of locales. - * @deprecated since version 4.3 - */ - private static defaultConfirmChoices: { [locale: string]: (string | Choice)[] } = { - 'es-es': ['Sí', 'No'], - 'nl-nl': ['Ja', 'Nee'], - 'en-us': ['Yes', 'No'], - 'fr-fr': ['Oui', 'Non'], - 'de-de': ['Ja', 'Nein'], - 'ja-jp': ['はい', 'いいえ'], - 'pt-br': ['Sim', 'Não'], - 'zh-cn': ['是的', '不'] - }; - - /** - * Default options for rendering the choices to the user based on locale. + * A dictionary of Default Choices based on [[PromptCultureModels.getSupportedCultures()]]. + * Can be replaced by user using the constructor that contains choiceDefaults. + * This is initially set in the constructor. */ - private static defaultChoiceOptions: { [locale: string]: { choices: (string|Choice)[]; options: ChoiceFactoryOptions }} = { - 'es-es': { choices: ['Sí', 'No'], options: { inlineSeparator: ', ', inlineOr: ' o ', inlineOrMore: ', o ', includeNumbers: true }}, - 'nl-nl': { choices: ['Ja', 'Nee'], options: { inlineSeparator: ', ', inlineOr: ' of ', inlineOrMore: ', of ', includeNumbers: true }}, - 'en-us': { choices: ['Yes', 'No'], options: { inlineSeparator: ', ', inlineOr: ' or ', inlineOrMore: ', or ', includeNumbers: true }}, - 'fr-fr': { choices: ['Oui', 'Non'], options: { inlineSeparator: ', ', inlineOr: ' ou ', inlineOrMore: ', ou ', includeNumbers: true }}, - 'de-de': { choices: ['Ja', 'Nein'], options: { inlineSeparator: ', ', inlineOr: ' oder ', inlineOrMore: ', oder ', includeNumbers: true }}, - 'ja-jp': { choices: ['はい', 'いいえ'], options: { inlineSeparator: '、 ', inlineOr: ' または ', inlineOrMore: '、 または ', includeNumbers: true }}, - 'pt-br': { choices: ['Sim', 'Não'], options: { inlineSeparator: ', ', inlineOr: ' ou ', inlineOrMore: ', ou ', includeNumbers: true }}, - 'zh-cn': { choices: ['是的', '不'], options: { inlineSeparator: ', ', inlineOr: ' 要么 ', inlineOrMore: ', 要么 ', includeNumbers: true }} - }; + private choiceDefaults: ChoiceDefaultsConfirmPrompt; /** * The prompts default locale that should be recognized. */ @@ -77,14 +59,28 @@ export class ConfirmPrompt extends Prompt { * @param validator (Optional) validator that will be called each time the user responds to the prompt. * @param defaultLocale (Optional) locale to use if `TurnContext.activity.locale` is not specified. Defaults to a value of `en-us`. */ - constructor(dialogId?: string, validator?: PromptValidator, defaultLocale?: string) { + public constructor(dialogId: string, validator?: PromptValidator, defaultLocale?: string, choiceDefaults?: ChoiceDefaultsConfirmPrompt) { super(dialogId, validator); this.style = ListStyle.auto; this.defaultLocale = defaultLocale; - } - protected onComputeID(): string { - return `confirmPrompt[${this.bindingPath()}]`; + if (choiceDefaults == undefined) { + const supported: ChoiceDefaultsConfirmPrompt = {}; + PromptCultureModels.getSupportedCultures().forEach((culture): void => { + supported[culture.locale] = { + choices: [culture.yesInLanguage, culture.noInLanguage], + options: { + inlineSeparator: culture.separator, + inlineOr: culture.inlineOr, + inlineOrMore: culture.inlineOrMore, + includeNumbers: true + } + }; + }); + this.choiceDefaults = supported; + } else { + this.choiceDefaults = choiceDefaults; + } } protected async onPrompt(context: TurnContext, state: any, options: PromptOptions, isRetry: boolean): Promise { @@ -93,8 +89,8 @@ export class ConfirmPrompt extends Prompt { let prompt: Partial; const channelId: string = context.activity.channelId; const culture: string = this.determineCulture(context.activity); - const choiceOptions: ChoiceFactoryOptions = this.choiceOptions || ConfirmPrompt.defaultChoiceOptions[culture].options; - const choices: any[] = this.confirmChoices || ConfirmPrompt.defaultChoiceOptions[culture].choices; + const choiceOptions: ChoiceFactoryOptions = this.choiceOptions || this.choiceDefaults[culture].options; + const choices: any[] = this.confirmChoices || this.choiceDefaults[culture].choices; if (isRetry && options.retryPrompt) { prompt = this.appendChoices(options.retryPrompt, channelId, choices, this.style, choiceOptions); } else { @@ -116,10 +112,10 @@ export class ConfirmPrompt extends Prompt { result.value = results[0].resolution.value; } else { // If the prompt text was sent to the user with numbers, the prompt should recognize number choices. - const choiceOptions = this.choiceOptions || ConfirmPrompt.defaultChoiceOptions[culture].options; + const choiceOptions = this.choiceOptions || this.choiceDefaults[culture].options; if (typeof choiceOptions.includeNumbers !== 'boolean' || choiceOptions.includeNumbers) { - const confirmChoices = this.confirmChoices || ConfirmPrompt.defaultChoiceOptions[culture].choices; + const confirmChoices = this.confirmChoices || this.choiceDefaults[culture].choices; const choices = [confirmChoices[0], confirmChoices[1]]; const secondOrMoreAttemptResults = recognizeChoices(utterance, choices); if (secondOrMoreAttemptResults.length > 0) { @@ -133,8 +129,8 @@ export class ConfirmPrompt extends Prompt { } private determineCulture(activity: Activity): string { - let culture: string = activity.locale || this.defaultLocale; - if (!culture || !ConfirmPrompt.defaultChoiceOptions.hasOwnProperty(culture)) { + let culture: string = PromptCultureModels.mapToNearestLanguage(activity.locale || this.defaultLocale); + if (!culture || !this.choiceDefaults.hasOwnProperty(culture)) { culture = 'en-us'; } return culture; diff --git a/libraries/botbuilder-dialogs/src/prompts/datetimePrompt.ts b/libraries/botbuilder-dialogs/src/prompts/datetimePrompt.ts index bf344d464b..2fa131d976 100644 --- a/libraries/botbuilder-dialogs/src/prompts/datetimePrompt.ts +++ b/libraries/botbuilder-dialogs/src/prompts/datetimePrompt.ts @@ -55,10 +55,6 @@ export class DateTimePrompt extends Prompt { this.defaultLocale = defaultLocale; } - protected onComputeID(): string { - return `datetimePrompt[${this.bindingPath()}]`; - } - protected async onPrompt(context: TurnContext, state: any, options: PromptOptions, isRetry: boolean): Promise { if (isRetry && options.retryPrompt) { await context.sendActivity(options.retryPrompt, undefined, InputHints.ExpectingInput); diff --git a/libraries/botbuilder-dialogs/src/prompts/index.ts b/libraries/botbuilder-dialogs/src/prompts/index.ts index 6c1be7bcc4..18032b7dbf 100644 --- a/libraries/botbuilder-dialogs/src/prompts/index.ts +++ b/libraries/botbuilder-dialogs/src/prompts/index.ts @@ -14,3 +14,4 @@ export * from './numberPrompt'; export * from './oauthPrompt'; export * from './prompt'; export * from './textPrompt'; +export * from './promptCultureModels'; diff --git a/libraries/botbuilder-dialogs/src/prompts/numberPrompt.ts b/libraries/botbuilder-dialogs/src/prompts/numberPrompt.ts index 3bc6111401..0e402e99b7 100644 --- a/libraries/botbuilder-dialogs/src/prompts/numberPrompt.ts +++ b/libraries/botbuilder-dialogs/src/prompts/numberPrompt.ts @@ -9,6 +9,21 @@ import * as Recognizers from '@microsoft/recognizers-text-number'; import { Activity, InputHints, TurnContext } from 'botbuilder-core'; import { Prompt, PromptOptions, PromptRecognizerResult, PromptValidator } from './prompt'; +import * as Chinese from 'cldr-data/main/zh/numbers.json'; +import * as English from 'cldr-data/main/en/numbers.json'; +import * as Dutch from 'cldr-data/main/nl/numbers.json'; +import * as German from 'cldr-data/main/de/numbers.json'; +import * as Japanese from 'cldr-data/main/ja/numbers.json'; +import * as LikelySubtags from 'cldr-data/supplemental/likelySubtags.json'; +import * as NumberingSystem from 'cldr-data/supplemental/numberingSystems.json'; +import * as Portuguese from 'cldr-data/main/pt/numbers.json'; +import * as Spanish from 'cldr-data/main/es/numbers.json'; + +import * as Globalize from 'globalize'; +Globalize.load( + Chinese, English, Dutch, German, Japanese, LikelySubtags, NumberingSystem, Portuguese, Spanish +); + /** * Prompts a user to enter a number. * @@ -33,10 +48,6 @@ export class NumberPrompt extends Prompt { this.defaultLocale = defaultLocale; } - protected onComputeID(): string { - return `numberPrompt[${this.bindingPath()}]`; - } - protected async onPrompt(context: TurnContext, state: any, options: PromptOptions, isRetry: boolean): Promise { if (isRetry && options.retryPrompt) { await context.sendActivity(options.retryPrompt, undefined, InputHints.ExpectingInput); @@ -53,9 +64,23 @@ export class NumberPrompt extends Prompt { const results: any = Recognizers.recognizeNumber(utterance, locale); if (results.length > 0 && results[0].resolution) { result.succeeded = true; - result.value = parseFloat(results[0].resolution.value); + + const culture = this.getCultureFormattedForGlobalize(locale); + const parser = Globalize(culture).numberParser(); + result.value = parser(results[0].resolution.value); } return result; } + + private getCultureFormattedForGlobalize(culture: string) { + // The portions of the Globalize parsing library we use + // only need the first 2 letters for internationalization culture + const formattedCulture = culture.replace( + culture, + `${culture[0]}${culture[1]}` + ); + + return formattedCulture.toLowerCase(); + } } diff --git a/libraries/botbuilder-dialogs/src/prompts/oauthPrompt.ts b/libraries/botbuilder-dialogs/src/prompts/oauthPrompt.ts index 96f33529d5..5bea388c69 100644 --- a/libraries/botbuilder-dialogs/src/prompts/oauthPrompt.ts +++ b/libraries/botbuilder-dialogs/src/prompts/oauthPrompt.ts @@ -5,9 +5,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { Token } from '@microsoft/recognizers-text-date-time'; -import { Activity, ActivityTypes, Attachment, CardFactory, InputHints, MessageFactory, TokenResponse, TurnContext, IUserTokenProvider } from 'botbuilder-core'; -import { Dialog, DialogTurnResult, DialogEvent } from '../dialog'; +import { Activity, ActivityTypes, Attachment, CardFactory, InputHints, MessageFactory, OAuthLoginTimeoutKey, TokenResponse, TurnContext, IUserTokenProvider } from 'botbuilder-core'; +import { Dialog, DialogTurnResult } from '../dialog'; import { DialogContext } from '../dialogContext'; import { PromptOptions, PromptRecognizerResult, PromptValidator } from './prompt'; import { channels } from '../choices/channel'; @@ -73,7 +72,7 @@ export interface OAuthPromptSettings { * needed and their access token will be passed as an argument to the callers next waterfall step: * * ```JavaScript - * const { ConversationState, MemoryStorage } = require('botbuilder'); + * const { ConversationState, MemoryStorage, OAuthLoginTimeoutMsValue } = require('botbuilder'); * const { DialogSet, OAuthPrompt, WaterfallDialog } = require('botbuilder-dialogs'); * * const convoState = new ConversationState(new MemoryStorage()); @@ -83,7 +82,7 @@ export interface OAuthPromptSettings { * dialogs.add(new OAuthPrompt('loginPrompt', { * connectionName: 'GitConnection', * title: 'Login To GitHub', - * timeout: 300000 // User has 5 minutes to login + * timeout: OAuthLoginTimeoutMsValue // User has 15 minutes to login * })); * * dialogs.add(new WaterfallDialog('taskNeedingLogin', [ @@ -249,11 +248,16 @@ export class OAuthPrompt extends Dialog { if (this.channelSupportsOAuthCard(context.activity.channelId)) { const cards: Attachment[] = msg.attachments.filter((a: Attachment) => a.contentType === CardFactory.contentTypes.oauthCard); if (cards.length === 0) { + let link: string = undefined; + if (OAuthPrompt.isFromStreamingConnection(context.activity)) { + link = await (context.adapter as any).getSignInLink(context, this.settings.connectionName); + } // Append oauth card msg.attachments.push(CardFactory.oauthCard( this.settings.connectionName, this.settings.title, - this.settings.text + this.settings.text, + link )); } } else { @@ -269,6 +273,12 @@ export class OAuthPrompt extends Dialog { } } + // Add the login timeout specified in OAuthPromptSettings to TurnState so it can be referenced if polling is needed + if (!context.turnState.get(OAuthLoginTimeoutKey) && this.settings.timeout) + { + context.turnState.set(OAuthLoginTimeoutKey, this.settings.timeout); + } + // Send prompt await context.sendActivity(msg); } @@ -287,7 +297,7 @@ export class OAuthPrompt extends Dialog { await context.sendActivity({ type: 'invokeResponse', value: { status: 404 }}); } } - catch + catch (e) { await context.sendActivity({ type: 'invokeResponse', value: { status: 500 }}); } @@ -301,12 +311,18 @@ export class OAuthPrompt extends Dialog { return token !== undefined ? { succeeded: true, value: token } : { succeeded: false }; } + private static isFromStreamingConnection(activity: Activity): boolean { + return activity && activity.serviceUrl && !activity.serviceUrl.toLowerCase().startsWith('http'); + } + private isTokenResponseEvent(context: TurnContext): boolean { const activity: Activity = context.activity; return activity.type === ActivityTypes.Event && activity.name === 'tokens/response'; } + + private isTeamsVerificationInvoke(context: TurnContext): boolean { const activity: Activity = context.activity; @@ -331,7 +347,7 @@ export class OAuthPrompt extends Dialog { * @private */ interface OAuthPromptState { - state: object; + state: any; options: PromptOptions; expires: number; // Timestamp of when the prompt will timeout. } \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/prompts/prompt.ts b/libraries/botbuilder-dialogs/src/prompts/prompt.ts index 3a4f2af6b3..2fbdf85ccb 100644 --- a/libraries/botbuilder-dialogs/src/prompts/prompt.ts +++ b/libraries/botbuilder-dialogs/src/prompts/prompt.ts @@ -322,11 +322,11 @@ export abstract class Prompt extends Dialog { let msg: Partial; switch (style) { case ListStyle.inline: - msg = ChoiceFactory.inline(choices, text, null, options); + msg = ChoiceFactory.inline(choices, text, undefined, options); break; case ListStyle.list: - msg = ChoiceFactory.list(choices, text, null, options); + msg = ChoiceFactory.list(choices, text, undefined, options); break; case ListStyle.suggestedAction: @@ -342,7 +342,7 @@ export abstract class Prompt extends Dialog { break; default: - msg = ChoiceFactory.forChannel(channelId, choices, text, null, options); + msg = ChoiceFactory.forChannel(channelId, choices, text, undefined, options); break; } @@ -356,7 +356,11 @@ export abstract class Prompt extends Dialog { } if (msg.attachments) { - prompt.attachments = msg.attachments; + if (prompt.attachments) { + prompt.attachments = prompt.attachments.concat(msg.attachments); + } else { + prompt.attachments = msg.attachments; + } } return prompt; @@ -372,6 +376,6 @@ export abstract class Prompt extends Dialog { * @private */ interface PromptState { - state: object; + state: any; options: PromptOptions; } \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/prompts/promptCultureModels.ts b/libraries/botbuilder-dialogs/src/prompts/promptCultureModels.ts new file mode 100644 index 0000000000..df1f29cc21 --- /dev/null +++ b/libraries/botbuilder-dialogs/src/prompts/promptCultureModels.ts @@ -0,0 +1,136 @@ +import { Culture } from '@microsoft/recognizers-text-suite'; + +export interface PromptCultureModel { + /** + * Culture Model's Locale. + * @example + * "en-US" + */ + locale: string; + /** + * Culture Model's InlineSeparator. + * @example + * ", " + */ + separator: string; + /** + * Culture Model's InlineOr. + * @example + * " or " + */ + inlineOr: string; + /** + * Culture Model's InlineOrMore. + * @example + * ", or " + */ + inlineOrMore: string; + /** + * Equivalent of "Yes" in Culture Model's Language. + * @example + * "Yes" + */ + yesInLanguage: string; + /** + * Equivalent of "No" in Culture Model's Language. + * @example + * "No" + */ + noInLanguage: string; +} + +/** + * Class container for currently-supported Culture Models in Confirm and Choice Prompt. + */ +export class PromptCultureModels { + public static Chinese: PromptCultureModel = { + locale: Culture.Chinese, + separator: ', ', + inlineOr: ' 要么 ', + inlineOrMore: ', 要么 ', + yesInLanguage: '是的', + noInLanguage: '不', + } + + public static Dutch: PromptCultureModel = { + locale: Culture.Dutch, + separator: ', ', + inlineOr: ' of ', + inlineOrMore: ', of ', + yesInLanguage: 'Ja', + noInLanguage: 'Nee', + } + + public static English: PromptCultureModel = { + locale: Culture.English, + separator: ', ', + inlineOr: ' or ', + inlineOrMore: ', or ', + yesInLanguage: 'Yes', + noInLanguage: 'No', + } + + public static French: PromptCultureModel = { + locale: Culture.French, + separator: ', ', + inlineOr: ' ou ', + inlineOrMore: ', ou ', + yesInLanguage: 'Oui', + noInLanguage: 'Non', + } + + public static German: PromptCultureModel = { + locale: Culture.German, + separator: ', ', + inlineOr: ' oder ', + inlineOrMore: ', oder ', + yesInLanguage: 'Ja', + noInLanguage: 'Nein', + } + + public static Japanese: PromptCultureModel = { + locale: Culture.Japanese, + separator: '、 ', + inlineOr: ' または ', + inlineOrMore: '、 または ', + yesInLanguage: 'はい', + noInLanguage: 'いいえ', + } + + public static Portuguese: PromptCultureModel = { + locale: Culture.Portuguese, + separator: ', ', + inlineOr: ' ou ', + inlineOrMore: ', ou ', + yesInLanguage: 'Sim', + noInLanguage: 'Não', + } + + public static Spanish: PromptCultureModel = { + locale: Culture.Spanish, + separator: ', ', + inlineOr: ' o ', + inlineOrMore: ', o ', + yesInLanguage: 'Sí', + noInLanguage: 'No', + } + + /** + * Use Recognizers-Text to normalize various potential Locale strings to a standard. + * @param cultureCode Represents locale. Examples: "en-US, en-us, EN". + * @returns Normalized locale. + */ + public static mapToNearestLanguage = (cultureCode: string): string => Culture.mapToNearestLanguage(cultureCode); + + public static getSupportedCultures = (): PromptCultureModel[] => + [ + PromptCultureModels.Chinese, + PromptCultureModels.Dutch, + PromptCultureModels.English, + PromptCultureModels.French, + PromptCultureModels.German, + PromptCultureModels.Japanese, + PromptCultureModels.Portuguese, + PromptCultureModels.Spanish, + ]; +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/prompts/textPrompt.ts b/libraries/botbuilder-dialogs/src/prompts/textPrompt.ts index 50d7cc7106..53cb89a617 100644 --- a/libraries/botbuilder-dialogs/src/prompts/textPrompt.ts +++ b/libraries/botbuilder-dialogs/src/prompts/textPrompt.ts @@ -27,10 +27,6 @@ export class TextPrompt extends Prompt { super(dialogId, validator); } - protected onComputeID(): string { - return `textPrompt[${this.bindingPath()}]`; - } - protected async onPrompt(context: TurnContext, state: any, options: PromptOptions, isRetry: boolean): Promise { if (isRetry && options.retryPrompt) { await context.sendActivity(options.retryPrompt, undefined, InputHints.ExpectingInput); diff --git a/libraries/botbuilder-dialogs/src/waterfallDialog.ts b/libraries/botbuilder-dialogs/src/waterfallDialog.ts index 85cf27bbc1..e1ba7c6c58 100644 --- a/libraries/botbuilder-dialogs/src/waterfallDialog.ts +++ b/libraries/botbuilder-dialogs/src/waterfallDialog.ts @@ -11,7 +11,6 @@ import { DialogInstance } from './dialog'; import { Dialog, DialogReason, DialogTurnResult } from './dialog'; import { DialogContext } from './dialogContext'; import { WaterfallStepContext } from './waterfallStepContext'; -import { StateMap } from './stateMap'; /** * Function signature of an individual waterfall step. @@ -89,10 +88,6 @@ export class WaterfallDialog extends Dialog { } } - protected onComputeID(): string { - return `waterfall[${this.bindingPath()}]`; - } - /** * Adds a new step to the waterfall. * @@ -139,16 +134,18 @@ export class WaterfallDialog extends Dialog { public async beginDialog(dc: DialogContext, options?: O): Promise { // Initialize waterfall state - const state = dc.state.dialog; - state.set(PERSISTED_OPTIONS, options || {}); - state.set(PERSISTED_VALUES, { + const state: WaterfallDialogState = dc.activeDialog.state as WaterfallDialogState; + state.options = options || {}; + state.values = { instanceId: generate_guid() - }); + }; - this.telemetryClient.trackEvent({name: "WaterfallStart", properties: { - "DialogId": this.id, - "InstanceId": state.get(PERSISTED_VALUES)['instanceId'] - }}); + this.telemetryClient.trackEvent({ + name: 'WaterfallStart', properties: { + 'DialogId': this.id, + 'InstanceId': state.values['instanceId'] + } + }); // Run the first step return await this.runStep(dc, 0, DialogReason.beginCalled); @@ -166,9 +163,9 @@ export class WaterfallDialog extends Dialog { public async resumeDialog(dc: DialogContext, reason: DialogReason, result?: any): Promise { // Increment step index and run step - const state = dc.state.dialog; + const state: WaterfallDialogState = dc.activeDialog.state as WaterfallDialogState; - return await this.runStep(dc, state.get(PERSISTED_STEP_INDEX) + 1, reason, result); + return await this.runStep(dc, state.stepIndex + 1, reason, result); } /** @@ -191,35 +188,36 @@ export class WaterfallDialog extends Dialog { // Log Waterfall Step event. var stepName = this.waterfallStepName(step.index); - const state = step.state.dialog; + const state: WaterfallDialogState = step.activeDialog.state as WaterfallDialogState; - var properties = - { - "DialogId": this.id, - "InstanceId": state.get(PERSISTED_VALUES)['instanceId'], - "StepName": stepName + var properties = + { + 'DialogId': this.id, + 'InstanceId': state.values['instanceId'], + 'StepName': stepName, }; - this.telemetryClient.trackEvent({name: 'WaterfallStep', properties: properties}); + this.telemetryClient.trackEvent({ name: 'WaterfallStep', properties: properties }); return await this.steps[step.index](step); } private async runStep(dc: DialogContext, index: number, reason: DialogReason, result?: any): Promise { if (index < this.steps.length) { // Update persisted step index - const state = dc.state.dialog; - state.set(PERSISTED_STEP_INDEX, index); + const state: WaterfallDialogState = dc.activeDialog.state as WaterfallDialogState; + state.stepIndex = index; + // Create step context let nextCalled = false; const step: WaterfallStepContext = new WaterfallStepContext(dc, { index: index, - options: state.get(PERSISTED_OPTIONS), + options: state.options, reason: reason, result: result, - values: state.get(PERSISTED_VALUES), + values: state.values, onNext: async (stepResult?: any): Promise> => { if (nextCalled) { - throw new Error(`WaterfallStepContext.next(): method already called for dialog and step '${ this.id }[${ index }]'.`); + throw new Error(`WaterfallStepContext.next(): method already called for dialog and step '${this.id}[${index}]'.`); } nextCalled = true; return await this.resumeDialog(dc, DialogReason.nextCalled, stepResult); @@ -241,22 +239,27 @@ export class WaterfallDialog extends Dialog { * @param instance The instance of the current dialog. * @param reason The reason the dialog is ending. */ - public async endDialog(context: TurnContext, instance: DialogInstance, reason: DialogReason) { - const state = new StateMap(instance.state); - const instanceId = state.get(PERSISTED_VALUES)['instanceId']; + public async endDialog(context: TurnContext, instance: DialogInstance, reason: DialogReason) { + + const state: WaterfallDialogState = instance.state as WaterfallDialogState; + const instanceId = state.values['instanceId']; if (reason === DialogReason.endCalled) { - this.telemetryClient.trackEvent({name: 'WaterfallComplete', properties: { - 'DialogId': this.id, - 'InstanceId': instanceId, - }}); + this.telemetryClient.trackEvent({ + name: 'WaterfallComplete', properties: { + 'DialogId': this.id, + 'InstanceId': instanceId, + } + }); } else if (reason === DialogReason.cancelCalled) { - var index = state.get(PERSISTED_STEP_INDEX); + var index = instance.state[state.stepIndex]; var stepName = this.waterfallStepName(index); - this.telemetryClient.trackEvent({name: 'WaterfallCancel', properties: { - 'DialogId': this.id, - 'StepName': stepName, - 'InstanceId': instanceId, - }}); + this.telemetryClient.trackEvent({ + name: 'WaterfallCancel', properties: { + 'DialogId': this.id, + 'StepName': stepName, + 'InstanceId': instanceId, + } + }); } } @@ -273,7 +276,7 @@ export class WaterfallDialog extends Dialog { } } } - return stepName; + return stepName; } } @@ -281,23 +284,17 @@ export class WaterfallDialog extends Dialog { /** * @private */ -const PERSISTED_OPTIONS = 'options'; - -/** - * @private - */ -const PERSISTED_STEP_INDEX = 'stepIndex'; - -/** - * @private - */ -const PERSISTED_VALUES = 'values'; +interface WaterfallDialogState { + options: object; + stepIndex: number; + values: object; +} /* * This function generates a GUID-like random number that should be sufficient for our purposes of tracking * instances of a given waterfall dialog. * Source: https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript - */ + */ function generate_guid() { function s4() { return Math.floor((1 + Math.random()) * 0x10000) @@ -305,5 +302,5 @@ function generate_guid() { .substring(1); } return s4() + s4() + '-' + s4() + '-' + s4() + '-' + - s4() + '-' + s4() + s4() + s4(); + s4() + '-' + s4() + s4() + s4(); } \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/src/waterfallStepContext.ts b/libraries/botbuilder-dialogs/src/waterfallStepContext.ts index 44e85c89b5..913f9c7eee 100644 --- a/libraries/botbuilder-dialogs/src/waterfallStepContext.ts +++ b/libraries/botbuilder-dialogs/src/waterfallStepContext.ts @@ -59,7 +59,7 @@ export class WaterfallStepContext extends DialogContext { * @param info Values to initialize the step context with. */ constructor(dc: DialogContext, info: WaterfallStepInfo) { - super(dc.dialogs, dc.context, { dialogStack: dc.stack }, dc.state.user, dc.state.conversation); + super(dc.dialogs, dc.context, { dialogStack: dc.stack }); this._info = info; this.parent = dc.parent; } diff --git a/libraries/botbuilder-dialogs/tests/choicePrompt.test.js b/libraries/botbuilder-dialogs/tests/choicePrompt.test.js index 159dd26a58..a37ff25fd8 100644 --- a/libraries/botbuilder-dialogs/tests/choicePrompt.test.js +++ b/libraries/botbuilder-dialogs/tests/choicePrompt.test.js @@ -1,5 +1,5 @@ -const { ActivityTypes, ConversationState, MemoryStorage, TestAdapter } = require('botbuilder-core'); -const { ChoicePrompt, DialogSet, ListStyle, DialogTurnStatus } = require('../'); +const { ActivityTypes, CardFactory, ConversationState, MemoryStorage, TestAdapter } = require('botbuilder-core'); +const { ChoicePrompt, ChoiceFactory, DialogSet, ListStyle, DialogTurnStatus } = require('../'); const assert = require('assert'); const answerMessage = { text: `red`, type: 'message' }; @@ -226,11 +226,13 @@ describe('ChoicePrompt', function () { }, 'es-es'); dialogs.add(choicePrompt); - await adapter.send({ text: 'Hello', type: ActivityTypes.Message }) - .assertReply('Please choose a color. (1) red, (2) green, o (3) blue') + await adapter.send({ text: 'Hello', type: ActivityTypes.Message, locale: undefined }) + .assertReply((activity) => { + assert('Please choose a color. (1) red, (2) green, o (3) blue'); + }) .send(invalidMessage) .assertReply('bad input.') - .send({ text: 'red', type: ActivityTypes.Message }) + .send({ text: 'red', type: ActivityTypes.Message, locale: undefined }) .assertReply('red'); }); @@ -298,6 +300,146 @@ describe('ChoicePrompt', function () { .assertReply('red'); }); + it('should recognize locale variations of correct locales', async function () { + const locales = [ + 'es-es', + 'nl-nl', + 'en-us', + 'fr-fr', + 'de-de', + 'ja-jp', + 'pt-br', + 'zh-cn' + ]; + // es-ES + const capEnding = (locale) => { + return `${ locale.split('-')[0] }-${ locale.split('-')[1].toUpperCase() }`; + }; + // es-Es + const titleEnding = (locale) => { + locale[3] = locale.charAt(3).toUpperCase(); + return locale; + }; + // ES + const capTwoLetter = (locale) => { + return locale.split('-')[0].toUpperCase(); + }; + // es + const lowerTwoLetter = (locale) => { + return locale.split('-')[0].toLowerCase(); + }; + + // This creates an object of the correct locale along with its test locales + const localeTests = locales.reduce((obj, locale) => { + obj[locale] = [ + locale, + capEnding(locale), + titleEnding(locale), + capTwoLetter(locale), + lowerTwoLetter(locale) + ]; + return obj; + }, {}); + + // Test each valid locale + await Promise.all(Object.keys(localeTests).map(async (validLocale) => { + // Hold the correct answer from when a valid locale is used + let expectedAnswer; + // Test each of the test locales + await Promise.all(localeTests[validLocale].map(async (testLocale) => { + const adapter = new TestAdapter(async (turnContext) => { + const dc = await dialogs.createContext(turnContext); + + const results = await dc.continueDialog(); + if (results.status === DialogTurnStatus.empty) { + await dc.prompt('prompt', { prompt: 'Please choose a color.', choices: stringChoices }); + } else if (results.status === DialogTurnStatus.complete) { + const selectedChoice = results.result; + await turnContext.sendActivity(selectedChoice.value); + } + await convoState.saveChanges(turnContext); + }); + const convoState = new ConversationState(new MemoryStorage()); + + const dialogState = convoState.createProperty('dialogState'); + const dialogs = new DialogSet(dialogState); + const choicePrompt = new ChoicePrompt('prompt', async (prompt) => { + assert(prompt); + if (!prompt.recognized.succeeded) { + await prompt.context.sendActivity('bad input.'); + } + return prompt.recognized.succeeded; + }, 'es-es'); + dialogs.add(choicePrompt); + + await adapter.send({ text: 'Hello', type: ActivityTypes.Message, locale: testLocale }) + .assertReply((activity) => { + // if the valid locale is tested, save the answer because then we can test to see + // if the test locales produce the same answer + if (validLocale === testLocale) { + expectedAnswer = activity.text; + } + assert.strictEqual(activity.text, expectedAnswer); + }); + })); + })); + }); + + it('should accept and recognize custom locale dict', async function() { + const adapter = new TestAdapter(async (turnContext) => { + const dc = await dialogs.createContext(turnContext); + + const results = await dc.continueDialog(); + if (results.status === DialogTurnStatus.empty) { + await dc.prompt('prompt', { prompt: 'Please choose a color.', choices: stringChoices }); + } else if (results.status === DialogTurnStatus.complete) { + const selectedChoice = results.result; + await turnContext.sendActivity(selectedChoice.value); + } + await convoState.saveChanges(turnContext); + }); + const convoState = new ConversationState(new MemoryStorage()); + + const culture = { + inlineOr: ' customOr ', + inlineOrMore: ' customOrMore ', + locale: 'custom-custom', + separator: 'customSeparator', + noInLanguage: 'customNo', + yesInLanguage: 'customYes' + }; + + const customDict = { + [culture.locale]: { + inlineOr: culture.inlineOr, + inlineOrMore: culture.inlineOrMore, + inlineSeparator: culture.separator, + includeNumbers: true, + } + }; + + const dialogState = convoState.createProperty('dialogState'); + const dialogs = new DialogSet(dialogState); + const choicePrompt = new ChoicePrompt('prompt', async (prompt) => { + assert(prompt); + if (!prompt.recognized.succeeded) { + await prompt.context.sendActivity('bad input.'); + } + return prompt.recognized.succeeded; + }, culture.locale, customDict); + dialogs.add(choicePrompt); + + await adapter.send({ text: 'Hello', type: ActivityTypes.Message, locale: culture.locale }) + .assertReply((activity) => { + const expectedChoices = ChoiceFactory.inline(stringChoices, undefined, undefined, { + inlineOr: culture.inlineOr, + inlineOrMore: culture.inlineOrMore, + inlineSeparator: culture.separator + }).text; + assert.strictEqual(activity.text, `Please choose a color.${ expectedChoices }`); + }); + }); + it('should not render choices and not blow up if choices aren\'t passed in', async function () { const adapter = new TestAdapter(async (turnContext) => { const dc = await dialogs.createContext(turnContext); @@ -535,4 +677,84 @@ describe('ChoicePrompt', function () { .assertReply('red'); }); + it('should display choices on a hero card', async function () { + const sizeChoices = ['large', 'medium', 'small']; + const adapter = new TestAdapter(async (turnContext) => { + const dc = await dialogs.createContext(turnContext); + + const results = await dc.continueDialog(); + if (results.status === DialogTurnStatus.empty) { + await dc.prompt('prompt', 'Please choose a size.', sizeChoices); + } else if (results.status === DialogTurnStatus.complete) { + const selectedChoice = results.result; + await turnContext.sendActivity(selectedChoice.value); + } + await convoState.saveChanges(turnContext); + }); + // Create new ConversationState with MemoryStorage and register the state as middleware. + const convoState = new ConversationState(new MemoryStorage()); + + // Create a DialogState property, DialogSet and ChoicePrompt. + const dialogState = convoState.createProperty('dialogState'); + const dialogs = new DialogSet(dialogState); + const choicePrompt = new ChoicePrompt('prompt'); + // Change the ListStyle of the prompt to ListStyle.none. + choicePrompt.style = ListStyle.heroCard; + + dialogs.add(choicePrompt); + + await adapter.send('Hello') + .assertReply(activity => { + assert(activity.attachments.length === 1); + assert(activity.attachments[0].contentType === CardFactory.contentTypes.heroCard); + assert(activity.attachments[0].content.text === 'Please choose a size.'); + }) + .send('1') + .assertReply('large'); + }); + + it('should display choices on a hero card with an additional attachment', async function (done) { + const sizeChoices = ['large', 'medium', 'small']; + const adapter = new TestAdapter(async (turnContext) => { + const dc = await dialogs.createContext(turnContext); + + const results = await dc.continueDialog(); + if (results.status === DialogTurnStatus.empty) { + await dc.prompt('prompt', activity, sizeChoices); + } else if (results.status === DialogTurnStatus.complete) { + const selectedChoice = results.result; + await turnContext.sendActivity(selectedChoice.value); + } + await convoState.saveChanges(turnContext); + }); + // Create new ConversationState with MemoryStorage and register the state as middleware. + const convoState = new ConversationState(new MemoryStorage()); + + // Create a DialogState property, DialogSet and ChoicePrompt. + const dialogState = convoState.createProperty('dialogState'); + const dialogs = new DialogSet(dialogState); + const choicePrompt = new ChoicePrompt('prompt'); + // Change the ListStyle of the prompt to ListStyle.none. + choicePrompt.style = ListStyle.heroCard; + + const card = CardFactory.adaptiveCard({ + "type": "AdaptiveCard", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.2", + "body": [] + }); + + const activity ={ attachments: [card], type: ActivityTypes.Message }; + dialogs.add(choicePrompt); + + adapter.send('Hello') + .assertReply(response => { + assert(response.attachments.length === 2); + assert(response.attachments[0].contentType === CardFactory.contentTypes.adaptiveCard); + assert(response.attachments[1].contentType === CardFactory.contentTypes.heroCard); + }); + done(); + }); + + }); diff --git a/libraries/botbuilder-dialogs/tests/confirmPrompt.test.js b/libraries/botbuilder-dialogs/tests/confirmPrompt.test.js index 597b75c85a..7b54ed3952 100644 --- a/libraries/botbuilder-dialogs/tests/confirmPrompt.test.js +++ b/libraries/botbuilder-dialogs/tests/confirmPrompt.test.js @@ -225,14 +225,14 @@ describe('ConfirmPrompt', function () { const dialogState = convoState.createProperty('dialogState'); const dialogs = new DialogSet(dialogState); - const choicePrompt = new ConfirmPrompt('prompt', async (prompt) => { + const confirmPrompt = new ConfirmPrompt('prompt', async (prompt) => { assert(prompt); if (!prompt.recognized.succeeded) { await prompt.context.sendActivity('bad input.'); } return prompt.recognized.succeeded; }, 'ja-jp'); - dialogs.add(choicePrompt); + dialogs.add(confirmPrompt); await adapter.send({ text: 'Hello', type: ActivityTypes.Message }) .assertReply('Please confirm. (1) はい または (2) いいえ') @@ -263,14 +263,14 @@ describe('ConfirmPrompt', function () { const dialogState = convoState.createProperty('dialogState'); const dialogs = new DialogSet(dialogState); - const choicePrompt = new ConfirmPrompt('prompt', async (prompt) => { + const confirmPrompt = new ConfirmPrompt('prompt', async (prompt) => { assert(prompt); if (!prompt.recognized.succeeded) { await prompt.context.sendActivity('bad input.'); } return prompt.recognized.succeeded; }); - dialogs.add(choicePrompt); + dialogs.add(confirmPrompt); await adapter.send({ text: 'Hello', type: ActivityTypes.Message, locale: 'ja-jp' }) .assertReply('Please confirm. (1) はい または (2) いいえ') @@ -299,14 +299,14 @@ describe('ConfirmPrompt', function () { const dialogState = convoState.createProperty('dialogState'); const dialogs = new DialogSet(dialogState); - const choicePrompt = new ConfirmPrompt('prompt', async (prompt) => { + const confirmPrompt = new ConfirmPrompt('prompt', async (prompt) => { assert(prompt); if (!prompt.recognized.succeeded) { await prompt.context.sendActivity('bad input.'); } return prompt.recognized.succeeded; }, 'es-es'); - dialogs.add(choicePrompt); + dialogs.add(confirmPrompt); await adapter.send({ text: 'Hello', type: ActivityTypes.Message, locale: 'ja-jp' }) .assertReply('Please confirm. (1) はい または (2) いいえ') @@ -314,6 +314,152 @@ describe('ConfirmPrompt', function () { .assertReply('false'); }); + it('should recognize locale variations of correct locales', async function () { + const locales = [ + 'es-es', + 'nl-nl', + 'en-us', + 'fr-fr', + 'de-de', + 'ja-jp', + 'pt-br', + 'zh-cn' + ]; + // es-ES + const capEnding = (locale) => { + return `${ locale.split('-')[0] }-${ locale.split('-')[1].toUpperCase() }`; + }; + // es-Es + const titleEnding = (locale) => { + locale[3] = locale.charAt(3).toUpperCase(); + return locale; + }; + // ES + const capTwoLetter = (locale) => { + return locale.split('-')[0].toUpperCase(); + }; + // es + const lowerTwoLetter = (locale) => { + return locale.split('-')[0].toLowerCase(); + }; + + // This creates an object of the correct locale along with its test locales + const localeTests = locales.reduce((obj, locale) => { + obj[locale] = [ + locale, + capEnding(locale), + titleEnding(locale), + capTwoLetter(locale), + lowerTwoLetter(locale) + ]; + return obj; + }, {}); + + // Test each valid locale + await Promise.all(Object.keys(localeTests).map(async (validLocale) => { + // Hold the correct answer from when a valid locale is used + let expectedAnswer; + // Test each of the test locales + await Promise.all(localeTests[validLocale].map(async (testLocale) => { + const adapter = new TestAdapter(async (turnContext) => { + const dc = await dialogs.createContext(turnContext); + + const results = await dc.continueDialog(); + if (results.status === DialogTurnStatus.empty) { + await dc.prompt('prompt', { prompt: 'Please confirm.' }); + } else if (results.status === DialogTurnStatus.complete) { + const confirmed = results.result; + if (confirmed) { + await turnContext.sendActivity('true'); + } else { + await turnContext.sendActivity('false'); + } + } + await convoState.saveChanges(turnContext); + }); + const convoState = new ConversationState(new MemoryStorage()); + + const dialogState = convoState.createProperty('dialogState'); + const dialogs = new DialogSet(dialogState); + const confirmPrompt = new ConfirmPrompt('prompt', async (prompt) => { + assert(prompt); + if (!prompt.recognized.succeeded) { + await prompt.context.sendActivity('bad input.'); + } + return prompt.recognized.succeeded; + }, testLocale); + dialogs.add(confirmPrompt); + + await adapter.send({ text: 'Hello', type: ActivityTypes.Message, locale: testLocale }) + .assertReply((activity) => { + // if the valid locale is tested, save the answer because then we can test to see + // if the test locales produce the same answer + if (validLocale === testLocale) { + expectedAnswer = activity.text; + } + assert.strictEqual(activity.text, expectedAnswer); + }); + })); + })); + }); + + it('should accept and recognize custom locale dict', async function() { + const adapter = new TestAdapter(async (turnContext) => { + const dc = await dialogs.createContext(turnContext); + + const results = await dc.continueDialog(); + if (results.status === DialogTurnStatus.empty) { + await dc.prompt('prompt', { prompt: 'Please confirm.' }); + } else if (results.status === DialogTurnStatus.complete) { + const confirmed = results.result; + if (confirmed) { + await turnContext.sendActivity('true'); + } else { + await turnContext.sendActivity('false'); + } + } + await convoState.saveChanges(turnContext); + }); + const convoState = new ConversationState(new MemoryStorage()); + + const culture = { + inlineOr: ' customOr ', + inlineOrMore: ' customOrMore ', + locale: 'custom-custom', + separator: 'customSeparator', + noInLanguage: 'customNo', + yesInLanguage: 'customYes' + }; + + const customDict = { + [culture.locale]: { + choices: [culture.yesInLanguage, culture.noInLanguage], + options: { + inlineSeparator: culture.separator, + inlineOr: culture.inlineOr, + inlineOrMore: culture.inlineOrMore, + includeNumbers: true + } + } + }; + + const dialogState = convoState.createProperty('dialogState'); + const dialogs = new DialogSet(dialogState); + const confirmPrompt = new ConfirmPrompt('prompt', async (prompt) => { + assert(prompt); + if (!prompt.recognized.succeeded) { + await prompt.context.sendActivity('bad input.'); + } + return prompt.recognized.succeeded; + }, culture.locale, customDict); + dialogs.add(confirmPrompt); + + await adapter.send({ text: 'Hello', type: ActivityTypes.Message, locale: culture.locale }) + .assertReply('Please confirm. (1) customYes customOr (2) customNo') + .send('customYes') + .assertReply('true'); + }); + it('should recognize yes with no PromptOptions.', async function () { const adapter = new TestAdapter(async (turnContext) => { const dc = await dialogs.createContext(turnContext); diff --git a/libraries/botbuilder-dialogs/tests/dialogContext.test.js b/libraries/botbuilder-dialogs/tests/dialogContext.test.js index 11ccd8f2cf..6264efec62 100644 --- a/libraries/botbuilder-dialogs/tests/dialogContext.test.js +++ b/libraries/botbuilder-dialogs/tests/dialogContext.test.js @@ -263,7 +263,7 @@ describe('DialogContext', function() { } catch (err) { assert(err, `Error not found.`); - assert.strictEqual(err.message, `DialogContext.continue(): Can't continue dialog. A dialog with an id of 'b' wasn't found.`, `unexpected error message thrown: "${err.message}"`); + assert.strictEqual(err.message, `DialogContext.continueDialog(): Can't continue dialog. A dialog with an id of 'b' wasn't found.`, `unexpected error message thrown: "${err.message}"`); return done(); } if (results.status === DialogTurnStatus.empty) { @@ -430,253 +430,4 @@ describe('DialogContext', function() { adapter.send(beginMessage); }); - - it(`should persist userState, conversationState, and thisState changes.`, function (done) { - const adapter = new TestAdapter(async (turnContext) => { - const dc = await dialogs.createContext(turnContext); - - const results = await dc.continueDialog(); - switch (results.status) { - case DialogTurnStatus.empty: - await dc.beginDialog('a'); - break; - - case DialogTurnStatus.complete: - done(); - break; - } - await convoState.saveChanges(turnContext); - }); - - const convoState = new ConversationState(new MemoryStorage()); - - const dialogState = convoState.createProperty('dialogState'); - - const dialogs = new DialogSet(dialogState); - dialogs.add(new WaterfallDialog('a', [ - async function (step) { - step.state.user.set('name', 'bill'); - step.state.conversation.set('order', 1); - step.state.dialog.set('result', 'foo'); - return Dialog.EndOfTurn; - }, - async function (step) { - assert(step.state.user.get('name') === 'bill'); - assert(step.state.conversation.get('order') === 1); - assert(step.state.dialog.get('result') === 'foo'); - done(); - return Dialog.EndOfTurn; - } - ])); - - adapter.send('start') - .send('continue'); - }); - - it(`should persist userState, conversationState, and thisState changes.`, function (done) { - const adapter = new TestAdapter(async (turnContext) => { - const dc = await dialogs.createContext(turnContext); - - const results = await dc.continueDialog(); - switch (results.status) { - case DialogTurnStatus.empty: - await dc.beginDialog('a'); - break; - - case DialogTurnStatus.complete: - done(); - break; - } - await convoState.saveChanges(turnContext); - }); - - const convoState = new ConversationState(new MemoryStorage()); - - const dialogState = convoState.createProperty('dialogState'); - - const dialogs = new DialogSet(dialogState); - dialogs.add(new WaterfallDialog('a', [ - async function (step) { - step.state.user.set('name', 'bill'); - step.state.conversation.set('order', 1); - step.state.dialog.set('result', 'foo'); - return Dialog.EndOfTurn; - }, - async function (step) { - assert(step.state.user.get('name') === 'bill'); - assert(step.state.conversation.get('order') === 1); - assert(step.state.dialog.get('result') === 'foo'); - done(); - return Dialog.EndOfTurn; - } - ])); - - adapter.send('start') - .send('continue'); - }); - - it(`should retrieve state values using state.query().`, function (done) { - const adapter = new TestAdapter(async (turnContext) => { - const dc = await dialogs.createContext(turnContext); - - const results = await dc.continueDialog(); - switch (results.status) { - case DialogTurnStatus.empty: - await dc.beginDialog('a'); - break; - - case DialogTurnStatus.complete: - done(); - break; - } - await convoState.saveChanges(turnContext); - }); - - const convoState = new ConversationState(new MemoryStorage()); - - const dialogState = convoState.createProperty('dialogState'); - - const dialogs = new DialogSet(dialogState); - dialogs.add(new WaterfallDialog('a', [ - async function (step) { - step.state.user.set('name', 'user'); - step.state.conversation.set('name', 'convo'); - step.state.dialog.set('name', 'dlg'); - return Dialog.EndOfTurn; - }, - async function (step) { - let result = step.state.query('user.name'); - assert(result.length === 1 && result[0] === 'user'); - result = step.state.query('conversation.name'); - assert(result.length === 1 && result[0] === 'convo'); - result = step.state.query('dialog.name'); - assert(result.length === 1 && result[0] === 'dlg'); - result = step.state.query('$..name'); - assert(result.length === 3); - done(); - return Dialog.EndOfTurn; - } - ])); - - adapter.send('start') - .send('continue'); - }); - - it(`should retrieve a state value using state.getValue().`, function (done) { - const adapter = new TestAdapter(async (turnContext) => { - const dc = await dialogs.createContext(turnContext); - - const results = await dc.continueDialog(); - switch (results.status) { - case DialogTurnStatus.empty: - await dc.beginDialog('a'); - break; - - case DialogTurnStatus.complete: - done(); - break; - } - await convoState.saveChanges(turnContext); - }); - - const convoState = new ConversationState(new MemoryStorage()); - - const dialogState = convoState.createProperty('dialogState'); - - const dialogs = new DialogSet(dialogState); - dialogs.add(new WaterfallDialog('a', [ - async function (step) { - step.state.user.set('firstName', 'bill'); - return Dialog.EndOfTurn; - }, - async function (step) { - assert(step.state.getValue('user.firstName') === 'bill'); - assert(step.state.getValue('user.lastName') === undefined); - assert(step.state.getValue('user.lastName', 'smith') === 'smith'); - done(); - return Dialog.EndOfTurn; - } - ])); - - adapter.send('start') - .send('continue'); - }); - - it(`should update a state value using state.setValue().`, function (done) { - const adapter = new TestAdapter(async (turnContext) => { - const dc = await dialogs.createContext(turnContext); - - const results = await dc.continueDialog(); - switch (results.status) { - case DialogTurnStatus.empty: - await dc.beginDialog('a'); - break; - - case DialogTurnStatus.complete: - done(); - break; - } - await convoState.saveChanges(turnContext); - }); - - const convoState = new ConversationState(new MemoryStorage()); - - const dialogState = convoState.createProperty('dialogState'); - - const dialogs = new DialogSet(dialogState); - dialogs.add(new WaterfallDialog('a', [ - async function (step) { - step.state.setValue('user.profile.firstName', 'bill'); - return Dialog.EndOfTurn; - }, - async function (step) { - assert(step.state.getValue('user.profile.firstName') === 'bill'); - assert(step.state.getValue('user.profile.lastName') === undefined); - assert(step.state.getValue('user.profile.lastName', 'smith') === 'smith'); - done(); - return Dialog.EndOfTurn; - } - ])); - - adapter.send('start') - .send('continue'); - }); - - it(`should bind input & output state to a dialog.`, function (done) { - const adapter = new TestAdapter(async (turnContext) => { - const dc = await dialogs.createContext(turnContext); - - const results = await dc.continueDialog(); - switch (results.status) { - case DialogTurnStatus.empty: - await dc.beginDialog('a'); - break; - - case DialogTurnStatus.complete: - done(); - break; - } - await convoState.saveChanges(turnContext); - }); - - const convoState = new ConversationState(new MemoryStorage()); - - const dialogState = convoState.createProperty('dialogState'); - - const dialogs = new DialogSet(dialogState); - dialogs.add(new WaterfallDialog('a', [ - async function (step) { - step.state.setValue('user.profile.name', 'bill'); - return await step.beginDialog('b'); - }, - async function (step) { - assert(step.state.getValue('dialog.result.name') === 'bill'); - done(); - return Dialog.EndOfTurn; - } - ])); - dialogs.add(new BindingTestDialog('b', 'user.profile.name', 'dialog.result.name')) - - adapter.send('start'); - }); }); diff --git a/libraries/botbuilder-dialogs/tests/dialogSet.test.js b/libraries/botbuilder-dialogs/tests/dialogSet.test.js index 12c7183b05..f1195b71bc 100644 --- a/libraries/botbuilder-dialogs/tests/dialogSet.test.js +++ b/libraries/botbuilder-dialogs/tests/dialogSet.test.js @@ -8,12 +8,12 @@ const continueMessage = { text: `continue`, type: 'message' }; describe('DialogSet', function () { this.timeout(5000); - it('should throw on createContext(null)', async function () { + it('should throw on createContext(undefined)', async function () { const convoState = new ConversationState(new MemoryStorage()); const dialogSet = new DialogSet(convoState.createProperty('dialogState')); try { - await dialogSet.createContext(null); - assert.fail('should have thrown error on null'); + await dialogSet.createContext(undefined); + assert.fail('should have thrown error on undefined'); } catch (err) { } }); @@ -64,6 +64,25 @@ describe('DialogSet', function () { done(); }); + + it('should increment the dialog ID when adding the same dialog twice.', function (done) { + const convoState = new ConversationState(new MemoryStorage()); + + const dialogState = convoState.createProperty('dialogState'); + const dialogs = new DialogSet(dialogState); + dialogs.add(new WaterfallDialog('a', [ + function (step) { } + ])); + + dialogs.add(new WaterfallDialog('a', [ + function (step) { } + ])); + + assert(dialogs.find('a')); + assert(dialogs.find('a2'), `second dialog didn't have ID incremented`); + done(); + }); + it('should find() a dialog that was added.', function (done) { const convoState = new ConversationState(new MemoryStorage()); diff --git a/libraries/botbuilder-dialogs/tests/memory_dialogStateManager.test.js b/libraries/botbuilder-dialogs/tests/memory_dialogStateManager.test.js new file mode 100644 index 0000000000..a1ce2193f0 --- /dev/null +++ b/libraries/botbuilder-dialogs/tests/memory_dialogStateManager.test.js @@ -0,0 +1,794 @@ +const { ConversationState, UserState, MemoryStorage, TurnContext, TestAdapter } = require('botbuilder-core'); +const { DialogStateManager, Dialog, DialogSet, DialogContext, DialogContainer, ConversationMemoryScope, UserMemoryScope } = require('../'); +const assert = require('assert'); + +const beginMessage = { + text: `begin`, + type: 'message', + channelId: 'test', + from: { id: 'user' }, + recipient: { id: 'bot' }, + conversation: { id: 'convo1' } +}; + +class TestDialog extends Dialog { + constructor(id, message) { + super(id); + this.message = message; + this.dialogType = 'child'; + } + + async beginDialog(dc, options) { + dc.activeDialog.state.isDialog = true; + await dc.context.sendActivity(this.message); + return Dialog.EndOfTurn; + } +} + +class TestContainer extends DialogContainer { + constructor(id, child) { + super(id); + if (child) { + this.dialogs.add(child); + this.childId = child.id; + } + this.dialogType = 'container'; + } + + async beginDialog(dc, options) { + const state = dc.activeDialog.state; + state.isContainer = true; + if (this.childId) { + state.dialog = {}; + const childDc = this.createChildContext(dc); + return await childDc.beginDialog(this.childId, options); + } else { + return Dialog.EndOfTurn; + } + } + + async continueDialog(dc) { + const childDc = this.createChildContext(dc); + if (childDc) { + return await childDc.continueDialog(); + } else { + return Dialog.EndOfTurn; + } + } + + createChildContext(dc) { + const state = dc.activeDialog.state; + if (state.dialog) { + const childDc = new DialogContext(this.dialogs, dc.context, state.dialog); + childDc.parent = dc; + return childDc; + } + + return undefined; + } +} + +async function createConfiguredTestDc(storage) { + if (!storage) { storage = new MemoryStorage() } + const convoState = new ConversationState(storage); + const userState = new UserState(storage); + const config = DialogStateManager.createStandardConfiguration(convoState, userState); + const dc = await createTestDc(convoState); + dc.state.configuration = config; + await dc.state.loadAllScopes(); + + return dc; +} + +async function createTestDc(convoState) { + // Create a DialogState property, DialogSet and register the dialogs. + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const container = new TestContainer('container', new TestDialog('child', 'test message')); + dialogs.add(container); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Start container dialog + await dc.beginDialog('container'); + return dc; +} + +describe('Memory - Dialog State Manager', function() { + this.timeout(5000); + + it('Should create a standard configuration.', async function () { + // Run test + const config = DialogStateManager.createStandardConfiguration(); + assert(config, `No config returned`); + assert(config.pathResolvers.length > 0, `No path resolvers`); + assert(config.memoryScopes.length > 0, `No memory scopes`); + }); + + it('Should create a standard configuration with added conversation state.', async function () { + // Run test + let convoScopeFound = false; + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const config = DialogStateManager.createStandardConfiguration(convoState); + config.memoryScopes.forEach(scope => { + if (scope instanceof ConversationMemoryScope) { + convoScopeFound = true; + assert(scope.name == 'conversation'); + } + }); + assert(convoScopeFound, `no conversation scope added`); + }); + + it('Should create a standard configuration with added conversation and user state.', async function () { + // Run test + let convoScopeFound = false; + let userScopeFound = false; + const dc = await createConfiguredTestDc(); + const config = dc.state.configuration; + config.memoryScopes.forEach(scope => { + if (scope instanceof ConversationMemoryScope) { + convoScopeFound = true; + assert(scope.name == 'conversation'); + } + if (scope instanceof UserMemoryScope) { + userScopeFound = true; + assert(scope.name == 'user'); + } + }); + assert(convoScopeFound, `no conversation scope added`); + assert(userScopeFound, `no user scope added`); + }); + + it('Should create a standard configuration by default.', async function () { + // Create test dc + const convoState = new ConversationState(new MemoryStorage()); + const dc = await createTestDc(convoState); + + // Run test + const config = dc.state.configuration; + assert(config, `No config returned`); + assert(config.pathResolvers.length > 0, `No path resolvers`); + assert(config.memoryScopes.length > 0, `No memory scopes`); + }); + + it('Should support customized configurations.', async function () { + // Create test dc + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const userState = new UserState(storage); + const dc = await createTestDc(convoState); + + // Run test + let convoScopeFound = false; + let userScopeFound = false; + dc.state.configuration = DialogStateManager.createStandardConfiguration(convoState, userState); + const config = dc.state.configuration; + config.memoryScopes.forEach(scope => { + if (scope instanceof ConversationMemoryScope) { convoScopeFound = true } + if (scope instanceof UserMemoryScope) { userScopeFound = true } + }); + assert(convoScopeFound, `no conversation scope added`); + assert(userScopeFound, `no user scope added`); + }); + + it('Should configure its parent when a child DC is configured.', async function () { + // Create test dc + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const userState = new UserState(storage); + const dc = await createTestDc(convoState); + + // Run test + let convoScopeFound = false; + let userScopeFound = false; + dc.child.state.configuration = DialogStateManager.createStandardConfiguration(convoState, userState); + const config = dc.state.configuration; + config.memoryScopes.forEach(scope => { + if (scope instanceof ConversationMemoryScope) { convoScopeFound = true } + if (scope instanceof UserMemoryScope) { userScopeFound = true } + }); + assert(convoScopeFound, `no conversation scope added`); + assert(userScopeFound, `no user scope added`); + }); + + it('Should read & write values to TURN memory scope.', async function () { + // Create test dc + const dc = await createConfiguredTestDc(); + + // Run test + dc.state.setValue('turn.foo', 'bar'); + const value = dc.state.getValue('turn.foo'); + assert(value == 'bar', `value returned: ${value}`); + }); + + it('Should read values from the SETTINGS memory scope.', async function () { + // Create test dc + const dc = await createConfiguredTestDc(); + + // Run test + let count = 0; + for (const key in process.env) { + const expected = process.env[key]; + if (typeof expected == 'string') { + count++ + const value = dc.state.getValue(`settings["${key}"]`); + assert (value == expected, `Value returned for "${key}": ${value}`); + } + } + assert(count > 0, `no settings tested`); + }); + + it('Should read & write values to DIALOG memory scope.', async function () { + // Create test dc + const dc = await createConfiguredTestDc(); + + // Run test + dc.state.setValue('dialog.foo', 'bar'); + const value = dc.state.getValue('dialog.foo'); + assert(value == 'bar', `value returned: ${value}`); + }); + + it('Should read values from the CLASS memory scope.', async function () { + // Create test dc + const dc = await createConfiguredTestDc(); + + // Run test + assert(dc.state.getValue('class.dialogType') === 'container'); + assert(dc.child.state.getValue('class.dialogType') === 'child'); + }); + + it('Should read & write values to THIS memory scope.', async function () { + // Create test dc + const dc = await createConfiguredTestDc(); + + // Run test + dc.state.setValue('this.foo', 'bar'); + const value = dc.state.getValue('this.foo'); + assert(value == 'bar', `value returned: ${value}`); + }); + + it('Should read & write values to CONVERSATION memory scope.', async function () { + // Create test dc + const dc = await createConfiguredTestDc(); + + // Run test + dc.state.setValue('conversation.foo', 'bar'); + const value = dc.state.getValue('conversation.foo'); + assert(value == 'bar', `value returned: ${value}`); + }); + + it('Should read & write values to USER memory scope.', async function () { + // Create test dc + const dc = await createConfiguredTestDc(); + + // Run test + dc.state.setValue('user.foo', 'bar'); + const value = dc.state.getValue('user.foo'); + assert(value == 'bar', `value returned: ${value}`); + }); + + it('Should read & write values using $ alias.', async function () { + // Create test dc + const dc = await createConfiguredTestDc(); + + // Run test + dc.state.setValue('$foo', 'bar'); + assert(dc.state.getValue('dialog.foo') == 'bar', `setValue() failed to use alias.`); + assert(dc.state.getValue('$foo') == 'bar', `getValue() failed to use alias.`); + }); + + it('Should read & write values using # alias.', async function () { + // Create test dc + const dc = await createConfiguredTestDc(); + + // Run test + dc.state.setValue('#foo', 'bar'); + assert(dc.state.getValue('turn.recognized.intents.foo') == 'bar', `setValue() failed to use alias.`); + assert(dc.state.getValue('#foo') == 'bar', `getValue() failed to use alias.`); + }); + + it('Should read & write values using @@ alias.', async function () { + // Create test dc + const dc = await createConfiguredTestDc(); + + // Run test + dc.state.setValue('@@foo', ['bar']); + const value = dc.state.getValue('turn.recognized.entities.foo'); + assert(Array.isArray(value) && value.length == 1, `setValue() failed to use alias.`); + assert(value[0] == 'bar'); + }); + + it('Should read entities using @ alias.', async function () { + // Create test dc + const dc = await createConfiguredTestDc(); + + // Run test + dc.state.setValue('@@foo', ['foo']); + dc.state.setValue('@@bar', [['bar']]); + assert(dc.state.getValue('@foo') == 'foo', `Simple entities not returning.`); + assert(dc.state.getValue('@bar') == 'bar', `Nested entities not returning.`); + }); + + it('Should write a entity using @ alias.', async function () { + // Create test dc + const dc = await createConfiguredTestDc(); + + // Run test + dc.state.setValue('@foo', 'bar'); + assert(dc.state.getValue('@foo') == 'bar', `Entity not round tripping.`); + }); + + it('Should read values using % alias.', async function () { + // Create test dc + const dc = await createConfiguredTestDc(); + + // Run test + assert(dc.state.getValue('%dialogType') === 'container'); + assert(dc.child.state.getValue('%dialogType') === 'child'); + }); + + it('Should delete values in a scope.', async function () { + // Create test dc + const dc = await createConfiguredTestDc(); + + // Run test + dc.state.setValue('turn.foo', 'bar'); + dc.state.deleteValue('turn.foo'); + const value = dc.state.getValue('turn.foo'); + assert(value == undefined, `value returned: ${value}`); + }); + + it('Should persist conversation & user values when saved.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Initialize state and save + dc.state.setValue('user.name', 'test user'); + dc.state.setValue('conversation.foo', 'bar'); + await dc.state.saveAllChanges(); + + // Create new dc and test loaded values + dc = await createConfiguredTestDc(storage); + assert(dc.state.getValue('user.name') == 'test user', `user state not saved`); + assert(dc.state.getValue('conversation.foo') == 'bar', `conversation state not saved`); + }); + + it('Should delete backing conversation & user state.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Initialize state and save + dc.state.setValue('user.name', 'test user'); + dc.state.setValue('conversation.foo', 'bar'); + await dc.state.saveAllChanges(); + + // Create new dc and delete backing state + dc = await createConfiguredTestDc(storage); + await dc.state.deleteScopesMemory('user'); + await dc.state.deleteScopesMemory('conversation'); + assert(dc.state.getValue('user.name') == undefined, `user state not delete`); + assert(dc.state.getValue('conversation.foo') == undefined, `conversation state not deleted`); + + // Double check + dc = await createConfiguredTestDc(storage); + assert(dc.state.getValue('user.name') == undefined, `user state deletion not persisted`); + assert(dc.state.getValue('conversation.foo') == undefined, `conversation state deletion not persisted`); + }); + + it('Should return default value when getValue() called with empty path.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + assert(dc.state.getValue('', 'default') == 'default'); + }); + + it('Should support passing a function to getValue() for the default.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + assert(dc.state.getValue('', () => 'default') == 'default'); + }); + + it('Should raise an error if getValue() called with an invalid scope.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + let = error = false; + try { + dc.state.getValue('foo.bar'); + } catch (err) { + error = true; + } + assert(error); + }); + + it('Should raise an error if getValue() called with an invalid scope.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + let = error = false; + try { + dc.state.getValue('foo.bar'); + } catch (err) { + error = true; + } + assert(error); + }); + + it('Should raise an error if setValue() called with missing path.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + let = error = false; + try { + dc.state.setValue('', 'bar'); + } catch (err) { + error = true; + } + assert(error); + }); + + it('Should raise an error if setValue() called with an invalid scope.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + let = error = false; + try { + dc.state.setValue('foo', 'bar'); + } catch (err) { + error = true; + } + assert(error); + }); + + it('Should overwrite memory when setValue() called with just a scope.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + dc.state.setValue('turn', { foo: 'bar' }); + assert(dc.state.getValue('turn.foo') == 'bar'); + }); + + it('Should raise an error if deleteValue() called with < 2 path path.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + let = error = false; + try { + dc.state.deleteValue('conversation'); + } catch (err) { + error = true; + } + assert(error); + }); + + it('Should raise an error if deleteValue() called with an invalid scope.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + let = error = false; + try { + dc.state.deleteValue('foo.bar'); + } catch (err) { + error = true; + } + assert(error); + }); + + it('Should read & write array values.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + dc.state.setValue('turn.foo', ['bar']); + assert(dc.state.getValue('turn.foo[0]') == 'bar'); + }); + + it('Should delete array values by index.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + dc.state.setValue('turn.test', ['foo', 'bar']); + dc.state.deleteValue('turn.test[0]') + assert(dc.state.getValue('turn.test[0]') == 'bar'); + }); + + it('Should ignore array deletions that are out of range.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + dc.state.setValue('turn.test', []); + dc.state.deleteValue('turn.test[0]') + assert(dc.state.getValue('turn.test').length == 0); + }); + + it('Should ignore property deletions off non-object properties.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + dc.state.setValue('turn.foo', []); + dc.state.deleteValue('turn.foo.bar'); + assert(dc.state.getValue('turn.foo').length == 0); + dc.state.setValue('turn.bar', 'test'); + dc.state.deleteValue('turn.bar.foo'); + assert(dc.state.getValue('turn.bar') == 'test'); + }); + + it('Should ignore property deletions of missing object properties.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + dc.state.setValue('turn.foo', { 'test': 'test' }); + dc.state.deleteValue('turn.foo.bar'); + let count = 0; + const value = dc.state.getValue('turn.foo'); + for (const key in value) { + count++; + } + assert(count == 1); + }); + + it('Should resolve nested expressions.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + dc.state.setValue('turn.addresses', { + 'work': { + street: 'one microsoft way', + city: 'Redmond', + state: 'wa', + zip: '98052' + } + }); + dc.state.setValue('turn.addressKeys', ['work']) + dc.state.setValue('turn.preferredAddress', 0); + const value = dc.state.getValue('turn.addresses[turn.addressKeys[turn.preferredAddress]].zip'); + assert(value == '98052'); + }); + + it('Should find a property quoted with single quotes.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + dc.state.setValue('turn.addresses', { + 'work': { + street: 'one microsoft way', + city: 'Redmond', + state: 'wa', + zip: '98052' + } + }); + const value = dc.state.getValue(`turn.addresses['work'].zip`); + assert(value == '98052'); + }); + + it('Should find a property quoted with double quotes.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + dc.state.setValue('turn.addresses', { + 'work': { + street: 'one microsoft way', + city: 'Redmond', + state: 'wa', + zip: '98052' + } + }); + const value = dc.state.getValue(`turn.addresses["work"].zip`); + assert(value == '98052'); + }); + + it('Should find a property containing embedded quotes.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + dc.state.setValue('turn.addresses', { + '"work"': { + street: 'one microsoft way', + city: 'Redmond', + state: 'wa', + zip: '98052' + } + }); + const value = dc.state.getValue(`turn.addresses['\\"work\\"'].zip`); + assert(value == '98052'); + }); + + it('Should raise an error for paths with miss-matched quotes.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + let error = false; + try { + dc.state.setValue('turn.addresses', { + 'work': { + street: 'one microsoft way', + city: 'Redmond', + state: 'wa', + zip: '98052' + } + }); + dc.state.getValue(`turn.addresses['work"].zip`); + } catch (err) { + error = true; + } + assert(error); + }); + + it('Should raise an error for segments with invalid path chars.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + let error = false; + try { + dc.state.setValue('turn.addresses', { + '~work': { + street: 'one microsoft way', + city: 'Redmond', + state: 'wa', + zip: '98052' + } + }); + dc.state.getValue(`turn.addresses.~work.zip`); + } catch (err) { + error = true; + } + assert(error); + }); + + it('Should raise an error for assignments to a negative array index.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + let error = false; + try { + dc.state.setValue(`turn.foo[-1]`, 'test'); + } catch (err) { + error = true; + } + assert(error); + }); + + it('Should raise an error for array assignments to non-array values.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + let error = false; + try { + dc.state.setValue('turn.foo', 'bar'); + dc.state.setValue(`turn.foo[3]`, 'test'); + } catch (err) { + error = true; + } + assert(error); + }); + + it('Should raise an error for un-matched brackets.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + let error = false; + try { + dc.state.setValue(`turn.foo[0`, 'test'); + } catch (err) { + error = true; + } + assert(error); + }); + + it('Should alow indexer based path lookups.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + dc.state.setValue('turn.foo', 'bar'); + const value = dc.state.getValue('["turn"].["foo"]'); + assert(value == 'bar'); + }); + + it('Should return "undefined" for index lookups again non-arrays.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + dc.state.setValue('turn.foo', 'bar'); + assert(dc.state.getValue('turn.foo[2]') == undefined); + }); + + it('Should return "undefined" when first() called for empty array.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + dc.state.setValue('turn.foo', []); + assert(dc.state.getValue('turn.foo.first()') == undefined); + }); + + it('Should return "undefined" when first() called for empty nested array.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + dc.state.setValue('turn.foo', [[]]); + assert(dc.state.getValue('turn.foo.first()') == undefined); + }); + + it('Should return "undefined" for a missing segment.', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + dc.state.setValue('turn.foo', 'bar'); + const value = dc.state.getValue('turn..foo'); + assert(value == undefined); + }); + + it('Should raise an error for paths starting with a ".".', async function () { + // Create test dc + const storage = new MemoryStorage(); + let dc = await createConfiguredTestDc(storage); + + // Run test + let error = false; + try { + dc.state.setValue('.turn.foo', 'bar'); + } catch (err) { + error = true; + } + assert(error); + }); + +}); \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/tests/memory_memoryScopes.test.js b/libraries/botbuilder-dialogs/tests/memory_memoryScopes.test.js new file mode 100644 index 0000000000..1c55c42aac --- /dev/null +++ b/libraries/botbuilder-dialogs/tests/memory_memoryScopes.test.js @@ -0,0 +1,804 @@ +const { ConversationState, UserState, MemoryStorage, TurnContext, TestAdapter } = require('botbuilder-core'); +const { ClassMemoryScope, ConversationMemoryScope, DialogMemoryScope, + SettingsMemoryScope, ThisMemoryScope, TurnMemoryScope, UserMemoryScope, + Dialog, DialogSet, DialogContext, DialogContainer } = require('../'); +const assert = require('assert'); + +const beginMessage = { + text: `begin`, + type: 'message', + channelId: 'test', + from: { id: 'user' }, + recipient: { id: 'bot' }, + conversation: { id: 'convo1' } +}; + +class TestDialog extends Dialog { + constructor(id, message) { + super(id); + this.message = message; + } + + async beginDialog(dc, options) { + dc.activeDialog.state.isDialog = true; + await dc.context.sendActivity(this.message); + return Dialog.EndOfTurn; + } +} + +class TestContainer extends DialogContainer { + constructor(id, child) { + super(id); + if (child) { + this.dialogs.add(child); + this.childId = child.id; + } + } + + async beginDialog(dc, options) { + const state = dc.activeDialog.state; + state.isContainer = true; + if (this.childId) { + state.dialog = {}; + const childDc = this.createChildContext(dc); + return await childDc.beginDialog(this.childId, options); + } else { + return Dialog.EndOfTurn; + } + } + + async continueDialog(dc) { + const childDc = this.createChildContext(dc); + if (childDc) { + return await childDc.continueDialog(); + } else { + return Dialog.EndOfTurn; + } + } + + createChildContext(dc) { + const state = dc.activeDialog.state; + if (state.dialog) { + const childDc = new DialogContext(this.dialogs, dc.context, state.dialog); + childDc.parent = dc; + return childDc; + } + + return undefined; + } +} + +describe('Memory - Memory Scopes', function() { + this.timeout(5000); + + it('ClassMemoryScope should find registered dialog.', async function () { + // Create new ConversationState with MemoryStorage and register the state as middleware. + const convoState = new ConversationState(new MemoryStorage()); + + // Create a DialogState property, DialogSet and register the dialogs. + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const dialog = new TestDialog('test', 'test message'); + dialogs.add(dialog); + + // Create test context + const context = new TurnContext(new TestAdapter(), beginMessage); + await dialogState.set(context, { + dialogStack: [ + { id: 'test', state: {} } + ] + }); + const dc = await dialogs.createContext(context); + + // Run test + const scope = new ClassMemoryScope(); + const memory = scope.getMemory(dc); + assert(typeof memory == 'object', `memory not returned`); + assert(memory.message == 'test message'); + }); + + it('ClassMemoryScope should not allow setMemory() call.', async function () { + // Create new ConversationState with MemoryStorage and register the state as middleware. + const convoState = new ConversationState(new MemoryStorage()); + + // Create a DialogState property, DialogSet and register the dialogs. + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const dialog = new TestDialog('test', 'test message'); + dialogs.add(dialog); + + // Create test context + const context = new TurnContext(new TestAdapter(), beginMessage); + await dialogState.set(context, { + dialogStack: [ + { id: 'test', state: {} } + ] + }); + const dc = await dialogs.createContext(context); + + // Run test + let error = false; + try { + const scope = new ClassMemoryScope(); + scope.setMemory(dc, {}); + } catch (err) { + error = true; + } + assert(error == true); + }); + + it('ClassMemoryScope should ignore load() and saveChanges() calls.', async function () { + // Create new ConversationState with MemoryStorage and register the state as middleware. + const convoState = new ConversationState(new MemoryStorage()); + + // Create a DialogState property, DialogSet and register the dialogs. + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const dialog = new TestDialog('test', 'test message'); + dialogs.add(dialog); + + // Create test context + const context = new TurnContext(new TestAdapter(), beginMessage); + await dialogState.set(context, { + dialogStack: [ + { id: 'test', state: {} } + ] + }); + const dc = await dialogs.createContext(context); + + // Run test + const scope = new ClassMemoryScope(); + await scope.load(dc); + const memory = scope.getMemory(dc); + memory.message = 'foo'; + await scope.saveChanges(dc); + assert(dialog.message == 'test message'); + }); + + it('ClassMemoryScope should not allow delete() call.', async function () { + // Create new ConversationState with MemoryStorage and register the state as middleware. + const convoState = new ConversationState(new MemoryStorage()); + + // Create a DialogState property, DialogSet and register the dialogs. + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const dialog = new TestDialog('test', 'test message'); + dialogs.add(dialog); + + // Create test context + const context = new TurnContext(new TestAdapter(), beginMessage); + await dialogState.set(context, { + dialogStack: [ + { id: 'test', state: {} } + ] + }); + const dc = await dialogs.createContext(context); + + // Run test + let error = false; + try { + const scope = new ClassMemoryScope(); + await scope.delete(dc); + } catch (err) { + error = true; + } + assert(error == true); + }); + + + it('ConversationMemoryScope should return conversation state.', async function () { + // Create new ConversationState with MemoryStorage and register the state as middleware. + const convoState = new ConversationState(new MemoryStorage()); + + // Create a DialogState property, DialogSet and register the dialogs. + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const dialog = new TestDialog('test', 'test message'); + dialogs.add(dialog); + + // Create test context + const context = new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Initialize conversation state + const state = convoState.createProperty('conversation'); + await state.set(context, { foo: 'bar' }); + + // Run test + const scope = new ConversationMemoryScope(convoState); + const memory = scope.getMemory(dc); + assert(typeof memory == 'object', `state not returned`); + assert(memory.foo == 'bar'); + }); + + it('UserMemoryScope should raise error if not loaded.', async function () { + // Initialize user state + const storage = new MemoryStorage(); + let context = new TurnContext(new TestAdapter(), beginMessage); + let userState = new UserState(storage); + await userState.createProperty('user').set(context, { foo: 'bar' }); + await userState.saveChanges(context); + + // Replace context and convoState with new instances + context = new TurnContext(new TestAdapter(), beginMessage); + userState = new UserState(storage); + + // Create a DialogState property, DialogSet and register the dialogs. + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const dialog = new TestDialog('test', 'test message'); + dialogs.add(dialog); + + // Create test context + const dc = await dialogs.createContext(context); + + // Run test + let error = false; + try { + const scope = new UserMemoryScope(userState); + const memory = scope.getMemory(dc); + } catch (err) { + error = true; + } + assert(error, `state returned`); + }); + + it('UserMemoryScope should return state once loaded.', async function () { + // Initialize user state + const storage = new MemoryStorage(); + let context = new TurnContext(new TestAdapter(), beginMessage); + let userState = new UserState(storage); + await userState.createProperty('user').set(context, { foo: 'bar' }); + await userState.saveChanges(context); + + // Replace context and convoState with new instances + context = new TurnContext(new TestAdapter(), beginMessage); + userState = new UserState(storage); + + // Create a DialogState property, DialogSet and register the dialogs. + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const dialog = new TestDialog('test', 'test message'); + dialogs.add(dialog); + + // Create test context + const dc = await dialogs.createContext(context); + + // Run test + const scope = new UserMemoryScope(userState); + await scope.load(dc); + const memory = scope.getMemory(dc); + assert(typeof memory == 'object', `state not returned`); + assert(memory.foo == 'bar'); + }); + + it('UserMemoryScope should save any changes.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const dialog = new TestDialog('test', 'test message'); + dialogs.add(dialog); + + // Create test context + let context = new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + let userState = new UserState(storage); + const scope = new UserMemoryScope(userState); + await scope.load(dc); + scope.setMemory(dc, { foo: 'bar' }); + await scope.saveChanges(dc); + + // Ensure changes saved + context = new TurnContext(new TestAdapter(), beginMessage); + userState = new UserState(storage); + const memory = await userState.createProperty('user').get(context); + assert(typeof memory == 'object', `state not returned`); + assert(memory.foo == 'bar'); + }); + + it('UserMemoryScope should delete existing state.', async function () { + // Initialize user state + const storage = new MemoryStorage(); + let context = new TurnContext(new TestAdapter(), beginMessage); + let userState = new UserState(storage); + await userState.createProperty('user').set(context, { foo: 'bar' }); + await userState.saveChanges(context); + + // Replace context and convoState with new instances + context = new TurnContext(new TestAdapter(), beginMessage); + userState = new UserState(storage); + + // Create a DialogState property, DialogSet and register the dialogs. + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const dialog = new TestDialog('test', 'test message'); + dialogs.add(dialog); + + // Create test context + const dc = await dialogs.createContext(context); + + // Check state + const scope = new UserMemoryScope(userState); + await scope.load(dc); + let memory = scope.getMemory(dc); + assert(typeof memory == 'object', `state not returned`); + assert(memory.foo == 'bar'); + + // Delete existing memory + await scope.delete(dc); + context = new TurnContext(new TestAdapter(), beginMessage); + userState = new UserState(storage); + memory = await userState.createProperty('user').get(context); + assert(memory == undefined, `state not deleted`); + }); + + it('DialogMemoryScope should return containers state.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const container = new TestContainer('container'); + dialogs.add(container); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + const scope = new DialogMemoryScope(); + await dc.beginDialog('container'); + const memory = scope.getMemory(dc); + assert(typeof memory == 'object', `state not returned`); + assert(memory.isContainer == true); + }); + + it('DialogMemoryScope should return parent containers state for children.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const container = new TestContainer('container', new TestDialog('child', 'test message')); + dialogs.add(container); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + const scope = new DialogMemoryScope(); + await dc.beginDialog('container'); + const childDc = dc.child; + assert(childDc != undefined, `No child DC`); + const memory = scope.getMemory(childDc); + assert(typeof memory == 'object', `state not returned`); + assert(memory.isContainer == true); + }); + + it('DialogMemoryScope should return childs state when no parent.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const dialog = new TestDialog('test', 'test message'); + dialogs.add(dialog); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + const scope = new DialogMemoryScope(); + await dc.beginDialog('test'); + const memory = scope.getMemory(dc); + assert(typeof memory != undefined, `state not returned`); + assert(memory.isDialog == true); + }); + + it('DialogMemoryScope should raise error when no active dialog.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const dialog = new TestDialog('test', 'test message'); + dialogs.add(dialog); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + let error = false; + try { + const scope = new DialogMemoryScope(); + const memory = scope.getMemory(dc); + } catch (err) { + error = true; + } + assert(error); + }); + + it('DialogMemoryScope should overwrite parents memory.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const container = new TestContainer('container', new TestDialog('child', 'test message')); + dialogs.add(container); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + const scope = new DialogMemoryScope(); + await dc.beginDialog('container'); + const childDc = dc.child; + assert(childDc != undefined, `No child DC`); + scope.setMemory(childDc, { foo: 'bar' }); + const memory = scope.getMemory(childDc); + assert(typeof memory == 'object', `state not returned`); + assert(memory.foo == 'bar'); + }); + + it('DialogMemoryScope should overwrite active dialogs memory.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const container = new TestContainer('container'); + dialogs.add(container); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + const scope = new DialogMemoryScope(); + await dc.beginDialog('container'); + scope.setMemory(dc, { foo: 'bar' }); + const memory = scope.getMemory(dc); + assert(typeof memory == 'object', `state not returned`); + assert(memory.foo == 'bar'); + }); + + it('DialogMemoryScope should raise error if setMemory() called without memory.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const container = new TestContainer('container'); + dialogs.add(container); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + let error = false; + try { + const scope = new DialogMemoryScope(); + await dc.beginDialog('container'); + scope.setMemory(dc, undefined); + } catch (err) { + error = true; + } + assert(error); + }); + + it('DialogMemoryScope should raise error if setMemory() called without active dialog.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const container = new TestContainer('container'); + dialogs.add(container); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + let error = false; + try { + const scope = new DialogMemoryScope(); + scope.setMemory(dc, { foo: 'bar' }); + } catch (err) { + error = true; + } + assert(error); + }); + + it('DialogMemoryScope should raise error if delete() called.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const container = new TestContainer('container'); + dialogs.add(container); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + let error = false; + try { + const scope = new DialogMemoryScope(); + await scope.delete(dc); + } catch (err) { + error = true; + } + assert(error); + }); + + it('SettingsMemoryScope should return clone of process env.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const convoState = new ConversationState(new MemoryStorage()); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState).add(new TestDialog('test', 'test message')); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + const scope = new SettingsMemoryScope(); + const memory = scope.getMemory(dc); + assert(typeof memory == 'object', `settings not returned`); + let count = 0; + for (const key in process.env) { + if (typeof process.env[key] == 'string') { + assert(memory[key] == process.env[key]); + count++; + } + } + assert(count > 0, `no settings found.`); + }); + + it('ThisMemoryScope should return active dialogs state.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const dialog = new TestDialog('test', 'test message'); + dialogs.add(dialog); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + const scope = new ThisMemoryScope(); + await dc.beginDialog('test'); + const memory = scope.getMemory(dc); + assert(typeof memory != undefined, `state not returned`); + assert(memory.isDialog == true); + }); + + it('ThisMemoryScope should raise error when no active dialog.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const dialog = new TestDialog('test', 'test message'); + dialogs.add(dialog); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + let error = false; + try { + const scope = new ThisMemoryScope(); + const memory = scope.getMemory(dc); + } catch (err) { + error = true; + } + assert(error); + }); + + it('ThisMemoryScope should overwrite active dialogs memory.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const container = new TestContainer('container'); + dialogs.add(container); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + const scope = new ThisMemoryScope(); + await dc.beginDialog('container'); + scope.setMemory(dc, { foo: 'bar' }); + const memory = scope.getMemory(dc); + assert(typeof memory == 'object', `state not returned`); + assert(memory.foo == 'bar'); + }); + + it('ThisMemoryScope should raise error if setMemory() called without memory.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const container = new TestContainer('container'); + dialogs.add(container); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + let error = false; + try { + const scope = new ThisMemoryScope(); + await dc.beginDialog('container'); + scope.setMemory(dc, undefined); + } catch (err) { + error = true; + } + assert(error); + }); + + it('ThisMemoryScope should raise error if setMemory() called without active dialog.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const container = new TestContainer('container'); + dialogs.add(container); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + let error = false; + try { + const scope = new ThisMemoryScope(); + scope.setMemory(dc, { foo: 'bar' }); + } catch (err) { + error = true; + } + assert(error); + }); + + it('ThisMemoryScope should raise error if delete() called.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const container = new TestContainer('container'); + dialogs.add(container); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + let error = false; + try { + const scope = new ThisMemoryScope(); + await scope.delete(dc); + } catch (err) { + error = true; + } + assert(error); + }); + + it('TurnMemoryScope should persist changes to turn state.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const dialog = new TestDialog('test', 'test message'); + dialogs.add(dialog); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + const scope = new TurnMemoryScope(); + let memory = scope.getMemory(dc); + assert(typeof memory != undefined, `state not returned`); + memory.foo = 'bar'; + memory = scope.getMemory(dc); + assert(memory.foo == 'bar'); + }); + + it('TurnMemoryScope should overwrite values in turn state.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const dialog = new TestDialog('test', 'test message'); + dialogs.add(dialog); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + const scope = new TurnMemoryScope(); + scope.setMemory(dc, { foo: 'bar' }); + const memory = scope.getMemory(dc); + assert(typeof memory != undefined, `state not returned`); + assert(memory.foo == 'bar'); + }); + + it('TurnMemoryScope should raise error when setMemory() called without memory.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const dialog = new TestDialog('test', 'test message'); + dialogs.add(dialog); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + let error = false; + try { + const scope = new TurnMemoryScope(); + scope.setMemory(dc, undefined); + } catch (err) { + error = true; + } + assert(error); + }); + + it('TurnMemoryScope should raise error when delete() called.', async function () { + // Create a DialogState property, DialogSet and register the dialogs. + const storage = new MemoryStorage(); + const convoState = new ConversationState(storage); + const dialogState = convoState.createProperty('dialogs'); + const dialogs = new DialogSet(dialogState); + const dialog = new TestDialog('test', 'test message'); + dialogs.add(dialog); + + // Create test context + const context= new TurnContext(new TestAdapter(), beginMessage); + const dc = await dialogs.createContext(context); + + // Run test + let error = false; + try { + const scope = new TurnMemoryScope(); + await scope.delete(dc); + } catch (err) { + error = true; + } + assert(error); + }); +}); \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/tests/memory_pathResolvers.test.js b/libraries/botbuilder-dialogs/tests/memory_pathResolvers.test.js new file mode 100644 index 0000000000..b1a1237e25 --- /dev/null +++ b/libraries/botbuilder-dialogs/tests/memory_pathResolvers.test.js @@ -0,0 +1,63 @@ +const { AliasPathResolver, AtAtPathResolver, AtPathResolver, + DollarPathResolver, HashPathResolver, PercentPathResolver } = require('../'); +const assert = require('assert'); + +describe('Memory - Path Resolvers', function() { + this.timeout(5000); + + it('AliasPathResolver should should prefix paths.', function (done) { + const resolver = new AliasPathResolver('@', 'turn.recognized.entities.'); + const path = resolver.transformPath('@test'); + assert(path == 'turn.recognized.entities.test', `path: ${path}`); + done(); + }); + + it('AliasPathResolver should should prefix and postfix paths.', function (done) { + const resolver = new AliasPathResolver('@', 'turn.recognized.entities.', '.first()'); + const path = resolver.transformPath('@test'); + assert(path == 'turn.recognized.entities.test.first()', `path: ${path}`); + done(); + }); + + it('AliasPathResolver should ignore non-matching aliases.', function (done) { + const resolver = new AliasPathResolver('@', 'turn.recognized.entities.', '.first()'); + const path = resolver.transformPath('$test'); + assert(path == '$test', `path: ${path}`); + done(); + }); + + it('AtAtPathResolver should transform @@ aliases.', function (done) { + const resolver = new AtAtPathResolver(); + const path = resolver.transformPath('@@test'); + assert(path == 'turn.recognized.entities.test', `path: ${path}`); + done(); + }); + + it('AtPathResolver should transform @ aliases.', function (done) { + const resolver = new AtPathResolver(); + const path = resolver.transformPath('@test'); + assert(path == 'turn.recognized.entities.test.first()', `path: ${path}`); + done(); + }); + + it('DollarPathResolver should transform $ aliases.', function (done) { + const resolver = new DollarPathResolver(); + const path = resolver.transformPath('$test'); + assert(path == 'dialog.test', `path: ${path}`); + done(); + }); + + it('HashPathResolver should transform # aliases.', function (done) { + const resolver = new HashPathResolver(); + const path = resolver.transformPath('#test'); + assert(path == 'turn.recognized.intents.test', `path: ${path}`); + done(); + }); + + it('PercentPathResolver should transform % aliases.', function (done) { + const resolver = new PercentPathResolver(); + const path = resolver.transformPath('%test'); + assert(path == 'class.test', `path: ${path}`); + done(); + }); +}); \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/tests/numberPrompt.test.js b/libraries/botbuilder-dialogs/tests/numberPrompt.test.js index ddd4957efc..bc08424b81 100644 --- a/libraries/botbuilder-dialogs/tests/numberPrompt.test.js +++ b/libraries/botbuilder-dialogs/tests/numberPrompt.test.js @@ -1,11 +1,10 @@ -const { ConversationState, MemoryStorage, TestAdapter } = require('botbuilder-core'); +const { ActivityTypes, ConversationState, MemoryStorage, TestAdapter } = require('botbuilder-core'); const { DialogSet, NumberPrompt, DialogTurnStatus } = require('../'); const assert = require('assert'); describe('NumberPrompt', function () { this.timeout(5000); it('should call NumberPrompt using dc.prompt().', async function () { - // Initialize Testawait adapter. const adapter = new TestAdapter(async (turnContext) => { const dc = await dialogs.createContext(turnContext); @@ -249,4 +248,89 @@ describe('NumberPrompt', function () { .send('0') .assertReply('ok') }); + + it('should consider culture specified in constructor', async function () { + const adapter = new TestAdapter(async (turnContext) => { + const dc = await dialogs.createContext(turnContext); + + const results = await dc.continueDialog(); + if (results.status === DialogTurnStatus.empty) { + await dc.prompt('prompt', 'Please send a number.'); + } else if (results.status === DialogTurnStatus.complete) { + const reply = results.result; + assert.strictEqual(reply, 3.14); + + await turnContext.sendActivity(reply); + } + await convoState.saveChanges(turnContext); + }); + // Create new ConversationState with MemoryStorage and register the state as middleware. + const convoState = new ConversationState(new MemoryStorage()); + + // Create a DialogState property, DialogSet and NumberPrompt. + const dialogState = convoState.createProperty('dialogState'); + const dialogs = new DialogSet(dialogState); + dialogs.add(new NumberPrompt('prompt', undefined, 'es-es')); + + await adapter.send('Hello') + .assertReply('Please send a number.') + .send('3,14') + }); + + it('should consider culture specified in activity', async function () { + const adapter = new TestAdapter(async (turnContext) => { + const dc = await dialogs.createContext(turnContext); + + const results = await dc.continueDialog(); + if (results.status === DialogTurnStatus.empty) { + await dc.prompt('prompt', 'Please send a number.'); + } else if (results.status === DialogTurnStatus.complete) { + const reply = results.result; + assert.strictEqual(reply, 3.14); + + await turnContext.sendActivity(reply); + } + await convoState.saveChanges(turnContext); + }); + // Create new ConversationState with MemoryStorage and register the state as middleware. + const convoState = new ConversationState(new MemoryStorage()); + + // Create a DialogState property, DialogSet and NumberPrompt. + const dialogState = convoState.createProperty('dialogState'); + const dialogs = new DialogSet(dialogState); + dialogs.add(new NumberPrompt('prompt', undefined, 'en-us')); + + await adapter.send('Hello') + .assertReply('Please send a number.') + .send({ type: ActivityTypes.Message, text: "3,14", locale: 'es-es'}) + }); + + it('should consider default to en-us culture when no culture is specified', async function () { + const adapter = new TestAdapter(async (turnContext) => { + const dc = await dialogs.createContext(turnContext); + + const results = await dc.continueDialog(); + if (results.status === DialogTurnStatus.empty) { + await dc.prompt('prompt', 'Please send a number.'); + } else if (results.status === DialogTurnStatus.complete) { + const reply = results.result; + assert.strictEqual(reply, 1500.25); + + await turnContext.sendActivity(reply); + } + await convoState.saveChanges(turnContext); + }); + // Create new ConversationState with MemoryStorage and register the state as middleware. + const convoState = new ConversationState(new MemoryStorage()); + + // Create a DialogState property, DialogSet and NumberPrompt. + const dialogState = convoState.createProperty('dialogState'); + const dialogs = new DialogSet(dialogState); + dialogs.add(new NumberPrompt('prompt', undefined)); + + await adapter.send('Hello') + .assertReply('Please send a number.') + .send('1,500.25') + }); + }); diff --git a/libraries/botbuilder-dialogs/tests/oauthPrompt.test.js b/libraries/botbuilder-dialogs/tests/oauthPrompt.test.js index 292d2546e9..04e7854ddd 100644 --- a/libraries/botbuilder-dialogs/tests/oauthPrompt.test.js +++ b/libraries/botbuilder-dialogs/tests/oauthPrompt.test.js @@ -48,6 +48,7 @@ describe('OAuthPrompt', function () { assert(activity.attachments.length === 1); assert(activity.attachments[0].contentType === CardFactory.contentTypes.oauthCard); assert(activity.inputHint === InputHints.AcceptingInput); + assert(!activity.attachments[0].content.buttons[0].value); // send a mock EventActivity back to the bot with the token adapter.addUserToken(connectionName, activity.channelId, activity.recipient.id, token); @@ -114,6 +115,83 @@ describe('OAuthPrompt', function () { .send(magicCode) .assertReply('Logged in.'); }); + + it('should call OAuthPrompt for streaming connection', async function () { + var connectionName = "myConnection"; + var token = "abc123"; + + // Initialize TestAdapter. + const adapter = new TestAdapter(async (turnContext) => { + const dc = await dialogs.createContext(turnContext); + + const results = await dc.continueDialog(); + if (results.status === DialogTurnStatus.empty) { + await dc.prompt('prompt', { }); + } else if (results.status === DialogTurnStatus.complete) { + if (results.result.token) { + await turnContext.sendActivity(`Logged in.`); + } + else { + await turnContext.sendActivity(`Failed`); + } + } + await convoState.saveChanges(turnContext); + }); + + // Create new ConversationState with MemoryStorage and register the state as middleware. + const convoState = new ConversationState(new MemoryStorage()); + + // Create a DialogState property, DialogSet and AttachmentPrompt. + const dialogState = convoState.createProperty('dialogState'); + const dialogs = new DialogSet(dialogState); + dialogs.add(new OAuthPrompt('prompt', { + connectionName, + title: 'Login', + timeout: 300000 + })); + + const streamingActivity = { + activityId: '1234', + channelId: 'directlinespeech', + serviceUrl: 'urn:botframework.com:websocket:wss://channel.com/blah', + user: { id: 'user', name: 'User Name' }, + bot: { id: 'bot', name: 'Bot Name' }, + conversation: { + id: 'convo1', + properties: { + 'foo': 'bar' + } + }, + attachments: [], + type: 'message', + text: 'Hello' + }; + + await adapter.send(streamingActivity) + .assertReply(activity => { + assert(activity.attachments.length === 1); + assert(activity.attachments[0].contentType === CardFactory.contentTypes.oauthCard); + assert(activity.inputHint === InputHints.AcceptingInput); + assert(activity.attachments[0].content.buttons[0].value); + + // send a mock EventActivity back to the bot with the token + adapter.addUserToken(connectionName, activity.channelId, activity.recipient.id, token); + + var eventActivity = createReply(activity); + eventActivity.type = ActivityTypes.Event; + var from = eventActivity.from; + eventActivity.from = eventActivity.recipient; + eventActivity.recipient = from; + eventActivity.name = "tokens/response"; + eventActivity.value = { + connectionName, + token + }; + + adapter.send(eventActivity); + }) + .assertReply('Logged in.'); + }); }); function createReply(activity) { diff --git a/libraries/botbuilder-dialogs/tsconfig.json b/libraries/botbuilder-dialogs/tsconfig.json index 3788218c0b..c44a0f138e 100644 --- a/libraries/botbuilder-dialogs/tsconfig.json +++ b/libraries/botbuilder-dialogs/tsconfig.json @@ -1,12 +1,14 @@ { "compilerOptions": { - "target": "ESNext", + "target": "es6", + "lib": ["es2015", "dom"], "module": "commonjs", "declaration": true, "sourceMap": true, "outDir": "./lib", "rootDir": "./src", - "types" : ["node"] + "types" : ["node"], + "resolveJsonModule": true }, "include": [ "src/**/*" diff --git a/libraries/botbuilder-testing/package.json b/libraries/botbuilder-testing/package.json index da0cc22bf7..651c5a6a1a 100644 --- a/libraries/botbuilder-testing/package.json +++ b/libraries/botbuilder-testing/package.json @@ -40,7 +40,7 @@ "scripts": { "test": "tsc && nyc mocha tests/", "build": "tsc", - "build-docs": "typedoc --theme markdown --entryPoint botbuilder --excludePrivate --includeDeclarations --ignoreCompilerErrors --module amd --out ..\\..\\doc\\botbuilder .\\lib\\index.d.ts ..\\botbuilder-core\\lib\\index.d.ts ..\\botbuilder-core-extensions\\lib\\index.d.ts ..\\botframework-schema\\lib\\index.d.ts --hideGenerator --name \"Bot Builder SDK\" --readme none", + "build-docs": "typedoc --theme markdown --entryPoint botbuilder --excludePrivate --includeDeclarations --ignoreCompilerErrors --module amd --out ..\\..\\doc\\botbuilder .\\lib\\index.d.ts ..\\botbuilder-core\\lib\\index.d.ts ..\\botframework-schema\\lib\\index.d.ts --hideGenerator --name \"Bot Builder SDK\" --readme none", "clean": "erase /q /s .\\lib", "set-version": "npm version --allow-same-version ${Version}" }, diff --git a/libraries/botbuilder-testing/tsconfig.json b/libraries/botbuilder-testing/tsconfig.json index 3788218c0b..f7ed49127c 100644 --- a/libraries/botbuilder-testing/tsconfig.json +++ b/libraries/botbuilder-testing/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "ESNext", + "target": "es6", + "lib": ["es2015"], "module": "commonjs", "declaration": true, "sourceMap": true, diff --git a/libraries/botbuilder/package.json b/libraries/botbuilder/package.json index 0985e67019..6262b2d440 100644 --- a/libraries/botbuilder/package.json +++ b/libraries/botbuilder/package.json @@ -40,7 +40,7 @@ "scripts": { "test": "tsc && nyc mocha tests/", "build": "tsc", - "build-docs": "typedoc --theme markdown --entryPoint botbuilder --excludePrivate --includeDeclarations --ignoreCompilerErrors --module amd --out ..\\..\\doc\\botbuilder .\\lib\\index.d.ts ..\\botbuilder-core\\lib\\index.d.ts ..\\botbuilder-core-extensions\\lib\\index.d.ts ..\\botframework-schema\\lib\\index.d.ts --hideGenerator --name \"Bot Builder SDK\" --readme none", + "build-docs": "typedoc --theme markdown --entryPoint botbuilder --excludePrivate --includeDeclarations --ignoreCompilerErrors --module amd --out ..\\..\\doc\\botbuilder .\\lib\\index.d.ts ..\\botbuilder-core\\lib\\index.d.ts ..\\botframework-schema\\lib\\index.d.ts --hideGenerator --name \"Bot Builder SDK\" --readme none", "clean": "erase /q /s .\\lib", "set-version": "npm version --allow-same-version ${Version}" }, diff --git a/libraries/botbuilder/src/botFrameworkAdapter.ts b/libraries/botbuilder/src/botFrameworkAdapter.ts index 103116262f..eb822ec7a4 100644 --- a/libraries/botbuilder/src/botFrameworkAdapter.ts +++ b/libraries/botbuilder/src/botFrameworkAdapter.ts @@ -6,72 +6,143 @@ * Licensed under the MIT License. */ -import { Activity, ActivityTypes, BotAdapter, ChannelAccount, ConversationAccount, ConversationParameters, ConversationReference, ConversationsResult, IUserTokenProvider, ResourceResponse, TokenResponse, TurnContext } from 'botbuilder-core'; +import { Activity, ActivityTypes, BotAdapter, BotCallbackHandlerKey, ChannelAccount, ConversationAccount, ConversationParameters, ConversationReference, ConversationsResult, IUserTokenProvider, ResourceResponse, TokenResponse, TurnContext } from 'botbuilder-core'; import { AuthenticationConstants, ChannelValidation, ConnectorClient, EmulatorApiClient, GovernmentConstants, GovernmentChannelValidation, JwtTokenValidation, MicrosoftAppCredentials, SimpleCredentialProvider, TokenApiClient, TokenStatus, TokenApiModels } from 'botframework-connector'; import * as os from 'os'; +export enum StatusCodes { + OK = 200, + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + NOT_FOUND = 404, + METHOD_NOT_ALLOWED = 405, + UPGRADE_REQUIRED = 426, + INTERNAL_SERVER_ERROR = 500, + NOT_IMPLEMENTED = 501, +} + /** - * Express or Restify Request object. + * Represents an Express or Restify request object. + * + * This interface supports the framework and is not intended to be called directly for your code. */ export interface WebRequest { + /** + * Optional. The request body. + */ body?: any; + + /*** + * Optional. The request headers. + */ headers: any; + + /*** + * Optional. The request method. + */ + method?: any; + + /** + * When implemented in a derived class, adds a listener for an event. + * The framework uses this method to retrieve the request body when the + * [body](xref:botbuilder.WebRequest.body) property is `null` or `undefined`. + * + * @param event The event name. + * @param args Arguments used to handle the event. + * + * @returns A reference to the request object. + */ on(event: string, ...args: any[]): any; } /** - * Express or Restify Response object. + * Represents an Express or Restify response object. + * + * This interface supports the framework and is not intended to be called directly for your code. */ export interface WebResponse { + /** + * + * Optional. The underlying socket. + */ + socket?: any; + + /** + * When implemented in a derived class, sends a FIN packet. + * + * @param args The arguments for the end event. + * + * @returns A reference to the response object. + */ end(...args: any[]): any; + + /** + * When implemented in a derived class, sends the response. + * + * @param body The response payload. + * + * @returns A reference to the response object. + */ send(body: any): any; + + /** + * When implemented in a derived class, sets the HTTP status code for the response. + * + * @param status The status code to use. + * + * @returns The status code. + */ status(status: number): any; } /** - * Settings used to configure a `BotFrameworkAdapter` instance. + * Contains settings used to configure a [BotFrameworkAdapter](xref:botbuilder.BotFrameworkAdapter) instance. */ export interface BotFrameworkAdapterSettings { /** - * ID assigned to your bot in the [Bot Framework Portal](https://dev.botframework.com/). + * The ID assigned to your bot in the [Bot Framework Portal](https://dev.botframework.com/). */ appId: string; /** - * Password assigned to your bot in the [Bot Framework Portal](https://dev.botframework.com/). + * The password assigned to your bot in the [Bot Framework Portal](https://dev.botframework.com/). */ appPassword: string; /** - * (Optional) The OAuth API Endpoint for your bot to use. + * Optional. The tenant to acquire the bot-to-channel token from. */ channelAuthTenant?: string; /** - * (Optional) The OAuth API Endpoint for your bot to use. + * Optional. The OAuth API endpoint for your bot to use. */ oAuthEndpoint?: string; + /** - * (Optional) The Open ID Metadata Endpoint for your bot to use. + * Optional. The OpenID Metadata endpoint for your bot to use. */ openIdMetadata?: string; + /** - * (Optional) The channel service option for this bot to validate connections from Azure or other channel locations + * Optional. The channel service option for this bot to validate connections from Azure or other channel locations. */ channelService?: string; } /** - * Response object expected to be sent in response to an `invoke` activity. + * Represents a response returned by a bot when it receives an `invoke` activity. + * + * This interface supports the framework and is not intended to be called directly for your code. */ export interface InvokeResponse { /** - * Status code to return for response. + * The HTTP status code of the response. */ status: number; /** - * (Optional) body to return for response. + * Optional. The body of the response. */ body?: any; } @@ -88,49 +159,66 @@ const USER_AGENT: string = `Microsoft-BotFramework/3.1 BotBuilder/${ pjson.versi `(Node.js,Version=${ NODE_VERSION }; ${ TYPE } ${ RELEASE }; ${ ARCHITECTURE })`; const OAUTH_ENDPOINT = 'https://api.botframework.com'; const US_GOV_OAUTH_ENDPOINT = 'https://api.botframework.azure.us'; -const INVOKE_RESPONSE_KEY: symbol = Symbol('invokeResponse'); + +// This key is exported internally so that the TeamsActivityHandler will not overwrite any already set InvokeResponses. +export const INVOKE_RESPONSE_KEY: symbol = Symbol('invokeResponse'); /** - * A BotAdapter class that connects your bot to Bot Framework channels and the Emulator. + * A [BotAdapter](xref:botbuilder-core.BotAdapter) that can connect a bot to a service endpoint. + * Implements [IUserTokenProvider](xref:botbuilder-core.IUserTokenProvider). * * @remarks - * Use this adapter to connect your bot to the Bot Framework service, through which - * your bot can reach many chat channels like Skype, Slack, and Teams. This adapter - * also allows your bot to work with the Bot Framework Emulator, which simulates - * the Bot Framework service and provides a chat interface for testing and debugging. + * The bot adapter encapsulates authentication processes and sends activities to and receives + * activities from the Bot Connector Service. When your bot receives an activity, the adapter + * creates a turn context object, passes it to your bot application logic, and sends responses + * back to the user's channel. * - * The following example shows the typical adapter setup: + * The adapter processes and directs incoming activities in through the bot middleware pipeline to + * your bot logic and then back out again. As each activity flows in and out of the bot, each + * piece of middleware can inspect or act upon the activity, both before and after the bot logic runs. + * Use the [use](xref:botbuilder-core.BotAdapter.use) method to add [Middleware](xref:botbuilder-core.Middleware) + * objects to your adapter's middleware collection. + * + * For more information, see the articles on + * [How bots work](https://docs.microsoft.com/azure/bot-service/bot-builder-basics) and + * [Middleware](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-middleware). * + * For example: * ```JavaScript * const { BotFrameworkAdapter } = require('botbuilder'); * * const adapter = new BotFrameworkAdapter({ - * appId: process.env.MICROSOFT_APP_ID, - * appPassword: process.env.MICROSOFT_APP_PASSWORD + * appId: process.env.MicrosoftAppId, + * appPassword: process.env.MicrosoftAppPassword * }); + * + * adapter.onTurnError = async (context, error) => { + * // Catch-all logic for errors. + * }; * ``` */ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvider { protected readonly credentials: MicrosoftAppCredentials; protected readonly credentialsProvider: SimpleCredentialProvider; protected readonly settings: BotFrameworkAdapterSettings; + private isEmulatingOAuthCards: boolean; /** - * Creates a new BotFrameworkAdapter instance. + * Creates a new instance of the [BotFrameworkAdapter](xref:botbuilder.BotFrameworkAdapter) class. + * + * @param settings Optional. The settings to use for this adapter instance. * * @remarks - * Settings for this adapter include: - * ```javascript - * { - * "appId": "ID assigned to your bot in the [Bot Framework Portal](https://dev.botframework.com/).", - * "appPassword": "Password assigned to your bot in the [Bot Framework Portal](https://dev.botframework.com/).", - * "openIdMetadata": "The Open ID Metadata Endpoint for your bot to use.", - * "oAuthEndpoint": "The OAuth API Endpoint for your bot to use.", - * "channelService": "(Optional) The channel service option for this bot to validate connections from Azure or other channel locations" - * } - * ``` - * @param settings (optional) configuration settings for the adapter. + * If the `settings` parameter does not include + * [channelService](xref:botbuilder.BotFrameworkAdapterSettings.channelService) or + * [openIdMetadata](xref:botbuilder.BotFrameworkAdapterSettings.openIdMetadata) values, the + * constructor checks the process' environment variables for these values. These values may be + * set when a bot is provisioned on Azure and if so are required for the bot to work properly + * in the global cloud or in a national cloud. + * + * The [BotFrameworkAdapterSettings](xref:botbuilder.BotFrameworkAdapterSettings) class defines + * the available adapter settings. */ constructor(settings?: Partial) { super(); @@ -154,7 +242,7 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide } // Relocate the tenantId field used by MS Teams to a new location (from channelData to conversation) - // This will only occur on actities from teams that include tenant info in channelData but NOT in conversation, + // This will only occur on activities from teams that include tenant info in channelData but NOT in conversation, // thus should be future friendly. However, once the the transition is complete. we can remove this. this.use(async(context, next) => { if (context.activity.channelId === 'msteams' && context.activity && context.activity.conversation && !context.activity.conversation.tenantId && context.activity.channelData && context.activity.channelData.tenant) { @@ -166,29 +254,39 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide } /** - * Resume a conversation with a user, possibly after some time has gone by. + * Asynchronously resumes a conversation with a user, possibly after some time has gone by. * + * @param reference A reference to the conversation to continue. + * @param logic The asynchronous method to call after the adapter middleware runs. + * * @remarks - * This is often referred to as the bot's "Proactive Messaging" flow as it lets the bot proactively - * send messages to a conversation or user without having to reply directly to an incoming message. - * Scenarios like sending notifications or coupons to a user are enabled by this method. + * This is often referred to as a _proactive notification_, the bot can proactively + * send a message to a conversation or user without waiting for an incoming message. + * For example, a bot can use this method to send notifications or coupons to a user. * - * In order to use this method, a ConversationReference must first be extracted from an incoming - * activity. This reference can be stored in a database and used to resume the conversation at a later time. - * The reference can be created from any incoming activity using `TurnContext.getConversationReference(context.activity)`. + * To send a proactive message: + * 1. Save a copy of a [ConversationReference](xref:botframework-schema.ConversationReference) + * from an incoming activity. For example, you can store the conversation reference in a database. + * 1. Call this method to resume the conversation at a later time. Use the saved reference to access the conversation. + * 1. On success, the adapter generates a [TurnContext](xref:botbuilder-core.TurnContext) object and calls the `logic` function handler. + * Use the `logic` function to send the proactive message. + * + * To copy the reference from any incoming activity in the conversation, use the + * [TurnContext.getConversationReference](xref:botbuilder-core.TurnContext.getConversationReference) method. * - * The processing steps for this method are very similar to [processActivity()](#processactivity) - * in that a `TurnContext` will be created which is then routed through the adapters middleware - * before calling the passed in logic handler. The key difference is that since an activity - * wasn't actually received from outside, it has to be created by the bot. The created activity will have its address - * related fields populated but will have a `context.activity.type === undefined`. + * This method is similar to the [processActivity](xref:botbuilder.BotFrameworkAdapter.processActivity) method. + * The adapter creates a [TurnContext](xref:botbuilder-core.TurnContext) and routes it through + * its middleware before calling the `logic` handler. The created activity will have a + * [type](xref:botframework-schema.Activity.type) of 'event' and a + * [name](xref:botframework-schema.Activity.name) of 'continueConversation'. * + * For example: * ```JavaScript * server.post('/api/notifyUser', async (req, res) => { - * // Lookup previously saved conversation reference + * // Lookup previously saved conversation reference. * const reference = await findReference(req.body.refId); * - * // Proactively notify the user + * // Proactively notify the user. * if (reference) { * await adapter.continueConversation(reference, async (context) => { * await context.sendActivity(req.body.message); @@ -199,8 +297,6 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide * } * }); * ``` - * @param reference A `ConversationReference` saved during a previous incoming activity. - * @param logic A function handler that will be called to perform the bots logic after the the adapters middleware has been run. */ public async continueConversation(reference: Partial, logic: (context: TurnContext) => Promise): Promise { const request: Partial = TurnContext.applyConversationReference( @@ -214,36 +310,48 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide } /** - * Starts a new conversation with a user. This is typically used to Direct Message (DM) a member - * of a group. + * Asynchronously creates and starts a conversation with a user on a channel. * + * @param reference A reference for the conversation to create. + * @param logic The asynchronous method to call after the adapter middleware runs. + * * @remarks - * This function creates a new conversation between the bot and a single user, as specified by - * the ConversationReference passed in. In multi-user chat environments, this typically means - * starting a 1:1 direct message conversation with a single user. If called on a reference - * already representing a 1:1 conversation, the new conversation will continue to be 1:1. + * To use this method, you need both the bot's and the user's account information on a channel. + * The Bot Connector service supports the creating of group conversations; however, this + * method and most channels only support initiating a direct message (non-group) conversation. + * + * To create and start a new conversation: + * 1. Get a copy of a [ConversationReference](xref:botframework-schema.ConversationReference) from an incoming activity. + * 1. Set the [user](xref:botframework-schema.ConversationReference.user) property to the + * [ChannelAccount](xref:botframework-schema.ChannelAccount) value for the intended recipient. + * 1. Call this method to request that the channel create a new conversation with the specified user. + * 1. On success, the adapter generates a turn context and calls the `logic` function handler. + * + * To get the initial reference, use the + * [TurnContext.getConversationReference](xref:botbuilder-core.TurnContext.getConversationReference) + * method on any incoming activity in the conversation. * - * * In order to use this method, a ConversationReference must first be extracted from an incoming - * activity. This reference can be stored in a database and used to resume the conversation at a later time. - * The reference can be created from any incoming activity using `TurnContext.getConversationReference(context.activity)`. + * If the channel establishes the conversation, the generated event activity's + * [conversation](xref:botframework-schema.Activity.conversation) property will contain the + * ID of the new conversation. * - * The processing steps for this method are very similar to [processActivity()](#processactivity) - * in that a `TurnContext` will be created which is then routed through the adapters middleware - * before calling the passed in logic handler. The key difference is that since an activity - * wasn't actually received from outside, it has to be created by the bot. The created activity will have its address - * related fields populated but will have a `context.activity.type === undefined`.. + * This method is similar to the [processActivity](xref:botbuilder.BotFrameworkAdapter.processActivity) method. + * The adapter creates a [TurnContext](xref:botbuilder-core.TurnContext) and routes it through + * middleware before calling the `logic` handler. The created activity will have a + * [type](xref:botframework-schema.Activity.type) of 'event' and a + * [name](xref:botframework-schema.Activity.name) of 'createConversation'. * + * For example: * ```JavaScript * // Get group members conversation reference * const reference = TurnContext.getConversationReference(context.activity); - * + * + * // ... * // Start a new conversation with the user * await adapter.createConversation(reference, async (ctx) => { * await ctx.sendActivity(`Hi (in private)`); * }); * ``` - * @param reference A `ConversationReference` of the user to start a new conversation with. - * @param logic A function handler that will be called to perform the bot's logic after the the adapters middleware has been run. */ public async createConversation(reference: Partial, logic?: (context: TurnContext) => Promise): Promise { if (!reference.serviceUrl) { throw new Error(`BotFrameworkAdapter.createConversation(): missing serviceUrl.`); } @@ -275,10 +383,11 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide id: response.id, isGroup: false, conversationType: null, - tenantId: null, + tenantId: reference.conversation.tenantId, name: null, }; request.conversation = conversation; + request.channelData = parameters.channelData; if (response.serviceUrl) { request.serviceUrl = response.serviceUrl; } @@ -288,16 +397,17 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide } /** - * Deletes an activity that was previously sent to a channel. - * + * Asynchronously deletes an existing activity. + * + * This interface supports the framework and is not intended to be called directly for your code. + * Use [TurnContext.deleteActivity](xref:botbuilder-core.TurnContext.deleteActivity) to delete + * an activity from your bot code. + * + * @param context The context object for the turn. + * @param reference Conversation reference information for the activity to delete. + * * @remarks - * Calling `TurnContext.deleteActivity()` is the preferred way of deleting activities (rather than calling it directly from the adapter), as that - * will ensure that any interested middleware will be notified. - * - * > [!TIP] - * > Note: Not all chat channels support this method. Calling it on an unsupported channel may result in an error. - * @param context Context for the current turn of conversation with the user. - * @param reference Conversation reference information for the activity being deleted. + * Not all channels support this operation. For channels that don't, this call may throw an exception. */ public async deleteActivity(context: TurnContext, reference: Partial): Promise { if (!reference.serviceUrl) { throw new Error(`BotFrameworkAdapter.deleteActivity(): missing serviceUrl`); } @@ -310,14 +420,15 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide } /** - * Deletes a member from the current conversation. + * Asynchronously removes a member from the current conversation. + * + * @param context The context object for the turn. + * @param memberId The ID of the member to remove from the conversation. * * @remarks * Remove a member's identity information from the conversation. - * - * Note that this method does not apply to all channels. - * @param context Context for the current turn of conversation with the user. - * @param memberId ID of the member to delete from the conversation. + * + * Not all channels support this operation. For channels that don't, this call may throw an exception. */ public async deleteConversationMember(context: TurnContext, memberId: string): Promise { if (!context.activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.deleteConversationMember(): missing serviceUrl`); } @@ -331,15 +442,20 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide } /** - * Lists the members of a given activity as specified in a TurnContext. + * Asynchronously lists the members of a given activity. + * + * @param context The context object for the turn. + * @param activityId Optional. The ID of the activity to get the members of. If not specified, the current activity ID is used. * + * @returns An array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for + * the users involved in a given activity. + * * @remarks - * Returns an array of ChannelAccount objects representing the users involved in a given activity. + * Returns an array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for + * the users involved in a given activity. * - * This is different from `getConversationMembers()` in that it will return only those users - * directly involved in the activity, not all members of the conversation. - * @param context Context for the current turn of conversation with the user. - * @param activityId (Optional) activity ID to enumerate. If not specified the current activities ID will be used. + * This is different from [getConversationMembers](xref:botbuilder.BotFrameworkAdapter.getConversationMembers) + * in that it will return only those users directly involved in the activity, not all members of the conversation. */ public async getActivityMembers(context: TurnContext, activityId?: string): Promise { if (!activityId) { activityId = context.activity.id; } @@ -358,15 +474,19 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide } /** - * Lists the members of the current conversation as specified in a TurnContext. + * Asynchronously lists the members of the current conversation. + * + * @param context The context object for the turn. * + * @returns An array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for + * all users currently involved in a conversation. + * * @remarks - * Returns an array of ChannelAccount objects representing the users currently involved in the conversation - * in which an activity occured. + * Returns an array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for + * all users currently involved in a conversation. * - * This is different from `getActivityMembers()` in that it will return all - * members of the conversation, not just those directly involved in the activity. - * @param context Context for the current turn of conversation with the user. + * This is different from [getActivityMembers](xref:botbuilder.BotFrameworkAdapter.getActivityMembers) + * in that it will return all members of the conversation, not just those directly involved in a specific activity. */ public async getConversationMembers(context: TurnContext): Promise { if (!context.activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.getConversationMembers(): missing serviceUrl`); } @@ -381,13 +501,28 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide } /** - * Lists the Conversations in which this bot has participated for a given channel server. + * For the specified channel, asynchronously gets a page of the conversations in which this bot has participated. + * + * @param contextOrServiceUrl The URL of the channel server to query or a + * [TurnContext](xref:botbuilder-core.TurnContext) object from a conversation on the channel. + * @param continuationToken Optional. The continuation token from the previous page of results. + * Omit this parameter or use `undefined` to retrieve the first page of results. + * + * @returns A [ConversationsResult](xref:botframework-schema.ConversationsResult) object containing a page of results + * and a continuation token. * * @remarks - * The channel server returns results in pages and each page will include a `continuationToken` - * that can be used to fetch the next page of results from the server. - * @param contextOrServiceUrl The URL of the channel server to query or a TurnContext. This can be retrieved from `context.activity.serviceUrl`. - * @param continuationToken (Optional) token used to fetch the next page of results from the channel server. This should be left as `undefined` to retrieve the first page of results. + * The the return value's [conversations](xref:botframework-schema.ConversationsResult.conversations) property contains a page of + * [ConversationMembers](xref:botframework-schema.ConversationMembers) objects. Each object's + * [id](xref:botframework-schema.ConversationMembers.id) is the ID of a conversation in which the bot has participated on this channel. + * This method can be called from outside the context of a conversation, as only the bot's service URL and credentials are required. + * + * The channel batches results in pages. If the result's + * [continuationToken](xref:botframework-schema.ConversationsResult.continuationToken) property is not empty, then + * there are more pages to get. Use the returned token to get the next page of results. + * If the `contextOrServiceUrl` parameter is a [TurnContext](xref:botbuilder-core.TurnContext), the URL of the channel server is + * retrieved from + * `contextOrServiceUrl`.[activity](xref:botbuilder-core.TurnContext.activity).[serviceUrl](xref:botframework-schema.Activity.serviceUrl). */ public async getConversations(contextOrServiceUrl: TurnContext | string, continuationToken?: string): Promise { const url: string = typeof contextOrServiceUrl === 'object' ? contextOrServiceUrl.activity.serviceUrl : contextOrServiceUrl; @@ -397,10 +532,13 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide } /** - * Retrieves the OAuth token for a user that is in a sign-in flow. - * @param context Context for the current turn of conversation with the user. - * @param connectionName Name of the auth connection to use. - * @param magicCode (Optional) Optional user entered code to validate. + * Asynchronously attempts to retrieve the token for a user that's in a login flow. + * + * @param context The context object for the turn. + * @param connectionName The name of the auth connection to use. + * @param magicCode Optional. The validation code the user entered. + * + * @returns A [TokenResponse](xref:botframework-schema.TokenResponse) object that contains the user token. */ public async getUserToken(context: TurnContext, connectionName: string, magicCode?: string): Promise { if (!context.activity.from || !context.activity.from.id) { @@ -423,11 +561,11 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide } /** - * Signs the user out with the token server. - * @param context Context for the current turn of conversation with the user. - * @param connectionName Name of the auth connection to use. - * @param userId id of user to sign out. - * @returns A promise that represents the work queued to execute. + * Asynchronously signs out the user from the token server. + * + * @param context The context object for the turn. + * @param connectionName The name of the auth connection to use. + * @param userId The ID of user to sign out. */ public async signOutUser(context: TurnContext, connectionName?: string, userId?: string): Promise { if (!context.activity.from || !context.activity.from.id) { @@ -444,9 +582,11 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide } /** - * Gets a signin link from the token server that can be sent as part of a SigninCard. - * @param context Context for the current turn of conversation with the user. - * @param connectionName Name of the auth connection to use. + * Asynchronously gets a sign-in link from the token server that can be sent as part + * of a [SigninCard](xref:botframework-schema.SigninCard). + * + * @param context The context object for the turn. + * @param connectionName The name of the auth connection to use. */ public async getSignInLink(context: TurnContext, connectionName: string): Promise { this.checkEmulatingOAuthCards(context); @@ -464,13 +604,16 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide } /** - * Retrieves the token status for each configured connection for the given user. - * @param context Context for the current turn of conversation with the user. - * @param userId The user Id for which token status is retrieved. - * @param includeFilter Optional comma seperated list of connection's to include. Blank will return token status for all configured connections. - * @returns Array of TokenStatus - * */ - + * Asynchronously retrieves the token status for each configured connection for the given user. + * + * @param context The context object for the turn. + * @param userId Optional. If present, the ID of the user to retrieve the token status for. + * Otherwise, the ID of the user who sent the current activity is used. + * @param includeFilter Optional. A comma-separated list of connection's to include. If present, + * the `includeFilter` parameter limits the tokens this method returns. + * + * @returns The [TokenStatus](xref:botframework-connector.TokenStatus) objects retrieved. + */ public async getTokenStatus(context: TurnContext, userId?: string, includeFilter?: string ): Promise { if (!userId && (!context.activity.from || !context.activity.from.id)) { @@ -485,9 +628,13 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide } /** - * Signs the user out with the token server. - * @param context Context for the current turn of conversation with the user. - * @param connectionName Name of the auth connection to use. + * Asynchronously signs out the user from the token server. + * + * @param context The context object for the turn. + * @param connectionName The name of the auth connection to use. + * @param resourceUrls The list of resource URLs to retrieve tokens for. + * + * @returns A map of the [TokenResponse](xref:botframework-schema.TokenResponse) objects by resource URL. */ public async getAadTokens(context: TurnContext, connectionName: string, resourceUrls: string[]): Promise<{ [propertyName: string]: TokenResponse; @@ -504,9 +651,15 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide } /** - * Tells the token service to emulate the sending of OAuthCards for a channel. + * Asynchronously sends an emulated OAuth card for a channel. + * + * This method supports the framework and is not intended to be called directly for your code. + * * @param contextOrServiceUrl The URL of the emulator. - * @param emulate If `true` the emulator will emulate the sending of OAuthCards. + * @param emulate `true` to send an emulated OAuth card to the emulator; or `false` to not send the card. + * + * @remarks + * When testing a bot in the Bot Framework Emulator, this method can emulate the OAuth card interaction. */ public async emulateOAuthCards(contextOrServiceUrl: TurnContext | string, emulate: boolean): Promise { this.isEmulatingOAuthCards = emulate; @@ -515,45 +668,43 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide } /** - * Processes an incoming request received by the bots web server into a TurnContext. + * Asynchronously creates a turn context and runs the middleware pipeline for an incoming activity. * + * @param req An Express or Restify style request object. + * @param res An Express or Restify style response object. + * @param logic The function to call at the end of the middleware pipeline. + * * @remarks - * This method is the main way a bot receives incoming messages. - * - * This method takes a raw incoming request object from a webserver and processes it into a - * normalized TurnContext that can be used by the bot. This includes any messages sent from a - * user and is the method that drives what is often referred to as the bot's "Reactive Messaging" - * flow. - * - * The following steps will be taken to process the activity: - * - * - The identity of the sender will be verified to be either the Emulator or a valid Microsoft - * server. The bots `appId` and `appPassword` will be used during this process and the request - * will be rejected if the senders identity can't be verified. - * - The activity will be parsed from the body of the incoming request. An error will be returned - * if the activity can't be parsed. - * - A `TurnContext` instance will be created for the received activity and wrapped with a - * [Revocable Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/revocable). - * - The context will be routed through any middleware registered with the adapter using - * [use()](#use). Middleware is executed in the order in which it's added and any middleware - * can intercept or prevent further routing of the context by simply not calling the passed - * in `next()` function. This is called the "Leading Edge" of the request; middleware will - * get a second chance to run on the "Trailing Edge" of the request after the bots logic has run. - * - Assuming the context hasn't been intercepted by a piece of middleware, the context will be - * passed to the logic handler passed in. The bot may perform additional routing or - * processing at this time. Returning a promise (or providing an `async` handler) will cause the - * adapter to wait for any asynchronous operations to complete. - * - Once the bot's logic completes, the promise chain set up by the middleware stack will be resolved, - * giving middleware a second chance to run on the "Trailing Edge" of the request. - * - After the middleware stack's promise chain has been fully resolved the context object will be - * `revoked()` and any future calls to the context will result in a `TypeError: Cannot perform - * 'set' on a proxy that has been revoked` being thrown. + * This is the main way a bot receives incoming messages and defines a turn in the conversation. This method: + * + * 1. Parses and authenticates an incoming request. + * - The activity is read from the body of the incoming request. An error will be returned + * if the activity can't be parsed. + * - The identity of the sender is authenticated as either the Emulator or a valid Microsoft + * server, using the bot's `appId` and `appPassword`. The request is rejected if the sender's + * identity is not verified. + * 1. Creates a [TurnContext](xref:botbuilder-core.TurnContext) object for the received activity. + * - This object is wrapped with a [revocable proxy](https://www.ecma-international.org/ecma-262/6.0/#sec-proxy.revocable). + * - When this method completes, the proxy is revoked. + * 1. Sends the turn context through the adapter's middleware pipeline. + * 1. Sends the turn context to the `logic` function. + * - The bot may perform additional routing or processing at this time. + * Returning a promise (or providing an `async` handler) will cause the adapter to wait for any asynchronous operations to complete. + * - After the `logic` function completes, the promise chain set up by the middleware is resolved. * * > [!TIP] - * > Note: If you see the error `TypeError: Cannot perform 'set' on a proxy that has been revoked` - * > appearing in your bot's console output, the likely cause is that an async function was used + * > If you see the error `TypeError: Cannot perform 'set' on a proxy that has been revoked` + * > in your bot's console output, the likely cause is that an async function was used * > without using the `await` keyword. Make sure all async functions use await! * + * Middleware can _short circuit_ a turn. When this happens, subsequent middleware and the + * `logic` function is not called; however, all middleware prior to this point still run to completion. + * For more information about the middleware pipeline, see the + * [how bots work](https://docs.microsoft.com/azure/bot-service/bot-builder-basics) and + * [middleware](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-middleware) articles. + * Use the adapter's [use](xref:botbuilder-core.BotAdapter.use) method to add middleware to the adapter. + * + * For example: * ```JavaScript * server.post('/api/messages', (req, res) => { * // Route received request to adapter for processing @@ -565,13 +716,11 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide * }); * }); * ``` - * @param req An Express or Restify style Request object. - * @param res An Express or Restify style Response object. - * @param logic A function handler that will be called to perform the bots logic after the received activity has been pre-processed by the adapter and routed through any middleware for processing. */ public async processActivity(req: WebRequest, res: WebResponse, logic: (context: TurnContext) => Promise): Promise { let body: any; let status: number; + let processError: Error; try { // Parse body of request status = 400; @@ -585,6 +734,7 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide // Process received activity status = 500; const context: TurnContext = this.createContext(request); + context.turnState.set(BotCallbackHandlerKey, logic); await this.runMiddleware(context, logic); // Retrieve cached invoke response. @@ -601,6 +751,8 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide status = 200; } } catch (err) { + // Catch the error to try and throw the stacktrace out of processActivity() + processError = err; body = err.toString(); } @@ -611,28 +763,76 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide // Check for an error if (status >= 400) { - console.warn(`BotFrameworkAdapter.processActivity(): ${ status } ERROR - ${ body.toString() }`); - throw new Error(body.toString()); + if (processError && (processError as Error).stack) { + throw new Error(`BotFrameworkAdapter.processActivity(): ${ status } ERROR\n ${ processError.stack }`); + } else { + throw new Error(`BotFrameworkAdapter.processActivity(): ${ status } ERROR`); + } } } /** - * Sends a set of outgoing activities to the appropriate channel server. + * Asynchronously creates a turn context and runs the middleware pipeline for an incoming activity. * + * @param activity The activity to process. + * @param logic The function to call at the end of the middleware pipeline. + * * @remarks - * The activities will be sent one after another in the order in which they're received. A response object will be returned for each - * sent activity. For `message` activities this will contain the id of the delivered message. + * This is the main way a bot receives incoming messages and defines a turn in the conversation. This method: + * + * 1. Creates a [TurnContext](xref:botbuilder-core.TurnContext) object for the received activity. + * - This object is wrapped with a [revocable proxy](https://www.ecma-international.org/ecma-262/6.0/#sec-proxy.revocable). + * - When this method completes, the proxy is revoked. + * 1. Sends the turn context through the adapter's middleware pipeline. + * 1. Sends the turn context to the `logic` function. + * - The bot may perform additional routing or processing at this time. + * Returning a promise (or providing an `async` handler) will cause the adapter to wait for any asynchronous operations to complete. + * - After the `logic` function completes, the promise chain set up by the middleware is resolved. * - * Instead of calling these methods directly on the adapter, calling `TurnContext.sendActivities()` or `TurnContext.sendActivity()` - * is the preferred way of sending activities as that will ensure that outgoing activities have been properly addressed - * and that any interested middleware has been notified. + * Middleware can _short circuit_ a turn. When this happens, subsequent middleware and the + * `logic` function is not called; however, all middleware prior to this point still run to completion. + * For more information about the middleware pipeline, see the + * [how bots work](https://docs.microsoft.com/azure/bot-service/bot-builder-basics) and + * [middleware](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-middleware) articles. + * Use the adapter's [use](xref:botbuilder-core.BotAdapter.use) method to add middleware to the adapter. + */ + public async processActivityDirect(activity: Activity, logic: (context: TurnContext) => Promise): Promise { + let processError: Error; + try { + // Process activity + const context: TurnContext = this.createContext(activity); + context.turnState.set(BotCallbackHandlerKey, logic); + await this.runMiddleware(context, logic); + } catch (err) { + // Catch the error to try and throw the stacktrace out of processActivity() + processError = err; + } + + if (processError) { + if (processError && (processError as Error).stack) { + throw new Error(`BotFrameworkAdapter.processActivity(): ${ status } ERROR\n ${ processError.stack }`); + } else { + throw new Error(`BotFrameworkAdapter.processActivity(): ${ status } ERROR`); + } + } + } + + /** + * Asynchronously sends a set of outgoing activities to a channel server. + * + * This method supports the framework and is not intended to be called directly for your code. + * Use the turn context's [sendActivity](xref:botbuilder-core.TurnContext.sendActivity) or + * [sendActivities](xref:botbuilder-core.TurnContext.sendActivities) method from your bot code. * - * The primary scenario for calling this method directly is when you want to explicitly bypass - * going through any middleware. For instance, sending the `typing` activity might - * be a good reason to call this method directly as those activities are unlikely to require - * handling by middleware. - * @param context Context for the current turn of conversation with the user. - * @param activities List of activities to send. + * @param context The context object for the turn. + * @param activities The activities to send. + * + * @returns An array of [ResourceResponse](xref:) + * + * @remarks + * The activities will be sent one after another in the order in which they're received. A + * response object will be returned for each sent activity. For `message` activities this will + * contain the ID of the delivered message. */ public async sendActivities(context: TurnContext, activities: Partial[]): Promise { const responses: ResourceResponse[] = []; @@ -676,15 +876,17 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide } /** - * Replaces an activity that was previously sent to a channel with an updated version. - * + * Asynchronously replaces a previous activity with an updated version. + * + * This interface supports the framework and is not intended to be called directly for your code. + * Use [TurnContext.updateActivity](xref:botbuilder-core.TurnContext.updateActivity) to update + * an activity from your bot code. + * + * @param context The context object for the turn. + * @param activity The updated version of the activity to replace. + * * @remarks - * Calling `TurnContext.updateActivity()` is the preferred way of updating activities (rather than calling it directly from the adapter) as that - * will ensure that any interested middleware has been notified. - * - * It should be noted that not all channels support this feature. - * @param context Context for the current turn of conversation with the user. - * @param activity New activity to replace a current activity with. + * Not all channels support this operation. For channels that don't, this call may throw an exception. */ public async updateActivity(context: TurnContext, activity: Partial): Promise { if (!activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.updateActivity(): missing serviceUrl`); } @@ -700,6 +902,32 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide ); } + /** + * Creates a connector client. + * + * @param serviceUrl The client's service URL. + * + * @remarks + * Override this in a derived class to create a mock connector client for unit testing. + */ + public createConnectorClient(serviceUrl: string): ConnectorClient { + const client: ConnectorClient = new ConnectorClient(this.credentials, { baseUri: serviceUrl, userAgent: USER_AGENT} ); + return client; + } + + /** + * Creates an OAuth API client. + * + * @param serviceUrl The client's service URL. + * + * @remarks + * Override this in a derived class to create a mock OAuth API client for unit testing. + */ + protected createTokenApiClient(serviceUrl: string): TokenApiClient { + const client = new TokenApiClient(this.credentials, { baseUri: serviceUrl, userAgent: USER_AGENT} ); + return client; + } + /** * Allows for the overriding of authentication in unit tests. * @param request Received request. @@ -715,26 +943,15 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide } /** - * Allows for mocking of the connector client in unit tests. - * @param serviceUrl Clients service url. - */ - protected createConnectorClient(serviceUrl: string): ConnectorClient { - const client: ConnectorClient = new ConnectorClient(this.credentials, { baseUri: serviceUrl, userAgent: USER_AGENT} ); - return client; - } - - /** - * Allows for mocking of the OAuth API Client in unit tests. - * @param serviceUrl Clients service url. - */ - protected createTokenApiClient(serviceUrl: string): TokenApiClient { - const client = new TokenApiClient(this.credentials, { baseUri: serviceUrl, userAgent: USER_AGENT} ); - return client; - } - - /** - * Allows for mocking of the OAuth Api URL in unit tests. - * @param contextOrServiceUrl The URL of the channel server to query or a TurnContext. This can be retrieved from `context.activity.serviceUrl`. + * Gets the OAuth API endpoint. + * + * @param contextOrServiceUrl The URL of the channel server to query or + * a [TurnContext](xref:botbuilder-core.TurnContext). For a turn context, the context's + * [activity](xref:botbuilder-core.TurnContext.activity).[serviceUrl](xref:botframework-schema.Activity.serviceUrl) + * is used for the URL. + * + * @remarks + * Override this in a derived class to create a mock OAuth API endpoint for unit testing. */ protected oauthApiUrl(contextOrServiceUrl: TurnContext | string): string { return this.isEmulatingOAuthCards ? @@ -745,20 +962,28 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide } /** - * Allows for mocking of toggling the emulating OAuthCards in unit tests. - * @param context The TurnContext + * Checks the environment and can set a flag to emulate OAuth cards. + * + * @param context The context object for the turn. + * + * @remarks + * Override this in a derived class to control how OAuth cards are emulated for unit testing. */ protected checkEmulatingOAuthCards(context: TurnContext): void { if (!this.isEmulatingOAuthCards && context.activity.channelId === 'emulator' && - (!this.credentials.appId || !this.credentials.appPassword)) { + (!this.credentials.appId)) { this.isEmulatingOAuthCards = true; } } /** - * Allows for the overriding of the context object in unit tests and derived adapters. - * @param request Received request. + * Creates a turn context. + * + * @param request An incoming request body. + * + * @remarks + * Override this in a derived class to modify how the adapter creates a turn context. */ protected createContext(request: Partial): TurnContext { return new TurnContext(this as any, request); @@ -766,7 +991,7 @@ export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvide } /** - * Handle incoming webhooks from the botframework + * Handles incoming webhooks from the botframework * @private * @param req incoming web request */ diff --git a/libraries/botbuilder/src/index.ts b/libraries/botbuilder/src/index.ts index 0aa5838d49..a8c9204aea 100644 --- a/libraries/botbuilder/src/index.ts +++ b/libraries/botbuilder/src/index.ts @@ -9,4 +9,7 @@ export * from './botFrameworkAdapter'; export * from './fileTranscriptStore'; export * from './inspectionMiddleware'; +export * from './teamsActivityHandler'; +export * from './teamsActivityHelpers'; +export * from './teamsInfo'; export * from 'botbuilder-core'; diff --git a/libraries/botbuilder/src/teamsActivityHandler.ts b/libraries/botbuilder/src/teamsActivityHandler.ts new file mode 100644 index 0000000000..a52a03af9d --- /dev/null +++ b/libraries/botbuilder/src/teamsActivityHandler.ts @@ -0,0 +1,541 @@ +/** + * @module botbuilder + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { InvokeResponse, INVOKE_RESPONSE_KEY } from './botFrameworkAdapter'; + +import { + ActivityHandler, + ActivityTypes, + AppBasedLinkQuery, + ChannelAccount, + ChannelInfo, + FileConsentCardResponse, + MessagingExtensionAction, + MessagingExtensionActionResponse, + MessagingExtensionQuery, + MessagingExtensionResponse, + O365ConnectorCardActionQuery, + SigninStateVerificationQuery, + TaskModuleTaskInfo, + TaskModuleRequest, + TaskModuleResponse, + TaskModuleResponseBase, + TeamsChannelData, + TeamsChannelAccount, + TeamInfo, + TurnContext +} from 'botbuilder-core'; +import { TeamsInfo } from './teamsInfo'; + +export class TeamsActivityHandler extends ActivityHandler { + + /** + * + * @param context + */ + protected async onTurnActivity(context: TurnContext): Promise { + switch (context.activity.type) { + case ActivityTypes.Invoke: + const invokeResponse = await this.onInvokeActivity(context); + // If onInvokeActivity has already sent an InvokeResponse, do not send another one. + if (invokeResponse && !context.turnState.get(INVOKE_RESPONSE_KEY)) { + await context.sendActivity({ value: invokeResponse, type: 'invokeResponse' }); + } + break; + default: + await super.onTurnActivity(context); + break; + } + } + + /** + * + * @param context + */ + protected async onInvokeActivity(context: TurnContext): Promise { + try { + if (!context.activity.name && context.activity.channelId === 'msteams') { + return await this.handleTeamsCardActionInvoke(context); + } else { + switch (context.activity.name) { + case 'signin/verifyState': + await this.handleTeamsSigninVerifyState(context, context.activity.value); + return TeamsActivityHandler.createInvokeResponse(); + + case 'fileConsent/invoke': + return TeamsActivityHandler.createInvokeResponse(await this.handleTeamsFileConsent(context, context.activity.value)); + + case 'actionableMessage/executeAction': + await this.handleTeamsO365ConnectorCardAction(context, context.activity.value); + return TeamsActivityHandler.createInvokeResponse(); + + case 'composeExtension/queryLink': + return TeamsActivityHandler.createInvokeResponse(await this.handleTeamsAppBasedLinkQuery(context, context.activity.value)); + + case 'composeExtension/query': + return TeamsActivityHandler.createInvokeResponse(await this.handleTeamsMessagingExtensionQuery(context, context.activity.value)); + + case 'composeExtension/selectItem': + return TeamsActivityHandler.createInvokeResponse(await this.handleTeamsMessagingExtensionSelectItem(context, context.activity.value)); + + case 'composeExtension/submitAction': + return TeamsActivityHandler.createInvokeResponse(await this.handleTeamsMessagingExtensionSubmitActionDispatch(context, context.activity.value)); + + case 'composeExtension/fetchTask': + return TeamsActivityHandler.createInvokeResponse(await this.handleTeamsMessagingExtensionFetchTask(context, context.activity.value)); + + case 'composeExtension/querySettingUrl': + return TeamsActivityHandler.createInvokeResponse(await this.handleTeamsMessagingExtensionConfigurationQuerySettingUrl(context, context.activity.value)); + + case 'composeExtension/setting': + await this.handleTeamsMessagingExtensionConfigurationSetting(context, context.activity.value); + return TeamsActivityHandler.createInvokeResponse(); + + case 'composeExtension/onCardButtonClicked': + await this.handleTeamsMessagingExtensionCardButtonClicked(context, context.activity.value); + return TeamsActivityHandler.createInvokeResponse(); + + case 'task/fetch': + return TeamsActivityHandler.createInvokeResponse(await this.handleTeamsTaskModuleFetch(context, context.activity.value)); + + case 'task/submit': + return TeamsActivityHandler.createInvokeResponse(await this.handleTeamsTaskModuleSubmit(context, context.activity.value)); + + default: + throw new Error('NotImplemented'); + } + } + } catch (err) { + if (err.message === 'NotImplemented') { + return { status: 501 }; + } else if (err.message === 'BadRequest') { + return { status: 400 }; + } + throw err; + } + } + + /** + * + * @param context + */ + protected async handleTeamsCardActionInvoke(context: TurnContext): Promise { + throw new Error('NotImplemented'); + } + + /** + * Receives invoke activities with Activity name of 'fileConsent/invoke'. Handlers registered here run before + * `handleTeamsFileConsentAccept` and `handleTeamsFileConsentDecline`. + * Developers are not passed a pointer to the next `handleTeamsFileConsent` handler because the _wrapper_ around + * the handler will call `onDialogs` handlers after delegating to `handleTeamsFileConsentAccept` or `handleTeamsFileConsentDecline`. + * @param context + * @param fileConsentCardResponse + */ + protected async handleTeamsFileConsent(context: TurnContext, fileConsentCardResponse: FileConsentCardResponse): Promise { + switch (fileConsentCardResponse.action) { + case 'accept': + return await this.handleTeamsFileConsentAccept(context, fileConsentCardResponse); + case 'decline': + return await this.handleTeamsFileConsentDecline(context, fileConsentCardResponse); + default: + throw new Error('BadRequest'); + } + } + + /** + * Receives invoke activities with Activity name of 'fileConsent/invoke' with confirmation from user + * @remarks + * This type of invoke activity occur during the File Consent flow. + * @param context + * @param fileConsentCardResponse + */ + protected async handleTeamsFileConsentAccept(context: TurnContext, fileConsentCardResponse: FileConsentCardResponse): Promise { + throw new Error('NotImplemented'); + } + + /** + * Receives invoke activities with Activity name of 'fileConsent/invoke' with decline from user + * @remarks + * This type of invoke activity occur during the File Consent flow. + * @param context + * @param fileConsentCardResponse + */ + protected async handleTeamsFileConsentDecline(context: TurnContext, fileConsentCardResponse: FileConsentCardResponse): Promise { + throw new Error('NotImplemented'); + } + + /** + * Receives invoke activities with Activity name of 'actionableMessage/executeAction' + */ + protected async handleTeamsO365ConnectorCardAction(context: TurnContext, query: O365ConnectorCardActionQuery): Promise { + throw new Error('NotImplemented'); + } + + /** + * Receives invoke activities with Activity name of 'signin/verifyState' + * @param context + * @param action + */ + protected async handleTeamsSigninVerifyState(context: TurnContext, query: SigninStateVerificationQuery): Promise { + throw new Error('NotImplemented'); + } + + /** + * Receives invoke activities with Activity name of 'composeExtension/onCardButtonClicked' + * @param context + * @param cardData + */ + protected async handleTeamsMessagingExtensionCardButtonClicked(context: TurnContext, cardData: any): Promise { + throw new Error('NotImplemented'); + } + + /** + * Receives invoke activities with Activity name of 'task/fetch' + * @param context + * @param taskModuleRequest + */ + protected async handleTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { + throw new Error('NotImplemented'); + } + + /** + * Receives invoke activities with Activity name of 'task/submit' + * @param context + * @param taskModuleRequest + */ + protected async handleTeamsTaskModuleSubmit(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { + throw new Error('NotImplemented'); + } + + /** + * Receives invoke activities with Activity name of 'composeExtension/queryLink' + * @remarks + * Used in creating a Search-based Message Extension. + * @param context + * @param query + */ + protected async handleTeamsAppBasedLinkQuery(context: TurnContext, query: AppBasedLinkQuery): Promise { + throw new Error('NotImplemented'); + } + + /** + * Receives invoke activities with the name 'composeExtension/query'. + * @remarks + * Used in creating a Search-based Message Extension. + * @param context + * @param action + */ + protected async handleTeamsMessagingExtensionQuery(context: TurnContext, query: MessagingExtensionQuery): Promise { + throw new Error('NotImplemented'); + } + + /** + * Receives invoke activities with the name 'composeExtension/selectItem'. + * @remarks + * Used in creating a Search-based Message Extension. + * @param context + * @param action + */ + protected async handleTeamsMessagingExtensionSelectItem(context: TurnContext, query: any): Promise { + throw new Error('NotImplemented'); + } + + /** + * Receives invoke activities with the name 'composeExtension/submitAction' and dispatches to botMessagePreview-flows as applicable. + * @remarks + * A handler registered through this method does not dispatch to the next handler (either `handleTeamsMessagingExtensionSubmitAction`, `handleTeamsMessagingExtensionBotMessagePreviewEdit`, or `handleTeamsMessagingExtensionBotMessagePreviewSend`). + * This method exists for developers to optionally add more logic before the TeamsActivityHandler routes the activity to one of the + * previously mentioned handlers. + * @param context + * @param action + */ + protected async handleTeamsMessagingExtensionSubmitActionDispatch(context: TurnContext, action: MessagingExtensionAction): Promise { + if (action.botMessagePreviewAction) { + switch (action.botMessagePreviewAction) { + case 'edit': + return await this.handleTeamsMessagingExtensionBotMessagePreviewEdit(context, action); + case 'send': + return await this.handleTeamsMessagingExtensionBotMessagePreviewSend(context, action); + default: + throw new Error('BadRequest'); + } + } else { + return await this.handleTeamsMessagingExtensionSubmitAction(context, action); + } + } + + /** + * Receives invoke activities with the name 'composeExtension/submitAction'. + * @remarks + * This invoke activity is received when a user + * @param context + * @param action + */ + protected async handleTeamsMessagingExtensionSubmitAction(context: TurnContext, action: MessagingExtensionAction): Promise { + throw new Error('NotImplemented'); + } + + /** + * Receives invoke activities with the name 'composeExtension/submitAction' with the 'botMessagePreview' property present on activity.value. + * The value for 'botMessagePreview' is 'edit'. + * @remarks + * This invoke activity is received when a user + * @param context + * @param action + */ + protected async handleTeamsMessagingExtensionBotMessagePreviewEdit(context: TurnContext, action: MessagingExtensionAction): Promise { + throw new Error('NotImplemented'); + } + + /** + * Receives invoke activities with the name 'composeExtension/submitAction' with the 'botMessagePreview' property present on activity.value. + * The value for 'botMessagePreview' is 'send'. + * @remarks + * This invoke activity is received when a user + * @param context + * @param action + */ + protected async handleTeamsMessagingExtensionBotMessagePreviewSend(context: TurnContext, action: MessagingExtensionAction): Promise { + throw new Error('NotImplemented'); + } + + /** + * Receives invoke activities with the name 'composeExtension/fetchTask' + * @param context + * @param action + */ + protected async handleTeamsMessagingExtensionFetchTask(context: TurnContext, action: MessagingExtensionAction): Promise { + throw new Error('NotImplemented'); + } + + /** + * Receives invoke activities with the name 'composeExtension/querySettingUrl' + * @param context + * @param query + */ + protected async handleTeamsMessagingExtensionConfigurationQuerySettingUrl(context: TurnContext, query: MessagingExtensionQuery): Promise { + throw new Error('NotImplemented'); + } + + /** + * Receives invoke activities with the name 'composeExtension/setting' + * @param context + * @param query + */ + protected handleTeamsMessagingExtensionConfigurationSetting(context: TurnContext, settings: any): Promise { + throw new Error('NotImplemented'); + } + + /** + * Override this method to change the dispatching of ConversationUpdate activities. + * @remarks + * + * @param context + */ + protected async dispatchConversationUpdateActivity(context: TurnContext): Promise { + await this.handle(context, 'ConversationUpdate', async () => { + + if (context.activity.channelId == "msteams") + { + const channelData = context.activity.channelData as TeamsChannelData; + + if (context.activity.membersAdded && context.activity.membersAdded.length > 0) { + return await this.onTeamsMembersAdded(context); + } + + if (context.activity.membersRemoved && context.activity.membersRemoved.length > 0) { + return await this.onTeamsMembersRemoved(context); + } + + if (!channelData || !channelData.eventType) { + return await super.dispatchConversationUpdateActivity(context); + } + + switch (channelData.eventType) + { + case 'channelCreated': + return await this.onTeamsChannelCreated(context); + + case 'channelDeleted': + return await this.onTeamsChannelDeleted(context); + + case 'channelRenamed': + return await this.onTeamsChannelRenamed(context); + + case 'teamRenamed': + return await this.onTeamsTeamRenamed(context); + + default: + return await super.dispatchConversationUpdateActivity(context); + } + } else { + return await super.dispatchConversationUpdateActivity(context); + } + }); + } + + /** + * Called in `dispatchConversationUpdateActivity()` to trigger the `'TeamsMembersAdded'` handlers. + * @remarks + * If no handlers are registered for the `'TeamsMembersAdded'` event, the `'MembersAdded'` handlers will run instead. + * @param context + */ + protected async onTeamsMembersAdded(context: TurnContext): Promise { + if ('TeamsMembersAdded' in this.handlers && this.handlers['TeamsMembersAdded'].length > 0) { + + let teamsChannelAccountLookup = null; + + for (let i=0; i teamsChannelAccountLookup[teamChannelAccount.id] = teamChannelAccount); + } + + // if we have the TeamsChannelAccount in our lookup table then overwrite the ChannelAccount with it + const teamsChannelAccount = teamsChannelAccountLookup[channelAccount.id]; + if (teamsChannelAccount !== undefined) { + context.activity.membersAdded[i] = teamsChannelAccount; + } + } + + await this.handle(context, 'TeamsMembersAdded', this.defaultNextEvent(context)); + } else { + await this.handle(context, 'MembersAdded', this.defaultNextEvent(context)); + } + } + + /** + * Called in `dispatchConversationUpdateActivity()` to trigger the `'TeamsMembersRemoved'` handlers. + * @remarks + * If no handlers are registered for the `'TeamsMembersRemoved'` event, the `'MembersRemoved'` handlers will run instead. + * @param context + */ + protected async onTeamsMembersRemoved(context: TurnContext): Promise { + if ('TeamsMembersRemoved' in this.handlers && this.handlers['TeamsMembersRemoved'].length > 0) { + await this.handle(context, 'TeamsMembersRemoved', this.defaultNextEvent(context)); + } else { + await this.handle(context, 'MembersRemoved', this.defaultNextEvent(context)); + } + } + + /** + * + * @param context + */ + protected async onTeamsChannelCreated(context): Promise { + await this.handle(context, 'TeamsChannelCreated', this.defaultNextEvent(context)); + } + + /** + * + * @param context + */ + protected async onTeamsChannelDeleted(context): Promise { + await this.handle(context, 'TeamsChannelDeleted', this.defaultNextEvent(context)); + } + + /** + * + * @param context + */ + protected async onTeamsChannelRenamed(context): Promise { + await this.handle(context, 'TeamsChannelRenamed', this.defaultNextEvent(context)); + } + + /** + * + * @param context + */ + protected async onTeamsTeamRenamed(context): Promise { + await this.handle(context, 'TeamsTeamRenamed', this.defaultNextEvent(context)); + } + + /** + * + * @param handler + */ + public onTeamsMembersAddedEvent(handler: (membersAdded: TeamsChannelAccount[], teamInfo: TeamInfo, context: TurnContext, next: () => Promise) => Promise): this { + return this.on('TeamsMembersAdded', async (context, next) => { + const teamsChannelData = context.activity.channelData as TeamsChannelData; + await handler(context.activity.membersAdded, teamsChannelData.team, context, next); + }); + } + + /** + * + * @param handler + */ + public onTeamsMembersRemovedEvent(handler: (membersRemoved: TeamsChannelAccount[], teamInfo: TeamInfo, context: TurnContext, next: () => Promise) => Promise): this { + return this.on('TeamsMembersRemoved', async (context, next) => { + const teamsChannelData = context.activity.channelData as TeamsChannelData; + await handler(context.activity.membersRemoved, teamsChannelData.team, context, next); + }); + } + + /** + * + * @param handler + */ + public onTeamsChannelCreatedEvent(handler: (channelInfo: ChannelInfo, teamInfo: TeamInfo, context: TurnContext, next: () => Promise) => Promise): this { + return this.on('TeamsChannelCreated', async (context, next) => { + const teamsChannelData = context.activity.channelData as TeamsChannelData; + await handler(teamsChannelData.channel, teamsChannelData.team, context, next); + }); + } + + /** + * + * @param handler + */ + public onTeamsChannelDeletedEvent(handler: (channelInfo: ChannelInfo, teamInfo: TeamInfo, context: TurnContext, next: () => Promise) => Promise): this { + return this.on('TeamsChannelDeleted', async (context, next) => { + const teamsChannelData = context.activity.channelData as TeamsChannelData; + await handler(teamsChannelData.channel, teamsChannelData.team, context, next); + }); + } + + /** + * + * @param handler + */ + public onTeamsChannelRenamedEvent(handler: (channelInfo: ChannelInfo, teamInfo: TeamInfo, context: TurnContext, next: () => Promise) => Promise): this { + return this.on('TeamsChannelRenamed', async (context, next) => { + const teamsChannelData = context.activity.channelData as TeamsChannelData; + await handler(teamsChannelData.channel, teamsChannelData.team, context, next); + }); + } + + /** + * + * @param handler + */ + public onTeamsTeamRenamedEvent(handler: (teamInfo: TeamInfo, context: TurnContext, next: () => Promise) => Promise): this { + return this.on('TeamsTeamRenamed', async (context, next) => { + const teamsChannelData = context.activity.channelData as TeamsChannelData; + await handler(teamsChannelData.team, context, next); + }); + } + + private static createInvokeResponse(body?: any): InvokeResponse { + return { status: 200, body }; + } +} \ No newline at end of file diff --git a/libraries/botbuilder/src/teamsActivityHelpers.ts b/libraries/botbuilder/src/teamsActivityHelpers.ts new file mode 100644 index 0000000000..1d94aadc03 --- /dev/null +++ b/libraries/botbuilder/src/teamsActivityHelpers.ts @@ -0,0 +1,51 @@ +/** + * @module botbuilder + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + Activity, + ChannelInfo, + NotificationInfo, + TeamInfo, + TeamsChannelData +} from 'botbuilder-core'; + +/** + * Activity helper methods for Teams. + */ +export function teamsGetChannelId(activity: Activity): string { + if (!activity) { + throw new Error('Missing activity parameter'); + } + + const channelData: TeamsChannelData = activity.channelData as TeamsChannelData; + const channel: ChannelInfo = channelData ? channelData.channel : null; + return channel && channel.id ? channel.id : null; +} + +export function teamsGetTeamId(activity: Activity): string { + if (!activity) { + throw new Error('Missing activity parameter'); + } + + const channelData: TeamsChannelData = activity.channelData as TeamsChannelData; + const team: TeamInfo = channelData ? channelData.team : null; + return team && team.id ? team.id : null; +} + +export function teamsNotifyUser(activity: Activity): void { + if (!activity) { + throw new Error('Missing activity parameter'); + } + + if (!activity.channelData || typeof activity.channelData !== 'object') { + activity.channelData = {}; + } + + const channelData: TeamsChannelData = activity.channelData as TeamsChannelData; + channelData.notification = { alert: true } as NotificationInfo; +} diff --git a/libraries/botbuilder/src/teamsInfo.ts b/libraries/botbuilder/src/teamsInfo.ts new file mode 100644 index 0000000000..f5d869c0f3 --- /dev/null +++ b/libraries/botbuilder/src/teamsInfo.ts @@ -0,0 +1,101 @@ +/** + * @module botbuilder + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + ChannelAccount, + ChannelInfo, + ConversationList, + TeamsChannelAccount, + TeamsChannelData, + TeamDetails, + TurnContext +} from 'botbuilder-core'; +import { ConnectorClient, TeamsConnectorClient } from 'botframework-connector'; + +import { BotFrameworkAdapter } from './botFrameworkAdapter'; + +export class TeamsInfo { + public static async getTeamDetails(context: TurnContext, teamId?: string): Promise { + const t = teamId || this.getTeamId(context); + if (!t) { + throw new Error('This method is only valid within the scope of a MS Teams Team.'); + } + + return await this.getTeamsConnectorClient(context).teams.fetchTeamDetails(t); + } + + public static async getTeamChannels(context: TurnContext, teamId?: string): Promise { + const t = teamId || this.getTeamId(context); + if (!t) { + throw new Error('This method is only valid within the scope of a MS Teams Team.'); + } + + const channelList: ConversationList = await this.getTeamsConnectorClient(context).teams.fetchChannelList(t); + return channelList.conversations; + } + + public static async getMembers(context: TurnContext): Promise { + const teamId = this.getTeamId(context); + if (teamId) { + return await this.getTeamMembers(context, teamId); + } else { + const conversation = context.activity.conversation; + const conversationId = conversation && conversation.id ? conversation.id : undefined; + return await this.getMembersInternal(this.getConnectorClient(context), conversationId); + } + } + + public static async getTeamMembers(context: TurnContext, teamId?: string): Promise { + const t = teamId || this.getTeamId(context); + if (!t) { + throw new Error('This method is only valid within the scope of a MS Teams Team.'); + } + return this.getMembersInternal(this.getConnectorClient(context), t); + } + + private static async getMembersInternal(connectorClient: ConnectorClient, conversationId: string): Promise { + if (!conversationId) { + throw new Error('The getMembers operation needs a valid conversationId.'); + } + + const teamMembers: ChannelAccount[] = await connectorClient.conversations.getConversationMembers(conversationId); + teamMembers.forEach((member): void => { + member.aadObjectId = (member as any).objectId; + }); + + return teamMembers as TeamsChannelAccount[]; + } + + private static getTeamId(context: TurnContext): string { + if (!context) { + throw new Error('Missing context parameter'); + } + + if (!context.activity) { + throw new Error('Missing activity on context'); + } + + const channelData = context.activity.channelData as TeamsChannelData; + const team = channelData && channelData.team ? channelData.team : undefined; + const teamId = team && typeof(team.id) === 'string' ? team.id : undefined; + return teamId; + } + + private static getConnectorClient(context: TurnContext): ConnectorClient { + if (!context.adapter || !('createConnectorClient' in context.adapter)) { + throw new Error('This method requires a connector client.'); + } + + return (context.adapter as BotFrameworkAdapter).createConnectorClient(context.activity.serviceUrl); + } + + private static getTeamsConnectorClient(context: TurnContext): TeamsConnectorClient { + const connectorClient = this.getConnectorClient(context); + return new TeamsConnectorClient(connectorClient.credentials, { baseUri: context.activity.serviceUrl }); + } +} diff --git a/libraries/botbuilder/tests/botFrameworkAdapter.test.js b/libraries/botbuilder/tests/botFrameworkAdapter.test.js index 82f86af482..f951dda9da 100644 --- a/libraries/botbuilder/tests/botFrameworkAdapter.test.js +++ b/libraries/botbuilder/tests/botFrameworkAdapter.test.js @@ -2,7 +2,6 @@ const assert = require('assert'); const { TurnContext } = require('botbuilder-core'); const connector = require('botframework-connector'); const { BotFrameworkAdapter } = require('../'); -const os = require('os'); const reference = { activityId: '1234', @@ -406,6 +405,23 @@ describe(`BotFrameworkAdapter`, function () { }); }); + it(`should createConversation() for Teams.`, function (done) { + const adapter = new AdapterUnderTest(); + adapter.createConversation(reference, (context) => { + try{ + assert(context, `context not passed.`); + assert(context.activity, `context has no request.`); + assert(context.activity.conversation, `request has invalid conversation.`); + assert.strictEqual(context.activity.conversation.id, 'convo2', `request has invalid conversation.id.`); + assert.strictEqual(context.activity.conversation.tenantId, reference.conversation.tenantId, `request has invalid tenantId on conversation.`); + assert.strictEqual(context.activity.channelData.tenant.id, reference.conversation.tenantId, `request has invalid tenantId in channelData.`); + done(); + } catch(err) { + done(err); + } + }); + }); + it(`should deliver a single activity using sendActivities().`, function (done) { const adapter = new AdapterUnderTest(); const context = new TurnContext(adapter, incomingMessage); diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/.env b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/.env new file mode 100644 index 0000000000..a695b3bf05 --- /dev/null +++ b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/.env @@ -0,0 +1,2 @@ +MicrosoftAppId= +MicrosoftAppPassword= diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/package.json b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/package.json new file mode 100644 index 0000000000..6704a4a804 --- /dev/null +++ b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/package.json @@ -0,0 +1,32 @@ +{ + "name": "action-based-messaging-extension-fetchtask", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "build": "tsc --build", + "lint": "tslint -c tslint.json 'src/**/*.ts'", + "start": "tsc --build && node ./lib/index.js", + "watch": "nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "botbuilder": "file:../../../", + "dotenv": "^8.1.0", + "node-fetch": "^2.6.0", + "restify": "^8.4.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^12.7.1", + "@types/node-fetch": "^2.5.0", + "@types/request": "^2.48.1", + "@types/restify": "^7.2.7", + "nodemon": "^1.19.1", + "ts-node": "^7.0.1", + "tslint": "~5.18.0", + "typescript": "^3.2.4" + } +} diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/src/actionBasedMessagingExtensionFetchBot.ts b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/src/actionBasedMessagingExtensionFetchBot.ts new file mode 100644 index 0000000000..3300553ebf --- /dev/null +++ b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/src/actionBasedMessagingExtensionFetchBot.ts @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Activity, + Attachment, + BotFrameworkAdapter, + ChannelInfo, + ConversationParameters, + ConversationReference, + ConversationResourceResponse, + MessageFactory, + MessagingExtensionAction, + MessagingExtensionActionResponse, + TeamsChannelData, + TeamDetails, + TeamsActivityHandler, + TeamsInfo, + TurnContext +} from 'botbuilder'; + +import { AdaptiveCardHelper } from './adaptiveCardHelper'; +import { CardResponseHelpers } from './cardResponseHelpers'; +import { SubmitExampleData } from './submitExampleData'; + +export class ActionBasedMessagingExtensionFetchTaskBot extends TeamsActivityHandler { + /* + * After installing this bot you will need to click on the 3 dots to pull up the extension menu to select the bot. Once you do you do + * see the extension pop a task module. + */ + constructor() { + super(); + + // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + this.onMessage(async (context, next) => { + await context.sendActivity(`You said '${context.activity.text}'`); + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } + + protected async handleTeamsMessagingExtensionFetchTask(context: TurnContext, action: MessagingExtensionAction): Promise { + const response = AdaptiveCardHelper.createTaskModuleAdaptiveCardResponse(); + return response; + } + + protected async handleTeamsMessagingExtensionSubmitAction(context: TurnContext, action: MessagingExtensionAction): Promise { + const submittedData = action.data as SubmitExampleData; + const adaptiveCard = AdaptiveCardHelper.toAdaptiveCardAttachment(submittedData); + const response = CardResponseHelpers.toMessagingExtensionBotMessagePreviewResponse(adaptiveCard); + return response; + } + + protected async handleTeamsMessagingExtensionBotMessagePreviewEdit(context: TurnContext, action: MessagingExtensionAction): Promise { + const submitData = AdaptiveCardHelper.toSubmitExampleData(action); + const response = AdaptiveCardHelper.createTaskModuleAdaptiveCardResponse( + submitData.Question, + (submitData.MultiSelect.toLowerCase() === 'true'), + submitData.Option1, + submitData.Option2, + submitData.Option3); + return response; + } + + protected async handleTeamsMessagingExtensionBotMessagePreviewSend(context: TurnContext, action: MessagingExtensionAction): Promise { + const submitData: SubmitExampleData = AdaptiveCardHelper.toSubmitExampleData(action); + const adaptiveCard: Attachment = AdaptiveCardHelper.toAdaptiveCardAttachment(submitData); + + const responseActivity = {type: 'message', attachments: [adaptiveCard] } as Activity; + + try { + // Send to channel where messaging extension invoked. + let results = await this.teamsCreateConversation(context, context.activity.channelData.channel.id, responseActivity); + + // Send card to "General" channel. + const teamDetails: TeamDetails = await TeamsInfo.getTeamDetails(context); + results = await this.teamsCreateConversation(context, teamDetails.id, responseActivity); + } catch { + console.error('In group chat or personal scope.'); + } + + // Send card to compose box for the current user. + const response = CardResponseHelpers.toComposeExtensionResultResponse(adaptiveCard); + return response; + } + + protected async handleTeamsMessagingExtensionCardButtonClicked(context: TurnContext, obj) { + const reply = MessageFactory.text('handleTeamsMessagingExtensionCardButtonClicked Value: ' + JSON.stringify(context.activity.value)); + await context.sendActivity(reply); + } + + private async teamsCreateConversation(context: TurnContext, teamsChannelId: string, message: Partial): Promise<[ConversationReference, string]> { + if (!teamsChannelId) { + throw new Error('Missing valid teamsChannelId argument'); + } + + if (!message) { + throw new Error('Missing valid message argument'); + } + + const conversationParameters = { + isGroup: true, + channelData: { + channel: { + id: teamsChannelId + } + }, + + activity: message, + }; + + const adapter = context.adapter; + const connectorClient = adapter.createConnectorClient(context.activity.serviceUrl); + + // This call does NOT send the outbound Activity is not being sent through the middleware stack. + const conversationResourceResponse: ConversationResourceResponse = await connectorClient.conversations.createConversation(conversationParameters); + const conversationReference = TurnContext.getConversationReference(context.activity); + conversationReference.conversation.id = conversationResourceResponse.id; + return [conversationReference, conversationResourceResponse.activityId]; + } +} diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/src/adaptiveCardHelper.ts b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/src/adaptiveCardHelper.ts new file mode 100644 index 0000000000..4250e209bb --- /dev/null +++ b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/src/adaptiveCardHelper.ts @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Attachment, + CardFactory, + MessagingExtensionAction, + MessagingExtensionActionResponse, + TaskModuleContinueResponse, + TaskModuleTaskInfo +} from 'botbuilder'; + +import { SubmitExampleData } from './submitExampleData'; + +export class AdaptiveCardHelper { + public static toSubmitExampleData(action: MessagingExtensionAction): SubmitExampleData { + const activityPreview = action.botActivityPreview[0]; + const attachmentContent = activityPreview.attachments[0].content; + + const userText = attachmentContent.body[1].text as string; + const choiceSet = attachmentContent.body[3]; + + return { + MultiSelect: choiceSet.isMultiSelect ? 'true' : 'false', + Option1: choiceSet.choices[0].title, + Option2: choiceSet.choices[1].title, + Option3: choiceSet.choices[2].title, + Question: userText + } as SubmitExampleData; + } + + public static createTaskModuleAdaptiveCardResponse( + userText: string = null, + isMultiSelect: boolean = true, + option1: string = null, + option2: string = null, + option3: string = null): MessagingExtensionActionResponse { + + const responseCard = CardFactory.adaptiveCard({ + actions: [ + { + data: { + submitLocation: 'messagingExtensionFetchTask' + }, + title: 'Submit', + type: 'Action.Submit' + } + ], + body: [ + { + text: 'This is an Adaptive Card within a Task Module', + type: 'TextBlock', + weight: 'bolder' + }, + { type: 'TextBlock', text: 'Enter text for Question:' }, + { + id: 'Question', + placeholder: 'Question text here', + type: 'Input.Text', + value: userText + }, + { type: 'TextBlock', text: 'Options for Question:' }, + { type: 'TextBlock', text: 'Is Multi-Select:' }, + { + choices: [{title: 'True', value: 'true'}, {title: 'False', value: 'false'}], + id: 'MultiSelect', + isMultiSelect: false, + style: 'expanded', + type: 'Input.ChoiceSet', + value: isMultiSelect ? 'true' : 'false' + }, + { + id: 'Option1', + placeholder: 'Option 1 here', + type: 'Input.Text', + value: option1 + }, + { + id: 'Option2', + placeholder: 'Option 2 here', + type: 'Input.Text', + value: option2 + }, + { + id: 'Option3', + placeholder: 'Option 3 here', + type: 'Input.Text', + value: option3 + } + ], + type: 'AdaptiveCard', + version : '1.0' + }); + + return { + task: { + type: 'continue', + value: { + card: responseCard as Attachment, + height: 450, + title: 'Task Module Fetch Example', + url: null, + width: 500 + } as TaskModuleTaskInfo + } as TaskModuleContinueResponse + } as MessagingExtensionActionResponse; + } + + public static toAdaptiveCardAttachment(data: SubmitExampleData): Attachment { + return CardFactory.adaptiveCard({ + actions: [ + { type: 'Action.Submit', title: 'Submit', data: { submitLocation: 'messagingExtensionSubmit'} } + ], + body: [ + { text: 'Adaptive Card from Task Module', type: 'TextBlock', weight: 'bolder' }, + { text: `${ data.Question }`, type: 'TextBlock', id: 'Question' }, + { id: 'Answer', placeholder: 'Answer here...', type: 'Input.Text' }, + { + choices: [ + {title: data.Option1, value: data.Option1}, + {title: data.Option2, value: data.Option2}, + {title: data.Option3, value: data.Option3} + ], + id: 'Choices', + isMultiSelect: Boolean(data.MultiSelect), + style: 'expanded', + type: 'Input.ChoiceSet' + } + ], + type: 'AdaptiveCard', + version: '1.0' + }); + } +} diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/src/cardResponseHelpers.ts b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/src/cardResponseHelpers.ts new file mode 100644 index 0000000000..cb138c3c59 --- /dev/null +++ b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/src/cardResponseHelpers.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Activity, + Attachment, + InputHints, + MessageFactory, + MessagingExtensionActionResponse, + MessagingExtensionAttachment, + MessagingExtensionResult, + TaskModuleContinueResponse, + TaskModuleTaskInfo +} from 'botbuilder'; + +export class CardResponseHelpers { + public static toTaskModuleResponse(cardAttachment: Attachment): MessagingExtensionActionResponse { + return { + task: { + height: 450, + title: 'Task Module Fetch Example', + value: { + card: cardAttachment + } as TaskModuleTaskInfo, + width: 500 + } as TaskModuleContinueResponse + } as MessagingExtensionActionResponse; + } + + public static toComposeExtensionResultResponse(cardAttachment: Attachment): MessagingExtensionActionResponse { + + return { + composeExtension: { + attachmentLayout: 'list', + attachments: [cardAttachment as MessagingExtensionAttachment], + type: 'result' + + } as MessagingExtensionResult + } as MessagingExtensionActionResponse; + } + + public static toMessagingExtensionBotMessagePreviewResponse(cardAttachment: Attachment): MessagingExtensionActionResponse { + return { + composeExtension: { + activityPreview: MessageFactory.attachment(cardAttachment, null, null, InputHints.ExpectingInput) as Activity, + type: 'botMessagePreview' + } as MessagingExtensionResult + } as MessagingExtensionActionResponse; + } +} diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/src/index.ts b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/src/index.ts new file mode 100644 index 0000000000..248ff6cf7f --- /dev/null +++ b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/src/index.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { BotFrameworkAdapter } from 'botbuilder'; + +import { ActionBasedMessagingExtensionFetchTaskBot } from './actionBasedMessagingExtensionFetchBot'; + +// Note: Ensure you have a .env file and include MicrosoftAppId and MicrosoftAppPassword. +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error('[onTurnError]:'); + console.error(error); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`); +}; + +// Create the bot. +const myBot = new ActionBasedMessagingExtensionFetchTaskBot(); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${ server.name } listening to ${ server.url }`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); +}); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route to bot + await myBot.run(context); + }); +}); diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/src/submitExampleData.ts b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/src/submitExampleData.ts new file mode 100644 index 0000000000..ed33fe6aa9 --- /dev/null +++ b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/src/submitExampleData.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export class SubmitExampleData { + public MultiSelect: string; + public Option1: string; + public Option2: string; + public Option3: string; + public Question: string; + public submitLocation: string; +} diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/teams-app-manifest/icon-color.png b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/teams-app-manifest/icon-color.png new file mode 100644 index 0000000000..bd9928dfc8 Binary files /dev/null and b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/teams-app-manifest/icon-color.png differ diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/teams-app-manifest/icon-outline.png b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/teams-app-manifest/icon-outline.png new file mode 100644 index 0000000000..45e7a7c56e Binary files /dev/null and b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/teams-app-manifest/icon-outline.png differ diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/teams-app-manifest/manifest.json b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/teams-app-manifest/manifest.json new file mode 100644 index 0000000000..061c54c9d1 --- /dev/null +++ b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/teams-app-manifest/manifest.json @@ -0,0 +1,70 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.3/MicrosoftTeams.schema.json", + "manifestVersion": "1.5", + "version": "1.0", + "id": "00000000-0000-0000-0000-000000000000", + "packageName": "com.teams.sample.fetchtask.messagingextension", + "developer": { + "name": "Microsoft", + "websiteUrl": "https://www.microsoft.com", + "privacyUrl": "https://www.teams.com/privacy", + "termsOfUseUrl": "https://www.teams.com/termsofuser" + }, + "icons": { + "color": "icon-color.png", + "outline": "icon-outline.png" + }, + "name": { + "short": "Fetch Task Ext", + "full": "Fetch Task Messaging Extension" + }, + "description": { + "short": "Demonstrates a Fetch Task Messaging Extension", + "full": "Demonstrates a Fetch Task Messaging Extension" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "scopes": [ + "groupchat", + "team", + "personal" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "composeExtensions": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "canUpdateConfiguration": false, + "commands": [ + { + "id": "createWithPreview", + "type": "action", + "title": "Create Survey Card", + "description": "Example of creating a Survey Card", + "initialRun": false, + "fetchTask": true, + "context": [ + "commandBox", + "compose", + "message" + ], + "parameters": [ + { + "name": "param", + "title": "param", + "description": "" + } + ] + } + ] + } +], +"permissions": [ + "identity", + "messageTeamMembers" +], +"validDomains": []} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/tsconfig.json b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/tsconfig.json new file mode 100644 index 0000000000..a168d60662 --- /dev/null +++ b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "composite": true, + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + } +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/tslint.json b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/tslint.json new file mode 100644 index 0000000000..ad00715f85 --- /dev/null +++ b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension-fetchTask/tslint.json @@ -0,0 +1,18 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "interface-name" : [true, "never-prefix"], + "max-line-length": [false], + "no-console": [false, "log", "error"], + "no-var-requires": false, + "quotemark": [true, "single"], + "one-variable-per-declaration": false, + "curly": [true, "ignore-same-line"], + "trailing-comma": [true, {"multiline": "never", "singleline": "never"}] + }, + "rulesDirectory": [] +} diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/.env b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/.env new file mode 100644 index 0000000000..660828e3e8 --- /dev/null +++ b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/.env @@ -0,0 +1,2 @@ +MicrosoftAppId= +MicrosoftAppPassword= \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/package.json b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/package.json new file mode 100644 index 0000000000..c51f0d7456 --- /dev/null +++ b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/package.json @@ -0,0 +1,32 @@ +{ + "name": "messaging-extension-action", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "build": "tsc --build", + "lint": "tslint -c tslint.json 'src/**/*.ts'", + "start": "tsc --build && node ./lib/index.js", + "watch": "nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "botbuilder": "file:../../../", + "dotenv": "^8.1.0", + "node-fetch": "^2.6.0", + "restify": "^8.4.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^12.7.1", + "@types/node-fetch": "^2.5.0", + "@types/request": "^2.48.1", + "@types/restify": "^7.2.7", + "nodemon": "^1.19.1", + "ts-node": "^7.0.1", + "tslint": "~5.18.0", + "typescript": "^3.2.4" + } +} diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/src/actionBasedMessagingExtensionBot.ts b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/src/actionBasedMessagingExtensionBot.ts new file mode 100644 index 0000000000..27aea9309d --- /dev/null +++ b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/src/actionBasedMessagingExtensionBot.ts @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Activity, + Attachment, + CardFactory, + MessagingExtensionAction, + MessagingExtensionActionResponse, + TaskModuleContinueResponse, + TaskModuleMessageResponse, + TaskModuleResponseBase, + TeamsActivityHandler, + TurnContext +} from 'botbuilder'; + +export class ActionBasedMessagingExtensionBot extends TeamsActivityHandler { + constructor() { + super(); + + // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + this.onMessage(async (context, next) => { + await context.sendActivity(`You said '${context.activity.text}'`); + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + + this.onMembersAdded(async (context, next) => { + const membersAdded = context.activity.membersAdded; + for (const member of membersAdded) { + if (member.id !== context.activity.recipient.id) { + await context.sendActivity('Hello and welcome!'); + } + } + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } + + protected async handleTeamsMessagingExtensionSubmitAction(context, action: MessagingExtensionAction): Promise { + const data = action.data; + let body: MessagingExtensionActionResponse; + if (data && data.done) { + // The commandContext check doesn't need to be used in this scenario as the manifest specifies the shareMessage command only works in the "message" context. + const sharedMessage = (action.commandId === 'shareMessage' && action.commandContext === 'message') + ? `Shared message:
${JSON.stringify(action.messagePayload)}

` + : ''; + const preview = CardFactory.thumbnailCard('Created Card', `Your input: ${data.userText}`); + const heroCard = CardFactory.heroCard('Created Card', `${sharedMessage}Your input:
${data.userText}
`); + body = { + composeExtension: { + attachmentLayout: 'list', + attachments: [ + { ...heroCard, preview } + ], + type: 'result' + } + }; + } else if (action.commandId === 'createWithPreview') { + // The commandId is definied in the manifest of the Teams Application + const activityPreview = { + attachments: [ + this.taskModuleResponseCard(action) + ] + } as Activity; + + body = { + composeExtension: { + activityPreview, + type: 'botMessagePreview' + } + }; + } else { + body = { + task: this.taskModuleResponse(action, false) + }; + } + + return body; + } + + // This method fires when an user uses an Action-based Messaging Extension from the Teams Client. + // It should send back the tab or task module for the user to interact with. + protected async handleTeamsMessagingExtensionFetchTask(context: TurnContext, action: MessagingExtensionAction): Promise { + return { + task: this.taskModuleResponse(action, false) + }; + } + + protected async handleTeamsMessagingExtensionBotMessagePreviewSend(context: TurnContext, action: MessagingExtensionAction): Promise { + let body: MessagingExtensionActionResponse; + const card = this.getCardFromPreviewMessage(action); + if (!card) { + body = { + task: { + type: 'message', + value: 'Missing user edit card. Something wrong on Teams client.' + } as TaskModuleMessageResponse + }; + } else { + body = undefined; + await context.sendActivity({ attachments: [card] }); + } + + return body; + } + + protected async handleTeamsMessagingExtensionBotMessagePreviewEdit(context, value: MessagingExtensionAction): Promise { + const card = this.getCardFromPreviewMessage(value); + let body: MessagingExtensionActionResponse; + if (!card) { + body = { + task: { + type: 'message', + value: 'Missing user edit card. Something wrong on Teams client.' + } as TaskModuleMessageResponse + }; + } else { + body = { + task: { + type: 'continue', + value: { card } + } as TaskModuleContinueResponse + }; + } + + return body; + } + + private getCardFromPreviewMessage(query: MessagingExtensionAction): Attachment { + const userEditActivities = query.botActivityPreview; + return userEditActivities + && userEditActivities[0] + && userEditActivities[0].attachments + && userEditActivities[0].attachments[0]; + } + + private taskModuleResponse(query: any, done: boolean): TaskModuleResponseBase { + if (done) { + return { + type: 'message', + value: 'Thanks for your inputs!' + } as TaskModuleMessageResponse; + } else { + return { + type: 'continue', + value: { + card: this.taskModuleResponseCard(query, (query.data && query.data.userText) || undefined), + title: 'More Page' + } + } as TaskModuleContinueResponse; + } + } + + private taskModuleResponseCard(data: any, textValue?: string) { + return CardFactory.adaptiveCard({ + actions: [ + { + data: { + done: false + }, + title: 'Next', + type: 'Action.Submit' + }, + { + data: { + done: true + }, + title: 'Submit', + type: 'Action.Submit' + } + ], + body: [ + { + size: 'large', + text: `Your request:`, + type: 'TextBlock', + weight: 'bolder' + }, + { + items: [ + { + text: JSON.stringify(data), + type: 'TextBlock', + wrap: true + } + ], + style: 'emphasis', + type: 'Container' + }, + { + id: 'userText', + placeholder: 'Type text here...', + type: 'Input.Text', + value: textValue + } + ], + type: 'AdaptiveCard', + version: '1.0.0' + }); + } +} diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/src/index.ts b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/src/index.ts new file mode 100644 index 0000000000..4038a65435 --- /dev/null +++ b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/src/index.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { BotFrameworkAdapter } from 'botbuilder'; + +import { ActionBasedMessagingExtensionBot } from './actionBasedMessagingExtensionBot'; + +// Note: Ensure you have a .env file and include MicrosoftAppId and MicrosoftAppPassword. +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error('[onTurnError]:'); + console.error(error); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`); +}; + +// Create the bot. +const myBot = new ActionBasedMessagingExtensionBot(); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${ server.name } listening to ${ server.url }`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); +}); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route to bot + await myBot.run(context); + }); +}); diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/teams-app-manifest/icon-color.png b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/teams-app-manifest/icon-color.png new file mode 100644 index 0000000000..bd9928dfc8 Binary files /dev/null and b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/teams-app-manifest/icon-color.png differ diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/teams-app-manifest/icon-outline.png b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/teams-app-manifest/icon-outline.png new file mode 100644 index 0000000000..45e7a7c56e Binary files /dev/null and b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/teams-app-manifest/icon-outline.png differ diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/teams-app-manifest/manifest.json b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/teams-app-manifest/manifest.json new file mode 100644 index 0000000000..afb9d34308 --- /dev/null +++ b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/teams-app-manifest/manifest.json @@ -0,0 +1,101 @@ +{ + "$schema": "https://github.com/OfficeDev/microsoft-teams-app-schema/blob/preview/DevPreview/MicrosoftTeams.schema.json", + "manifestVersion": "devPreview", + "version": "1.0", + "id": "00000000-0000-0000-0000-000000000000", + "packageName": "com.microsoft.teams.samples.v4bot", + "developer": { + "name": "Microsoft Corp", + "websiteUrl": "https://example.azurewebsites.net", + "privacyUrl": "https://example.azurewebsites.net/privacy", + "termsOfUseUrl": "https://example.azurewebsites.net/termsofuse" + }, + "name": { + "short": "messaging-extension-action", + "full": "Microsoft Teams V4 Action Messaging Extension Bot - C#" + }, + "description": { + "short": "Microsoft Teams V4 Action Messaging Extension Bot - C#", + "full": "Sample Action Messaging Extension Bot using V4 Bot Builder SDK and V4 Microsoft Teams Extension SDK" + }, + "icons": { + "outline": "icon-outline.png", + "color": "icon-color.png" + }, + "accentColor": "#0080FF", + "bots": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "scopes": [ + "team", "personal", "groupchat" + ] + } + ], + "composeExtensions": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "commands": [ + { + "id": "createCard", + "type": "action", + "description": "Test command to run action to create a card from Compose Box or Command Bar", + "title": "Create card - manifest params", + "parameters": [ + { + "name": "title", + "title": "Title parameter", + "description": "Text for title in Hero Card", + "inputType": "text" + }, + { + "name": "subtitle", + "title": "Subtitle parameter", + "description": "Text for subtitle in Hero Card", + "inputType": "text" + }, + { + "name": "text", + "title": "Body text", + "description": "Text for body in Hero Card", + "inputType": "text" + } + ] + }, + { + "id": "createWithPreview", + "type": "action", + "description": "Test command to run action to create a card with preview before sending", + "title": "Create cards with preview", + "fetchTask": true, + "parameters": [ + { + "name": "dummy", + "title": "Dummy parameter", + "description": "Dummy parameter" + } + ] + }, + { + "id": "shareMessage", + "type": "action", + "context": [ "message" ], + "description": "Test command to run action on message context (message sharing)", + "title": "Share Message", + "parameters": [ + { + "name": "includeImage", + "title": "Include Image", + "description": "Include image in Hero Card", + "inputType": "toggle" + } + ] + } + ] + } + ], + "validDomains": [ + "*.ngrok.io", + "*.azurewebsites.net", + "*.example.com" + ] +} diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/tsconfig.json b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/tsconfig.json new file mode 100644 index 0000000000..a168d60662 --- /dev/null +++ b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "composite": true, + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + } +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/tslint.json b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/tslint.json new file mode 100644 index 0000000000..ad00715f85 --- /dev/null +++ b/libraries/botbuilder/tests/teams/actionBasedMessagingExtension/tslint.json @@ -0,0 +1,18 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "interface-name" : [true, "never-prefix"], + "max-line-length": [false], + "no-console": [false, "log", "error"], + "no-var-requires": false, + "quotemark": [true, "single"], + "one-variable-per-declaration": false, + "curly": [true, "ignore-same-line"], + "trailing-comma": [true, {"multiline": "never", "singleline": "never"}] + }, + "rulesDirectory": [] +} diff --git a/libraries/botbuilder/tests/teams/activityUpdateAndDelete/.env b/libraries/botbuilder/tests/teams/activityUpdateAndDelete/.env new file mode 100644 index 0000000000..660828e3e8 --- /dev/null +++ b/libraries/botbuilder/tests/teams/activityUpdateAndDelete/.env @@ -0,0 +1,2 @@ +MicrosoftAppId= +MicrosoftAppPassword= \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/activityUpdateAndDelete/package.json b/libraries/botbuilder/tests/teams/activityUpdateAndDelete/package.json new file mode 100644 index 0000000000..0e73f0e426 --- /dev/null +++ b/libraries/botbuilder/tests/teams/activityUpdateAndDelete/package.json @@ -0,0 +1,32 @@ +{ + "name": "activity-update-delete-bot", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "build": "tsc --build", + "lint": "tslint -c tslint.json 'src/**/*.ts'", + "start": "tsc --build && node ./lib/index.js", + "watch": "nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "botbuilder": "file:../../../", + "dotenv": "^8.1.0", + "node-fetch": "^2.6.0", + "restify": "^8.4.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^12.7.1", + "@types/node-fetch": "^2.5.0", + "@types/request": "^2.48.1", + "@types/restify": "^7.2.7", + "nodemon": "^1.19.1", + "ts-node": "^7.0.1", + "tslint": "~5.18.0", + "typescript": "^3.2.4" + } +} diff --git a/libraries/botbuilder/tests/teams/activityUpdateAndDelete/src/activityUpdateAndDeleteBot.ts b/libraries/botbuilder/tests/teams/activityUpdateAndDelete/src/activityUpdateAndDeleteBot.ts new file mode 100644 index 0000000000..ec4e19df42 --- /dev/null +++ b/libraries/botbuilder/tests/teams/activityUpdateAndDelete/src/activityUpdateAndDeleteBot.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + ActivityHandler, + ActivityTypes, + TurnContext +} from 'botbuilder'; + +/* + * From the UI you can just @mention the bot from any channelwith any string EXCEPT for "delete". If you send the bot "delete" it will delete + * all of the previous bot responses and empty it's internal storage. + */ +export class ActivityUpdateAndDeleteBot extends ActivityHandler { + protected activityIds: string[]; + + constructor(activityIds: string[]) { + super(); + + this.activityIds = activityIds; + + // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + this.onMessage(async (context, next) => { + + TurnContext.removeRecipientMention(context.activity); + if (context.activity.text === 'delete') { + for (const activityId of this.activityIds) { + await context.deleteActivity(activityId); + } + + this.activityIds = []; + } else { + await this.sendMessageAndLogActivityId(context, `${context.activity.text}`); + + const text = context.activity.text; + for (const id of this.activityIds) { + await context.updateActivity({ id, text, type: ActivityTypes.Message }); + } + } + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } + + private async sendMessageAndLogActivityId(context: TurnContext, text: string): Promise { + const resourceResponse = await context.sendActivity({ text }); + await this.activityIds.push(resourceResponse.id); + } +} diff --git a/libraries/botbuilder/tests/teams/activityUpdateAndDelete/src/index.ts b/libraries/botbuilder/tests/teams/activityUpdateAndDelete/src/index.ts new file mode 100644 index 0000000000..cd22e3d7f0 --- /dev/null +++ b/libraries/botbuilder/tests/teams/activityUpdateAndDelete/src/index.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { BotFrameworkAdapter } from 'botbuilder'; + +import { ActivityUpdateAndDeleteBot } from './activityUpdateAndDeleteBot'; + +// Note: Ensure you have a .env file and include MicrosoftAppId and MicrosoftAppPassword. +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error('[onTurnError]:'); + console.error(error); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`); +}; + +const activityIds: string[] = []; + +// Create the bot. +const myBot = new ActivityUpdateAndDeleteBot(activityIds); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${ server.name } listening to ${ server.url }`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); +}); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route to bot + await myBot.run(context); + }); +}); diff --git a/libraries/botbuilder/tests/teams/activityUpdateAndDelete/teams-app-manifest/color.png b/libraries/botbuilder/tests/teams/activityUpdateAndDelete/teams-app-manifest/color.png new file mode 100644 index 0000000000..48a2de1330 Binary files /dev/null and b/libraries/botbuilder/tests/teams/activityUpdateAndDelete/teams-app-manifest/color.png differ diff --git a/libraries/botbuilder/tests/teams/activityUpdateAndDelete/teams-app-manifest/manifest.json b/libraries/botbuilder/tests/teams/activityUpdateAndDelete/teams-app-manifest/manifest.json new file mode 100644 index 0000000000..950f3d2012 --- /dev/null +++ b/libraries/botbuilder/tests/teams/activityUpdateAndDelete/teams-app-manifest/manifest.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.3/MicrosoftTeams.schema.json", + "manifestVersion": "1.3", + "version": "1.0.0", + "id": "00000000-0000-0000-0000-000000000000", + "packageName": "com.teams.sample.activityupdateanddelete", + "developer": { + "name": "ActivityUpdateAndDeleteBot", + "websiteUrl": "https://www.microsoft.com", + "privacyUrl": "https://www.teams.com/privacy", + "termsOfUseUrl": "https://www.teams.com/termsofuser" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "ActivityUpdateAndDeleteBot", + "full": "ActivityUpdateAndDeleteBot" + }, + "description": { + "short": "TeamsActivityUpdateAndDeleteBot", + "full": "TeamsActivityUpdateAndDeleteBot" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "scopes": [ + "groupchat", + "team", + "personal" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/activityUpdateAndDelete/teams-app-manifest/outline.png b/libraries/botbuilder/tests/teams/activityUpdateAndDelete/teams-app-manifest/outline.png new file mode 100644 index 0000000000..dbfa927729 Binary files /dev/null and b/libraries/botbuilder/tests/teams/activityUpdateAndDelete/teams-app-manifest/outline.png differ diff --git a/libraries/botbuilder/tests/teams/activityUpdateAndDelete/tsconfig.json b/libraries/botbuilder/tests/teams/activityUpdateAndDelete/tsconfig.json new file mode 100644 index 0000000000..a168d60662 --- /dev/null +++ b/libraries/botbuilder/tests/teams/activityUpdateAndDelete/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "composite": true, + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + } +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/activityUpdateAndDelete/tslint.json b/libraries/botbuilder/tests/teams/activityUpdateAndDelete/tslint.json new file mode 100644 index 0000000000..147316aee4 --- /dev/null +++ b/libraries/botbuilder/tests/teams/activityUpdateAndDelete/tslint.json @@ -0,0 +1,18 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "interface-name" : [true, "never-prefix"], + "max-line-length": [false], + "no-console": [false, "log", "error"], + "no-var-requires": false, + "quotemark": [true, "single"], + "one-variable-per-declaration": false, + "curly": [true, "ignore-same-line"], + "trailing-comma": [true, {"multiline": "never", "singleline": "never"}] + }, + "rulesDirectory": [] +} diff --git a/libraries/botbuilder/tests/teams/adaptiveCards/.env b/libraries/botbuilder/tests/teams/adaptiveCards/.env new file mode 100644 index 0000000000..660828e3e8 --- /dev/null +++ b/libraries/botbuilder/tests/teams/adaptiveCards/.env @@ -0,0 +1,2 @@ +MicrosoftAppId= +MicrosoftAppPassword= \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/adaptiveCards/package.json b/libraries/botbuilder/tests/teams/adaptiveCards/package.json new file mode 100644 index 0000000000..e6cc1d433a --- /dev/null +++ b/libraries/botbuilder/tests/teams/adaptiveCards/package.json @@ -0,0 +1,32 @@ +{ + "name": "adaptive-cards-bot", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "build": "tsc --build", + "lint": "tslint -c tslint.json 'src/**/*.ts'", + "start": "tsc --build && node ./lib/index.js", + "watch": "nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "botbuilder": "file:../../../", + "dotenv": "^8.1.0", + "node-fetch": "^2.6.0", + "restify": "^8.4.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^12.7.1", + "@types/node-fetch": "^2.5.0", + "@types/request": "^2.48.1", + "@types/restify": "^7.2.7", + "nodemon": "^1.19.1", + "ts-node": "^7.0.1", + "tslint": "~5.18.0", + "typescript": "^3.2.4" + } +} diff --git a/libraries/botbuilder/tests/teams/adaptiveCards/src/adaptiveCardsBot.ts b/libraries/botbuilder/tests/teams/adaptiveCards/src/adaptiveCardsBot.ts new file mode 100644 index 0000000000..8ea0bd7a0a --- /dev/null +++ b/libraries/botbuilder/tests/teams/adaptiveCards/src/adaptiveCardsBot.ts @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CardFactory, + InvokeResponse, + MessageFactory, + TaskModuleContinueResponse, + TaskModuleMessageResponse, + TaskModuleRequest, + TaskModuleResponse, + TaskModuleTaskInfo, + TeamsActivityHandler, + TurnContext, +} from 'botbuilder'; + +/** + * You can @mention the bot the text "1", "2", or "3". "1" will send back adaptive cards. "2" will send back a + * task module that contains an adpative card. "3" will return an adpative card that contains BF card actions. + */ +export class AdaptiveCardsBot extends TeamsActivityHandler { + constructor() { + super(); + + // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + this.onMessage(async (context, next) => { + TurnContext.removeRecipientMention(context.activity); + let text = context.activity.text; + if (text && text.length > 0) { + text = text.trim(); + switch (text) { + case '1': + await this.sendAdaptiveCard1(context); + break; + + case '2': + await this.sendAdaptiveCard2(context); + break; + + case '3': + await this.sendAdaptiveCard3(context); + break; + + default: + await context.sendActivity(`You said: ${text}`); + } + } else { + await context.sendActivity('App sent a message with empty text'); + const activityValue = context.activity.value; + if (activityValue) { + await context.sendActivity(`but with value ${JSON.stringify(activityValue)}`); + } + } + await next(); + }); + + this.onMembersAdded(async (context, next) => { + const membersAdded = context.activity.membersAdded; + for (const member of membersAdded) { + if (member.id !== context.activity.recipient.id) { + await context.sendActivity('Hello and welcome!'); + } + } + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } + + protected async handleTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { + await context.sendActivity(MessageFactory.text(`handleTeamsTaskModuleFetchAsync TaskModuleRequest: ${JSON.stringify(taskModuleRequest)}`)); + + /** + * The following line disables the lint rules for the Adaptive Card so that users can + * easily copy-paste the object into the Adaptive Cards Designer to modify their cards + * The Designer can be found here: https://adaptivecards.io/designer/ + */ + /* tslint:disable:quotemark object-literal-key-quotes */ + const card = CardFactory.adaptiveCard({ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "actions": [ + { + "data": { + "submitLocation": "taskModule" + }, + "title": "Action.Submit", + "type": "Action.Submit" + } + ], + "body": [ + { + "text": "This is an Adaptive Card within a Task Module", + "type": "TextBlock" + } + ], + "type": "AdaptiveCard", + "version": "1.0" + }); + /* tslint:enable:quotemark object-literal-key-quotes */ + return { + task: { + type: 'continue', + value: { + card, + height: 200, + title: 'Task Module Example', + width: 400 + } as TaskModuleTaskInfo + } as TaskModuleContinueResponse + } as TaskModuleResponse; + } + + protected async handleTeamsTaskModuleSubmit(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { + await context.sendActivity(MessageFactory.text(`handleTeamsTaskModuleSubmit value: ${JSON.stringify(taskModuleRequest)}`)); + + return { + task: { + type: 'message', + value: 'Thanks!' + } as TaskModuleMessageResponse + } as TaskModuleResponse; + } + + protected async handleTeamsCardActionInvoke(context: TurnContext): Promise { + await context.sendActivity(MessageFactory.text(`handleTeamsCardActionInvoke value: ${JSON.stringify(context.activity.value)}`)); + return { status: 200 } as InvokeResponse; + } + + private async sendAdaptiveCard1(context: TurnContext): Promise { + /* tslint:disable:quotemark object-literal-key-quotes */ + const card = CardFactory.adaptiveCard({ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "actions": [ + { + "data": { + "msteams": { + "type": "imBack", + "value": "text" + } + }, + "title": "imBack", + "type": "Action.Submit" + }, + { + "data": { + "msteams": { + "type": "messageBack", + "value": { "key": "value" } + } + }, + "title": "message back", + "type": "Action.Submit" + }, + { + "data": { + "msteams": { + "displayText": "display text message back", + "text": "text received by bots", + "type": "messageBack", + "value": { "key": "value" } + } + }, + "title": "message back local echo", + "type": "Action.Submit" + }, + { + "data": { + "msteams": { + "type": "invoke", + "value": { "key": "value" } + } + }, + "title": "invoke", + "type": "Action.Submit" + } + ], + "body": [ + { + "text": "Bot Builder actions", + "type": "TextBlock" + } + ], + "type": "AdaptiveCard", + "version": "1.0" + }); + /* tslint:enable:quotemark object-literal-key-quotes */ + await context.sendActivity(MessageFactory.attachment(card)); + } + + private async sendAdaptiveCard2(context: TurnContext): Promise { + /* tslint:disable:quotemark object-literal-key-quotes */ + const card = CardFactory.adaptiveCard({ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "actions": [ + { + "data": { + "msteams": { + "type": "invoke", + "value": { + "hiddenKey": "hidden value from task module launcher", + "type": "task/fetch" + } + } + }, + "title": "Launch Task Module", + "type": "Action.Submit" + } + ], + "body": [ + { + "text": "Task Module Adaptive Card", + "type": "TextBlock" + } + ], + "type": "AdaptiveCard", + "version": "1.0" + }); + /* tslint:enable:quotemark object-literal-key-quotes */ + await context.sendActivity(MessageFactory.attachment(card)); + } + + private async sendAdaptiveCard3(context: TurnContext): Promise { + /* tslint:disable:quotemark object-literal-key-quotes */ + const card = CardFactory.adaptiveCard({ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "actions": [ + { + "data": { + "key": "value" + }, + "title": "Action.Submit", + "type": "Action.Submit" + } + ], + "body": [ + { + "text": "Bot Builder actions", + "type": "TextBlock" + }, + { + "id": "x", + "type": "Input.Text" + } + ], + "type": "AdaptiveCard", + "version": "1.0" + }); + /* tslint:enable:quotemark object-literal-key-quotes */ + await context.sendActivity(MessageFactory.attachment(card)); + } +} diff --git a/libraries/botbuilder/tests/teams/adaptiveCards/src/index.ts b/libraries/botbuilder/tests/teams/adaptiveCards/src/index.ts new file mode 100644 index 0000000000..42c76f893f --- /dev/null +++ b/libraries/botbuilder/tests/teams/adaptiveCards/src/index.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { BotFrameworkAdapter } from 'botbuilder'; + +import { AdaptiveCardsBot } from './adaptiveCardsBot'; + +// Note: Ensure you have a .env file and include MicrosoftAppId and MicrosoftAppPassword. +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error('[onTurnError]:'); + console.error(error); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`); +}; + +// Create the bot. +const myBot = new AdaptiveCardsBot(); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${ server.name } listening to ${ server.url }`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); +}); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route to bot + await myBot.run(context); + }); +}); diff --git a/libraries/botbuilder/tests/teams/adaptiveCards/teams-app-manifest/color.png b/libraries/botbuilder/tests/teams/adaptiveCards/teams-app-manifest/color.png new file mode 100644 index 0000000000..48a2de1330 Binary files /dev/null and b/libraries/botbuilder/tests/teams/adaptiveCards/teams-app-manifest/color.png differ diff --git a/libraries/botbuilder/tests/teams/adaptiveCards/teams-app-manifest/manifest.json b/libraries/botbuilder/tests/teams/adaptiveCards/teams-app-manifest/manifest.json new file mode 100644 index 0000000000..7ceff74d7e --- /dev/null +++ b/libraries/botbuilder/tests/teams/adaptiveCards/teams-app-manifest/manifest.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.3/MicrosoftTeams.schema.json", + "manifestVersion": "1.3", + "version": "1.0.0", + "id": "00000000-0000-0000-0000-000000000000", + "packageName": "com.teams.sample.echobot", + "developer": { + "name": "CardActionsBotJS", + "websiteUrl": "https://www.microsoft.com", + "privacyUrl": "https://www.teams.com/privacy", + "termsOfUseUrl": "https://www.teams.com/termsofuser" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "CardActionsBot", + "full": "CardActionsBot" + }, + "description": { + "short": "CardActionsBot", + "full": "CardActionsBot" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "scopes": [ + "groupchat", + "team" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/adaptiveCards/teams-app-manifest/outline.png b/libraries/botbuilder/tests/teams/adaptiveCards/teams-app-manifest/outline.png new file mode 100644 index 0000000000..dbfa927729 Binary files /dev/null and b/libraries/botbuilder/tests/teams/adaptiveCards/teams-app-manifest/outline.png differ diff --git a/libraries/botbuilder/tests/teams/adaptiveCards/tsconfig.json b/libraries/botbuilder/tests/teams/adaptiveCards/tsconfig.json new file mode 100644 index 0000000000..a168d60662 --- /dev/null +++ b/libraries/botbuilder/tests/teams/adaptiveCards/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "composite": true, + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + } +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/adaptiveCards/tslint.json b/libraries/botbuilder/tests/teams/adaptiveCards/tslint.json new file mode 100644 index 0000000000..147316aee4 --- /dev/null +++ b/libraries/botbuilder/tests/teams/adaptiveCards/tslint.json @@ -0,0 +1,18 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "interface-name" : [true, "never-prefix"], + "max-line-length": [false], + "no-console": [false, "log", "error"], + "no-var-requires": false, + "quotemark": [true, "single"], + "one-variable-per-declaration": false, + "curly": [true, "ignore-same-line"], + "trailing-comma": [true, {"multiline": "never", "singleline": "never"}] + }, + "rulesDirectory": [] +} diff --git a/libraries/botbuilder/tests/teams/cardBotFramework/.env b/libraries/botbuilder/tests/teams/cardBotFramework/.env new file mode 100644 index 0000000000..660828e3e8 --- /dev/null +++ b/libraries/botbuilder/tests/teams/cardBotFramework/.env @@ -0,0 +1,2 @@ +MicrosoftAppId= +MicrosoftAppPassword= \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/cardBotFramework/package.json b/libraries/botbuilder/tests/teams/cardBotFramework/package.json new file mode 100644 index 0000000000..18d8ee9fc5 --- /dev/null +++ b/libraries/botbuilder/tests/teams/cardBotFramework/package.json @@ -0,0 +1,32 @@ +{ + "name": "cards-bot", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "build": "tsc --build", + "lint": "tslint -c tslint.json 'src/**/*.ts'", + "start": "tsc --build && node ./lib/index.js", + "watch": "nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "botbuilder": "file:../../../", + "dotenv": "^8.1.0", + "node-fetch": "^2.6.0", + "restify": "^8.4.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^12.7.1", + "@types/node-fetch": "^2.5.0", + "@types/request": "^2.48.1", + "@types/restify": "^7.2.7", + "nodemon": "^1.19.1", + "ts-node": "^7.0.1", + "tslint": "~5.18.0", + "typescript": "^3.2.4" + } +} diff --git a/libraries/botbuilder/tests/teams/cardBotFramework/src/cardsBot.ts b/libraries/botbuilder/tests/teams/cardBotFramework/src/cardsBot.ts new file mode 100644 index 0000000000..44a30362dd --- /dev/null +++ b/libraries/botbuilder/tests/teams/cardBotFramework/src/cardsBot.ts @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + ActionTypes, + Activity, + CardAction, + CardFactory, + MessageFactory, + TeamsActivityHandler, + TurnContext +} from 'botbuilder'; + +export class CardsBot extends TeamsActivityHandler { + // NOT SUPPORTED ON TEAMS: AnimationCard, AudioCard, VideoCard, OAuthCard + protected cardTypes: string[]; + constructor() { + super(); + /* + * From the UI you can @mention the bot, from any scope, any of the strings listed below to get that card back. + */ + const HeroCard = 'Hero'; + const ThumbnailCard = 'Thumbnail'; + const ReceiptCard = 'Receipt'; + const SigninCard = 'Signin'; + const Carousel = 'Carousel'; + const List = 'List'; + this.cardTypes = [HeroCard, ThumbnailCard, ReceiptCard, SigninCard, Carousel, List]; + + // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + this.onMessage(async (context, next) => { + const text = context.activity.text.trim().split(' ').splice(-1)[0]; + await context.sendActivity('You said ' + text); + + const activity = context.activity; + TurnContext.removeRecipientMention(activity); + let reply: Partial; + switch (text.toLowerCase()) { + case HeroCard.toLowerCase(): + reply = MessageFactory.attachment(this.getHeroCard()); + break; + case ThumbnailCard.toLowerCase(): + reply = MessageFactory.attachment(this.getThumbnailCard()); + break; + case ReceiptCard.toLowerCase(): + reply = MessageFactory.attachment(this.getReceiptCard()); + break; + case SigninCard.toLowerCase(): + reply = MessageFactory.attachment(this.getSigninCard()); + break; + case Carousel.toLowerCase(): + // NOTE: if cards are NOT the same height in a carousel, Teams will instead display as AttachmentLayoutTypes.List + reply = MessageFactory.carousel([this.getHeroCard(), this.getHeroCard(), this.getHeroCard()]); + break; + case List.toLowerCase(): + // NOTE: MessageFactory.Attachment with multiple attachments will default to AttachmentLayoutTypes.List + reply = MessageFactory.list([this.getHeroCard(), this.getHeroCard(), this.getHeroCard()]); + break; + + default: + reply = MessageFactory.attachment(this.getChoices()); + break; + } + + await context.sendActivity(reply); + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } + + private getHeroCard() { + return CardFactory.heroCard('BotFramework Hero Card', + 'Build and connect intelligent bots to interact with your users naturally wherever they are,' + + ' from text/sms to Skype, Slack, Office 365 mail and other popular services.', + ['https://sec.ch9.ms/ch9/7ff5/e07cfef0-aa3b-40bb-9baa-7c9ef8ff7ff5/buildreactionbotframework_960.jpg'], + [{ type: ActionTypes.OpenUrl, title: 'Get Started', value: 'https://docs.microsoft.com/bot-framework' }]); + } + + private getThumbnailCard() { + return CardFactory.thumbnailCard('BotFramework Thumbnail Card', + 'Build and connect intelligent bots to interact with your users naturally wherever they are,' + + ' from text/sms to Skype, Slack, Office 365 mail and other popular services.', + ['https://sec.ch9.ms/ch9/7ff5/e07cfef0-aa3b-40bb-9baa-7c9ef8ff7ff5/buildreactionbotframework_960.jpg'], + [{ type: ActionTypes.OpenUrl, title: 'Get Started', value: 'https://docs.microsoft.com/bot-framework' }]); + } + + private getReceiptCard() { + return CardFactory.receiptCard({ + buttons: [ + { + image: 'https://account.windowsazure.com/content/6.10.1.38-.8225.160809-1618/aux-pre/images/offer-icon-freetrial.png', + title: 'More information', + type: ActionTypes.OpenUrl, + value: 'https://azure.microsoft.com/en-us/pricing/' + } + ], + facts: [ + { key: 'Order Number', value: '1234' }, + { key: 'Payment Method', value: 'VISA 5555-****' } + ], + items: [ + { + image: { url: 'https://github.com/amido/azure-vector-icons/raw/master/renders/traffic-manager.png' }, + price: '$ 38.45', + quantity: '368', + subtitle: '', + tap: { title: '', type: '', value: null }, + text: '', + title: 'Data Transfer' + }, + { + image: { url: 'https://github.com/amido/azure-vector-icons/raw/master/renders/cloud-service.png' }, + price: '$ 45.00', + quantity: '720', + subtitle: '', + tap: { title: '', type: '', value: null }, + text: '', + title: 'App Service' + } + ], + tap: { title: '', type: '', value: null }, + tax: '$ 7.50', + title: 'John Doe', + total: '$ 90.95', + vat: '' + }); + } + + private getSigninCard() { + return CardFactory.signinCard('BotFramework Sign-in Card', 'https://login.microsoftonline.com/', 'Sign-in'); + } + + private getChoices() { + const actions = this.cardTypes.map((cardType) => ({ type: ActionTypes.MessageBack, title: cardType, text: cardType })) as CardAction[]; + return CardFactory.heroCard('Task Module Invocation from Hero Card', null, actions); + } +} diff --git a/libraries/botbuilder/tests/teams/cardBotFramework/src/index.ts b/libraries/botbuilder/tests/teams/cardBotFramework/src/index.ts new file mode 100644 index 0000000000..34de3e31cb --- /dev/null +++ b/libraries/botbuilder/tests/teams/cardBotFramework/src/index.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { BotFrameworkAdapter } from 'botbuilder'; + +import { CardsBot } from './cardsBot'; + +// Note: Ensure you have a .env file and include MicrosoftAppId and MicrosoftAppPassword. +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error('[onTurnError]:'); + console.error(error); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`); +}; + +// Create the bot. +const myBot = new CardsBot(); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${ server.name } listening to ${ server.url }`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); +}); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route to bot + await myBot.run(context); + }); +}); diff --git a/libraries/botbuilder/tests/teams/cardBotFramework/teams-app-manifest/icon-color.png b/libraries/botbuilder/tests/teams/cardBotFramework/teams-app-manifest/icon-color.png new file mode 100644 index 0000000000..bd9928dfc8 Binary files /dev/null and b/libraries/botbuilder/tests/teams/cardBotFramework/teams-app-manifest/icon-color.png differ diff --git a/libraries/botbuilder/tests/teams/cardBotFramework/teams-app-manifest/icon-outline.png b/libraries/botbuilder/tests/teams/cardBotFramework/teams-app-manifest/icon-outline.png new file mode 100644 index 0000000000..45e7a7c56e Binary files /dev/null and b/libraries/botbuilder/tests/teams/cardBotFramework/teams-app-manifest/icon-outline.png differ diff --git a/libraries/botbuilder/tests/teams/cardBotFramework/teams-app-manifest/manifest.json b/libraries/botbuilder/tests/teams/cardBotFramework/teams-app-manifest/manifest.json new file mode 100644 index 0000000000..85c3fd503d --- /dev/null +++ b/libraries/botbuilder/tests/teams/cardBotFramework/teams-app-manifest/manifest.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.3/MicrosoftTeams.schema.json", + "manifestVersion": "1.3", + "version": "1.0.0", + "id": "00000000-0000-0000-0000-000000000000", + "packageName": "com.teams.sample.cardsbot", + "developer": { + "name": "CardsBot", + "websiteUrl": "https://www.microsoft.com", + "privacyUrl": "https://www.teams.com/privacy", + "termsOfUseUrl": "https://www.teams.com/termsofuser" + }, + "icons": { + "color": "icon-color.png", + "outline": "icon-outline.png" + }, + "name": { + "short": "CardsBot", + "full": "CardsBot" + }, + "description": { + "short": "CardsBot", + "full": "CardsBot" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "scopes": [ + "groupchat", + "team", + "personal" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/cardBotFramework/tsconfig.json b/libraries/botbuilder/tests/teams/cardBotFramework/tsconfig.json new file mode 100644 index 0000000000..a168d60662 --- /dev/null +++ b/libraries/botbuilder/tests/teams/cardBotFramework/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "composite": true, + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + } +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/cardBotFramework/tslint.json b/libraries/botbuilder/tests/teams/cardBotFramework/tslint.json new file mode 100644 index 0000000000..147316aee4 --- /dev/null +++ b/libraries/botbuilder/tests/teams/cardBotFramework/tslint.json @@ -0,0 +1,18 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "interface-name" : [true, "never-prefix"], + "max-line-length": [false], + "no-console": [false, "log", "error"], + "no-var-requires": false, + "quotemark": [true, "single"], + "one-variable-per-declaration": false, + "curly": [true, "ignore-same-line"], + "trailing-comma": [true, {"multiline": "never", "singleline": "never"}] + }, + "rulesDirectory": [] +} diff --git a/libraries/botbuilder/tests/teams/conversationUpdate/.env b/libraries/botbuilder/tests/teams/conversationUpdate/.env new file mode 100644 index 0000000000..660828e3e8 --- /dev/null +++ b/libraries/botbuilder/tests/teams/conversationUpdate/.env @@ -0,0 +1,2 @@ +MicrosoftAppId= +MicrosoftAppPassword= \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/conversationUpdate/package.json b/libraries/botbuilder/tests/teams/conversationUpdate/package.json new file mode 100644 index 0000000000..ba963f6f5b --- /dev/null +++ b/libraries/botbuilder/tests/teams/conversationUpdate/package.json @@ -0,0 +1,30 @@ +{ + "name": "conversation-update", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "start": "tsc --build && node ./lib/index.js", + "build": "tsc --build", + "watch": "nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "botbuilder": "file:../../../", + "dotenv": "^8.1.0", + "node-fetch": "^2.6.0", + "restify": "^8.4.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^12.7.1", + "@types/node-fetch": "^2.5.0", + "@types/request": "^2.48.1", + "@types/restify": "^7.2.7", + "nodemon": "^1.19.1", + "ts-node": "^7.0.1", + "typescript": "^3.2.4" + } +} diff --git a/libraries/botbuilder/tests/teams/conversationUpdate/src/conversationUpdateBot.ts b/libraries/botbuilder/tests/teams/conversationUpdate/src/conversationUpdateBot.ts new file mode 100644 index 0000000000..80e70cbfa8 --- /dev/null +++ b/libraries/botbuilder/tests/teams/conversationUpdate/src/conversationUpdateBot.ts @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CardFactory, + ChannelAccount, + ChannelInfo, + MessageFactory, + TeamInfo, + TeamsActivityHandler, + TurnContext, +} from 'botbuilder'; + + +export class ConversationUpdateBot extends TeamsActivityHandler { + constructor() { + super(); + + // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + this.onMessage(async (context, next) => { + await context.sendActivity('You said ' + context.activity.text); + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + this.onTeamsChannelRenamedEvent(async (channelInfo: ChannelInfo, teamInfo: TeamInfo, context: TurnContext, next: () => Promise): Promise => { + const card = CardFactory.heroCard('Channel Renamed', `${channelInfo.name} is the new Channel name`); + const message = MessageFactory.attachment(card); + await context.sendActivity(message); + await next(); + }); + this.onTeamsChannelCreatedEvent(async (channelInfo: ChannelInfo, teamInfo: TeamInfo, context: TurnContext, next: () => Promise): Promise => { + const card = CardFactory.heroCard('Channel Created', `${channelInfo.name} is the Channel created`); + const message = MessageFactory.attachment(card); + await context.sendActivity(message); + await next(); + }); + this.onTeamsChannelDeletedEvent(async (channelInfo: ChannelInfo, teamInfo: TeamInfo, context: TurnContext, next: () => Promise): Promise => { + const card = CardFactory.heroCard('Channel Deleted', `${channelInfo.name} is the Channel deleted`); + const message = MessageFactory.attachment(card); + await context.sendActivity(message); + await next(); + }); + this.onTeamsTeamRenamedEvent(async (teamInfo: TeamInfo, context: TurnContext, next: () => Promise): Promise => { + const card = CardFactory.heroCard('Team Renamed', `${teamInfo.name} is the new Team name`); + const message = MessageFactory.attachment(card); + await context.sendActivity(message); + await next(); + }); + this.onTeamsMembersAddedEvent(async (membersAdded: ChannelAccount[], teamInfo: TeamInfo, context: TurnContext, next: () => Promise): Promise => { + let newMembers: string = ''; + console.log(JSON.stringify(membersAdded)); + membersAdded.forEach((account) => { + newMembers += account.id + ' '; + }); + const name = !teamInfo ? 'not in team' : teamInfo.name; + const card = CardFactory.heroCard('Account Added', `${newMembers} joined ${name}.`); + const message = MessageFactory.attachment(card); + await context.sendActivity(message); + await next(); + }); + this.onTeamsMembersRemovedEvent(async (membersRemoved: ChannelAccount[], teamInfo: TeamInfo, context: TurnContext, next: () => Promise): Promise => { + let removedMembers: string = ''; + console.log(JSON.stringify(membersRemoved)); + membersRemoved.forEach((account) => { + removedMembers += account.id + ' '; + }); + const name = !teamInfo ? 'not in team' : teamInfo.name; + const card = CardFactory.heroCard('Account Removed', `${removedMembers} removed from ${teamInfo.name}.`); + const message = MessageFactory.attachment(card); + await context.sendActivity(message); + await next(); + }); + } +} diff --git a/libraries/botbuilder/tests/teams/conversationUpdate/src/index.ts b/libraries/botbuilder/tests/teams/conversationUpdate/src/index.ts new file mode 100644 index 0000000000..bb90fd9eea --- /dev/null +++ b/libraries/botbuilder/tests/teams/conversationUpdate/src/index.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { BotFrameworkAdapter } from 'botbuilder'; + +// This bot's main dialog. +import { ConversationUpdateBot } from './conversationUpdateBot'; + +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${server.name} listening to ${server.url}`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); + console.log(`\nTo test your bot, see: https://aka.ms/debug-with-emulator`); +}); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// adapter.use(new TranscriptLoggerMiddleware(new FileTranscriptStore('./transcripts'))); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error('[onTurnError]:'); + console.error(error); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`); +}; + +// Create the main dialog. +const myBot = new ConversationUpdateBot(); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route to main dialog. + await myBot.run(context); + }); +}); diff --git a/libraries/botbuilder/tests/teams/conversationUpdate/teams-app-manifest/icon-color.png b/libraries/botbuilder/tests/teams/conversationUpdate/teams-app-manifest/icon-color.png new file mode 100644 index 0000000000..bd9928dfc8 Binary files /dev/null and b/libraries/botbuilder/tests/teams/conversationUpdate/teams-app-manifest/icon-color.png differ diff --git a/libraries/botbuilder/tests/teams/conversationUpdate/teams-app-manifest/icon-outline.png b/libraries/botbuilder/tests/teams/conversationUpdate/teams-app-manifest/icon-outline.png new file mode 100644 index 0000000000..45e7a7c56e Binary files /dev/null and b/libraries/botbuilder/tests/teams/conversationUpdate/teams-app-manifest/icon-outline.png differ diff --git a/libraries/botbuilder/tests/teams/conversationUpdate/teams-app-manifest/manifest.json b/libraries/botbuilder/tests/teams/conversationUpdate/teams-app-manifest/manifest.json new file mode 100644 index 0000000000..549f3d8b0a --- /dev/null +++ b/libraries/botbuilder/tests/teams/conversationUpdate/teams-app-manifest/manifest.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.3/MicrosoftTeams.schema.json", + "manifestVersion": "1.3", + "version": "1.0.0", + "id": "00000000-0000-0000-0000-000000000000", + "packageName": "com.teams.sample.conversationupdates", + "developer": { + "name": "ConversationUpdatesBot", + "websiteUrl": "https://www.microsoft.com", + "privacyUrl": "https://www.teams.com/privacy", + "termsOfUseUrl": "https://www.teams.com/termsofuser" + }, + "icons": { + "color": "icon-color.png", + "outline": "icon-outline.png" + }, + "name": { + "short": "ConversationUpdatesBot", + "full": "ConversationUpdatesBot" + }, + "description": { + "short": "ConversationUpdatesBot", + "full": "ConversationUpdatesBot" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "scopes": [ + "groupchat", + "team", + "personal" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/conversationUpdate/tsconfig.json b/libraries/botbuilder/tests/teams/conversationUpdate/tsconfig.json new file mode 100644 index 0000000000..a168d60662 --- /dev/null +++ b/libraries/botbuilder/tests/teams/conversationUpdate/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "composite": true, + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + } +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/fileUpload/.env b/libraries/botbuilder/tests/teams/fileUpload/.env new file mode 100644 index 0000000000..660828e3e8 --- /dev/null +++ b/libraries/botbuilder/tests/teams/fileUpload/.env @@ -0,0 +1,2 @@ +MicrosoftAppId= +MicrosoftAppPassword= \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/fileUpload/files/teams-logo.png b/libraries/botbuilder/tests/teams/fileUpload/files/teams-logo.png new file mode 100644 index 0000000000..78b0a0c308 Binary files /dev/null and b/libraries/botbuilder/tests/teams/fileUpload/files/teams-logo.png differ diff --git a/libraries/botbuilder/tests/teams/fileUpload/package.json b/libraries/botbuilder/tests/teams/fileUpload/package.json new file mode 100644 index 0000000000..395c5279d3 --- /dev/null +++ b/libraries/botbuilder/tests/teams/fileUpload/package.json @@ -0,0 +1,30 @@ +{ + "name": "teams-file-bot", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "start": "tsc --build && node ./lib/index.js", + "build": "tsc --build", + "watch": "nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "botbuilder": "file:../../../", + "dotenv": "^8.1.0", + "node-fetch": "^2.6.0", + "restify": "^8.4.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^12.7.1", + "@types/node-fetch": "^2.5.0", + "@types/request": "^2.48.1", + "@types/restify": "^7.2.7", + "nodemon": "^1.19.1", + "ts-node": "^7.0.1", + "typescript": "^3.2.4" + } +} diff --git a/libraries/botbuilder/tests/teams/fileUpload/src/fileUploadBot.ts b/libraries/botbuilder/tests/teams/fileUpload/src/fileUploadBot.ts new file mode 100644 index 0000000000..f7cb43e77b --- /dev/null +++ b/libraries/botbuilder/tests/teams/fileUpload/src/fileUploadBot.ts @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Activity, + Attachment, + TeamsActivityHandler, + TurnContext, + FileInfoCard, + FileConsentCard, + FileConsentCardResponse +} from 'botbuilder'; + +export class FileUploadBot extends TeamsActivityHandler { + constructor() { + super(); + + // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + this.onMessage(async (context, next) => { + await this.sendFileCard(context); + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + + this.onMembersAdded(async (context, next) => { + const membersAdded = context.activity.membersAdded; + for (const member of membersAdded) { + if (member.id !== context.activity.recipient.id) { + await context.sendActivity('Hello and welcome!'); + } + } + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } + + protected async handleTeamsFileConsentAccept(context: TurnContext, fileConsentCardResponse: FileConsentCardResponse): Promise { + try { + await this.sendFile(fileConsentCardResponse); + await this.fileUploadCompleted(context, fileConsentCardResponse); + } + catch (err) { + await this.fileUploadFailed(context, err.toString()); + } + } + + protected async handleTeamsFileConsentDecline(context: TurnContext, fileConsentCardResponse: FileConsentCardResponse): Promise { + let reply = this.createReply(context.activity); + reply.textFormat = "xml"; + reply.text = `Declined. We won't upload file ${fileConsentCardResponse.context["filename"]}.`; + await context.sendActivity(reply); + } + + private createReply(activity, text = null, locale = null) : Activity { + return { + type: 'message', + from: { id: activity.recipient.id, name: activity.recipient.name }, + recipient: { id: activity.from.id, name: activity.from.name }, + replyToId: activity.id, + serviceUrl: activity.serviceUrl, + channelId: activity.channelId, + conversation: { isGroup: activity.conversation.isGroup, id: activity.conversation.id, name: activity.conversation.name }, + text: text || '', + locale: locale || activity.locale + } as Activity; + } + + private async sendFile(fileConsentCardResponse: FileConsentCardResponse): Promise { + const request = require("request"); + const fs = require('fs'); + let context = fileConsentCardResponse.context; + let path = require('path'); + let filePath = path.join('files', context["filename"]); + let stats = fs.statSync(filePath); + let fileSizeInBytes = stats['size']; + fs.createReadStream(filePath).pipe(request.put(fileConsentCardResponse.uploadInfo.uploadUrl)); + } + + private async sendFileCard(context: TurnContext): Promise { + let filename = "teams-logo.png"; + let fs = require('fs'); + let path = require('path'); + let stats = fs.statSync(path.join('files', filename)); + let fileSizeInBytes = stats['size']; + + let fileContext = { + filename: filename + }; + + let attachment = { + content: { + description: 'This is the file I want to send you', + fileSizeInBytes: fileSizeInBytes, + acceptContext: fileContext, + declineContext: fileContext + }, + contentType: 'application/vnd.microsoft.teams.card.file.consent', + name: filename + } as Attachment; + + var replyActivity = this.createReply(context.activity); + replyActivity.attachments = [ attachment ]; + await context.sendActivity(replyActivity); + } + + private async fileUploadCompleted(context: TurnContext, fileConsentCardResponse: FileConsentCardResponse): Promise { + let fileUploadInfoName = fileConsentCardResponse.uploadInfo.name; + let downloadCard = { + uniqueId: fileConsentCardResponse.uploadInfo.uniqueId, + fileType: fileConsentCardResponse.uploadInfo.fileType, + }; + + let attachment = { + content: downloadCard, + contentType: 'application/vnd.microsoft.teams.card.file.info', + name: fileUploadInfoName, + contentUrl: fileConsentCardResponse.uploadInfo.contentUrl, + }; + + let reply = this.createReply(context.activity, `File uploaded. Your file ${fileUploadInfoName} is ready to download`); + reply.textFormat = 'xml'; + reply.attachments = [attachment]; + await context.sendActivity(reply); + } + + private async fileUploadFailed(context: TurnContext, error: string): Promise { + let reply = this.createReply(context.activity, `File upload failed. Error:
${error}
`); + reply.textFormat = 'xml'; + await context.sendActivity(reply); + } +} diff --git a/libraries/botbuilder/tests/teams/fileUpload/src/index.ts b/libraries/botbuilder/tests/teams/fileUpload/src/index.ts new file mode 100644 index 0000000000..2f26514f33 --- /dev/null +++ b/libraries/botbuilder/tests/teams/fileUpload/src/index.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { BotFrameworkAdapter } from 'botbuilder'; + +// This bot's main dialog. +import { FileUploadBot } from './fileUploadBot'; + +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${server.name} listening to ${server.url}`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); + console.log(`\nTo test your bot, see: https://aka.ms/debug-with-emulator`); +}); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// adapter.use(new TranscriptLoggerMiddleware(new FileTranscriptStore('./transcripts'))); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error('[onTurnError]:'); + console.error(error); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`); +}; + +// Create the main dialog. +const myBot = new FileUploadBot(); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route to main dialog. + await myBot.run(context); + }); +}); diff --git a/libraries/botbuilder/tests/teams/fileUpload/teams-app-manifest/color.png b/libraries/botbuilder/tests/teams/fileUpload/teams-app-manifest/color.png new file mode 100644 index 0000000000..48a2de1330 Binary files /dev/null and b/libraries/botbuilder/tests/teams/fileUpload/teams-app-manifest/color.png differ diff --git a/libraries/botbuilder/tests/teams/fileUpload/teams-app-manifest/manifest.json b/libraries/botbuilder/tests/teams/fileUpload/teams-app-manifest/manifest.json new file mode 100644 index 0000000000..21fa9fdca1 --- /dev/null +++ b/libraries/botbuilder/tests/teams/fileUpload/teams-app-manifest/manifest.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.3/MicrosoftTeams.schema.json", + "manifestVersion": "1.3", + "version": "1.0", + "id": "00000000-0000-0000-0000-000000000000", + "packageName": "com.microsoft.teams.samples.filebot", + "developer": { + "name": "Microsoft Corp", + "websiteUrl": "https://example.azurewebsites.net", + "privacyUrl": "https://example.azurewebsites.net/privacy", + "termsOfUseUrl": "https://example.azurewebsites.net/termsofuse" + }, + "name": { + "short": "V4 File Sample", + "full": "Microsoft Teams V4 File Sample Bot" + }, + "description": { + "short": "Sample bot using V4 SDK to demo bot file features", + "full": "Sample bot using V4 Bot Builder SDK and V4 Microsoft Teams Extension SDK to demo bot file features" + }, + "icons": { + "outline": "outline.png", + "color": "color.png" + }, + "accentColor": "#abcdef", + "bots": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "scopes": [ + "personal" + ], + "supportsFiles": true + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [ + "*.azurewebsites.net" + ] +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/fileUpload/teams-app-manifest/outline.png b/libraries/botbuilder/tests/teams/fileUpload/teams-app-manifest/outline.png new file mode 100644 index 0000000000..dbfa927729 Binary files /dev/null and b/libraries/botbuilder/tests/teams/fileUpload/teams-app-manifest/outline.png differ diff --git a/libraries/botbuilder/tests/teams/fileUpload/tsconfig.json b/libraries/botbuilder/tests/teams/fileUpload/tsconfig.json new file mode 100644 index 0000000000..a168d60662 --- /dev/null +++ b/libraries/botbuilder/tests/teams/fileUpload/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "composite": true, + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + } +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/integrationBot/.env b/libraries/botbuilder/tests/teams/integrationBot/.env new file mode 100644 index 0000000000..53264abea7 --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/.env @@ -0,0 +1,3 @@ +MicrosoftAppId= +MicrosoftAppPassword= +AllowedTeamsTenantId= \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/integrationBot/README.md b/libraries/botbuilder/tests/teams/integrationBot/README.md new file mode 100644 index 0000000000..cea335b754 --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/README.md @@ -0,0 +1,491 @@ +# Integration Bot + +This bot has been created using [Bot Framework](https://dev.botframework.com), it is used to exercise features of [Teams](https://products.office.com/microsoft-teams/) integration. + +## Recordings for integration testing. +This bot demonstrates how to perform recordings with Teams for integration testing. See [background info](https://github.com/daveta/teams/blob/master/README.md) for more information. + +### Recording +See [the prequisites for running this sample](#prerequisites) to completely set up **ngrok**. + +Recordings use [nock](https://github.com/nock/nock) to capture traffic from the client. The specific script/scenario used to exercise all the features of this bot are [here](#recording-script) + +```bash +# Install +mkdir recordings +npm install + +# Set record mode + # Windows + set TEST_MODE=RECORD + # Powershell + $env:TEST_MODE=RECORD + # Linux + export TEST_MODE=RECORD + +# run the bot +npm start +``` +Once the bot is started in record mode, execute the scenario(s) you wish to repeat later. In this sample, typing ''`exit`" shuts the bot down gracefully. All the traffic should be captured in the form of .json files in the `recordings` directory. + +### Playing recordings locally +To play back the recordings locally, perform the following. +```bash +# Set record mode - true or false + # Windows + set TEST_MODE=PLAY + # Powershell + $env:TEST_MODE=PLAY + # Linux + export TEST_MODE=PLAY + +# run the bot +npm start +``` +The bot should run through the recording and validate the data is correct. + +### Proxy host the recordings + +For other languages such as C#, it's convenient to host the recordings as a pseudo Teams server, and run through the scenarios that were recorded. + +To create the proxy host (which acts as the Teams proxy) perform the following: + + > NOTE: The recordings exercise any AuthN paths. + > + > NOTE: The proxy by default listens on port 3979/ +``` + # Windows + set TEST_MODE=PROXY_HOST + set PROXY_HOST=http://localhost:3979 # Or wherever the proxy is running + # Powershell + $env:TEST_MODE=PROXY_HOST + $env:PROXY_HOST=http://localhost:3979 # Or wherever the proxy is running + # Linux + export TEST_MODE=PROXY_HOST + export PROXY_HOST=http://localhost:3979 # Or wherever the proxy is running + +# run the bot +npm start +``` + + +In order to trigger the test, the client first must begin recording (`GET /api/runtests`) to start the tests. + + + +![Proxy Recordingsl](./_images/proxy_recordings.jpg) + +### Play recordings against proxy + +To use the proxy host, the client must trigger the proxy to begin the calls (see drawing above). + +To create the use the proxy host, perform the following: + > NOTE: The recordings exercise any AuthN paths. +``` + # Windows + set TEST_MODE=PROXY_PLAY + # Powershell + $env:TEST_MODE=PROXY_PLAY + # Linux + export TEST_MODE=PROXY_PLAY + +# run the bot +npm start +``` +## Recording Script +- Action Based Messaging Extension - Fetch Task + + - Enter a Teams **channel** (other than "General") where the bot is registered. + + - Click on triple dots (". . .") menu at bottom of compose window. + + - Select "Integration Bot" + + - Click on "+" to the right of the title. A menu should pop up. + + - Click on "Create cards with preview" + + - Type a question in the "Enter text for Question" (ie, What is your phone?) + + - Select "Is Multi-Select" to False + + - Enter three options to choose from (ie, Android, Apple, Other). + + - Click the "Submit" button This should take you to a different dialog. + + - Click on the "Edit" button. This should return you back to the original screen (with question and options you originally typed in) + + - Click "Submit" button a second time. + + - Click "Send" + - **Validate:** You should see the same card posted in 3 areas: + + - Your compose window + + - The Channel you are currently in. + + - The General channel for the team. + + - Click Submit on one of the cards. + + - **Validate**: You should see a message something lik: + + `App sent a message with empty text but with value {"submitLocation":"messagingExtensionSubmit", "Answer":""}` + +- Link Unfurl: + + - Type `@Integration Bot https://foo.com` (Integration Bot being the name of the registered bot.) + + - Type a space after the 'm' in '.com'. + - **Validate**: You should see a hero card with "HeroCard for Link Unfurling" with "You entered https://foo.com" + + - Hit enter. + - Validate: The bot should reply with "You said 'https://foo.com''" + +- Add/Delete Users + + - On a Team where the bot is registered, click on the ". . ." menu next to the name -> Add member. + - Select a user to add to the team. + - **Validate**: A message should be posted to the "General" channel with "Account Added" message. + - Remove the user from the team. + - **Validate**: A message should be posted to the "General" channel with "Account Removed" message. + +- Rename Events + + - On a Team where the bot is registered, click on the ". . ." menu next to the name -> "Edit Team". + - Under "Team name" rename the team. + - **Validate**: A message should be posted to the "General" channel with "Team Renamed" message. + - On a Team **Channel** where the bot is registered, click on the ". . ." menu next to the name -> "Edit Channel". + - Under "Channel name" rename the channel. + - **Validate**: A message should be posted to the "General" channel with "Channel Renamed" message + +- Add Channel Event + + - On a Team where the bot is registered, click on the ". . ." menu next to the name -> "Add Channel". + - Under "Channel name" type a new channel. + - **Validate**: A message should be posted to the "General" channel with "Channel Created" message + +- Delete channel Event + + - On a Team Channel where the bot is registered, click on the ". . ." menu next to the name -> "Delete this channel". + - Click on Delete. + - **Validate**: A message should be posted to the "General" channel with "Channel Deleted" message + +- Add member to Group Chat + + - On a Teams Group Chat, add a member (click on the icon with people and plus sign towards the top left of the UI) + - Add a new user. + - **Validate**: A message should be posted to the group chat with "Member Added" message. + +- Remove member from Group Chat + + - On a Teams Group Chat, click on the icon with people and plus sign towards the top left of the UI. + - Click the "X" to the right of a user to remove the user. + - **Validate**: A message should be posted to the group chat with "Member Removed" message. + +- Adaptive card "1" + - Works in personal, group chat and teams + + - On a Teams channel or General channel with the bot installed, type "@IntegrationBot1". The "1" is the command sent to the bot. + + - Validate: A card should appear that says "Bot Builder actions" and contains four buttons: + + - imBack: Click this and it should add a message that references the bot and adds "text". + - message back: The bot should respond with "text" / "App sent a message with empty text but with value {"key":"value}. + - message back local echo: Click this and it should send a message from you that says 'display text message back'. + - invoke: Click this and see the bot should send a message that states: "text received by bots" / "handleTeamsCardActionInvoke value: {"key":"value"}" + +- Adaptive card "2" + - Works in personal, group chat and teams + - On a Teams channel or General channel with the bot installed, type "@IntegrationBot2". The "2" is the command sent to the bot. + + - Validate: A card should appear that says "Task Module Adaptive Card" and contains one button: + + - Launch Task Module: Click this and it should add a message that references the bot and adds "text". + - Type a message and submit. + - It should show "Thanks" + - Click X to dismiss the modal dialog. + +- Adaptive card "3" + - Works in personal, group chat and teams + - On a Teams channel or General channel with the bot installed, type "@IntegrationBot3". The "3" is the command sent to the bot. + - Validate: A card should appear that says "Bot Builder actions" with an edit control and button labeled "Action.Submit". + - Validate: Type some text and click "Action.Submit". You should receive a message `App sent a message with empty text but with valid {"key":"value", "x":""}` + +- "Hero" card + - Works in personal, group chat and teams + - On a Teams channel or General channel with the bot installed, type "@IntegrationBothero". The "hero" is the command sent to the bot. + - The bot should respond with a hero card and button "Get Started" + +- "Thumbnail" card + - Works in personal, group chat and teams + - On a Teams channel or General channel with the bot installed, type "@IntegrationBotthumbnail". The "thumbnail" is the command sent to the bot. + - The bot should respond with a thumbnail card and button "Get Started" + +- "Receipt" card + - Works in personal, group chat and teams + - On a Teams channel or General channel with the bot installed, type "@IntegrationBotreceipt". The "receipt" is the command sent to the bot. + - The bot should respond with a receipt card and button "More information" + +- "Signin" card + - Works in personal, group chat and teams + - On a Teams channel or General channel with the bot installed, type "@IntegrationBotsignin". The "signin" is the command sent to the bot. + - The bot should respond with a Sign-In card and button "BotFramework Sign-in Card" + +- "Carousel" card + - Works in personal, group chat and teams + - On a Teams channel or General channel with the bot installed, type "@IntegrationBotcarousel". The "carousel" is the command sent to the bot. + - The bot should respond with a carousel card with three cards. + +- "List" card + - Works in personal, group chat and teams + - On a Teams channel or General channel with the bot installed, type "@IntegrationBotlist". The "list" is the command sent to the bot. + - The bot should respond with a list of two cards. + +- "show members" + - Works in personal, group chat and teams + - On a Teams channel or General channel with the bot installed, type "@IntegrationBotshow members". The "show members" is the command sent to the bot. + - The bot should respond with a list of members currently in the team. + +- "show channels" + - Works in personal, group chat and teams + - On a Teams channel or General channel with the bot installed, type "@IntegrationBotshow channels". The "show channels" is the command sent to the bot. + - The bot should respond with a list of channels.. + +- "show details" + - Works in personal, group chat and teams + - On a Teams channel or General channel with the bot installed, type "@IntegrationBotshow details". The "show details" is the command sent to the bot. + - The bot should respond with team name, team ID and AAD GroupID. + +- "updatetext" + - Works in personal, group chat and teams + - On a Teams channel or General channel with the bot installed, type "@IntegrationBotupdatetext". The "updatetext" (or anything that isn't a command listed above) is the command sent to the bot. + - The bot should respond with "You said..." and then update the message with "updatetext" or whatever you typed. + +- File upload "file" + - Works in personal chat only + - On a Teams channel or General channel with the bot installed, type "@IntegrationBotfile". The "file" is the command sent to the bot. + - The bot should respond with a file consent card with the 'Allow' and 'Decline' buttons +- If 'Allow' button is clicked, bot will respond with "File is uploaded. Your file teams-logo.png is ready to download". + - If 'Decline' button is clicked, bot will respond with "Declined. We won't upload file teams-logo.png". + +- O365 card "o365" + - Works in personal, group chat and teams + - On a Teams channel, group chat or personal chat with the bot installed, type "@IntegrationBoto365". The "0365" is the command sent to the bot. + - The bot should respond with Office 365 card with 'Multiple Choice', 'Text Input', 'Date Input', 'View Action', 'Open Uri' buttons at the bottom of the card + - Click on each of the button and add inputs. With the exceptions of 'View Action' and 'Open Uri' buttons, bot should respond back with text containing input names/values and button name, ie 'O365ConnectorCardActionQuery event value: {"body":"{"date1":"2019-10-09T07:30:00.000Z", "date2":"2019-10-02T07:00:00.000Z"}","actionId":"card-3-btn-1"}'. + +- Task Module "task module" + - Works in personal, group chat and teams + - On a Teams channel, group chat or personal chat with the bot installed, type "@IntegrationBottask module". The "task module" is the command sent to the bot. + - The bot should respond with Task Module Invocation Hero Card. + - Click on the Adaptive Card button. A task module popup shows up, type some text and click on Submit button. Bot should reply with 'Thanks' inside the popup and also with something like 'handleTeamsTaskModuleFetchAsync Value: {"data":{"usertext":"<>"},"context":{"theme":"dark"}}'. + +- Search Based Messaging Extension + - Type @IntegrationBothello on the search box and hit enter. There should be three hero cards displayed. + - 1st card 'You searched for: You said "hello"'. Clicking on the card should display a card with bot name. + - 2nd card 'Learn more about Teams' with link to doc. Clicking on the card should display a card with bot name. + - 3rd card 'You said "hello"'. Clicking on this card will display card with text 'You selected a search result!' and 'You searched for "hello"'. + + +## Prerequisites + +This sample **requires** prerequisites in order to run. + +### Clone the repository + +```bash +git clone https://github.com/Microsoft/botbuilder-samples.git +``` + + + + + +### Ngrok setup + +1. Download and install [Ngrok](https://ngrok.com/download) +2. In terminal navigate to the directory where Ngrok is installed +3. Run this command: ```ngrok http -host-header=rewrite 3978 ``` +4. Copy the https://xxxxxxxx.ngrok.io address and put it into notepad. + >**NOTE** : You want the `https` address. + +### Azure Set up to provision bot with Team Channel enabled + +1. Login to the [Azure Portal](https://portal.azure.com) +2. (optional) create a new resource group if you don't currently have one +3. Go to your resource group +4. Click "Create a new resource" +5. Search for "Bot Channel Registration" +6. Click Create +7. Enter bot name, subscription +8. In the "Messaging endpoint url" enter the ngrok address from earlier. +9. Finish the url with "/api/messages. It should look like ```https://xxxxxxxxx.ngrok.io/api/messages``` +10. Click the "Microsoft App Id and password" box +11. Click on "Create New" +12. Click on "Create App ID in the App Registration Portal" +13. Click "New registration" +14. Enter a name +15. Under "Supported account types" select "Accounts in any organizational directory and personal Microsoft accounts" +16. Click register +17. Copy the application (client) ID and put it in Notepad. Label it "Microsoft App ID" +18. Go to "Certificates & Secrets" +19. Click "+ New client secret" +20. Enter a description +21. Click "Add" +22. Copy the value and put it into Notepad. Label it "Password" +23. (back in the channel registration view) Copy/Paste the Microsoft App ID and Password into their respective fields +24. Click Create +25. Go to "Resource groups" on the left +26. Select the resource group that the bot channel reg was created in +27. Select the bot channel registration +28. Go to Settings +29. Select the "Teams" icon under "Add a featured channel +30. Click Save + + + +### Updating Sample Settings + +1. Open `.env` file. +2. Enter the app id under the `MicrosoftAppId` and the password under the `MicrosoftAppPassword`. +3. Save the close the file. +4. Under the `TeamsAppManifest` folder open the `manifest.json` file. +5. Update the ```botId``` fields with the Microsoft App ID from before (2 places) +6. Update the ```id``` with the Microsoft App ID from before + + + +### Uploading the bot to Teams + +1. In file explorer navigate to the `teams-app-manifest` folder in the project +2. Select the 3 files (`color.png`, `manifest.json` and `outline.png`) and zip them. +3. Open Teams +4. Click on "Apps" +5. Select "Upload a custom app" on the left at the bottom +6. Select the zip +7. Select for you +8. (optionally) click install if prompted +9. Click open + + + +## To try this sample + +- In a terminal, navigate to bot folder (ie `link-unfurling`). + +It's suggested you run the bot twice. First with recording turned on, second with recording turned off. + +```bash +# Install +mkdir recordings +npm install + +# Set record mode - true or false + # Windows + set AZURE_NOCK_RECORD=true + # Powershell + $env:AZURE_NOCK_RECORD="true" + +# run the bot +npm start +``` + +### Interacting with the bot + +1. Send a message to your bot in Teams. +2. Type any valid https url, ie https://www.bing.com, wait until the url is bolded, hit the space key and you should see a thumbnail card for the url info populated, like below. + ![Sample Unfurl](./_images/1569017810114.png) +3. If recording, as you send messages, you should see new text files in the `./recordings` directory. +4. Type `exit` into the Teams bot if you want the bot to exit. + +### Notes +1. Url unfurls are cached. Try using different https `.com` sites each time. +2. If you install multiple bots which handle link unfurling, the first bot that responds will be displayed. +3. If the bot returns multiple results, the first result will be displayed. +4. Link unfurling action is handled by `onAppBasedLinkQuery` method in the bot code + +# Appendix A + +To edit drawing, go to [Web Sequence Diagrams](https://www.websequencediagrams.com/) and copy and paste the following: +``` +title Proxy Recordings Scenario + +note left of Bot\n(TEST_MODE=PROXY_PLAY) : **Start the tests**\nAll the tests run\nin context of this call. +Bot\n(TEST_MODE=PROXY_PLAY) ->Proxy Service\n(TEST_MODE=PROXY_HOST): GET /api/runtests +note right of Proxy Service\n(TEST_MODE=PROXY_HOST): Load recordings and\ninvoke bot simulating user. +Proxy Service\n(TEST_MODE=PROXY_HOST)->Bot\n(TEST_MODE=PROXY_PLAY): Activity Request +Bot\n(TEST_MODE=PROXY_PLAY)->Proxy Service\n(TEST_MODE=PROXY_HOST): Activity Response +note left of Bot\n(TEST_MODE=PROXY_PLAY): Bot processes Activity +Bot\n(TEST_MODE=PROXY_PLAY)->Proxy Service\n(TEST_MODE=PROXY_HOST): Activity Reply +note right of Proxy Service\n(TEST_MODE=PROXY_HOST): Validate reply\nmatches recordings. +Proxy Service\n(TEST_MODE=PROXY_HOST)->Bot\n(TEST_MODE=PROXY_PLAY): Activity Reply Response +Proxy Service\n(TEST_MODE=PROXY_HOST)->Bot\n(TEST_MODE=PROXY_PLAY): Response /api/runtests +note left of Bot\n(TEST_MODE=PROXY_PLAY): **End the tests**\nAll tests are complete. +``` + +# Appendix B + +Here's an example `.vscode/launch.json` to debug this sample. +It's assumed the `workspaceFolder` is set to `botbuilder-js` directory. + +```json +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + { + "type": "node", + "request": "launch", + "name": "Proxy Play", + "program": "${workspaceFolder}\\libraries\\botbuilder\\tests\\teams\\integrationBot\\lib\\index.js", + "sourceMaps": false, + "env": {"TEST_MODE":"PROXY_PLAY", "PROXY_HOST":"http://localhost:3979"}, + "cwd": "${workspaceFolder}\\libraries\\botbuilder\\tests\\teams\\integrationBot", + "outFiles": [ + "${workspaceFolder}/**/*.js" + ] + }, + { + "type": "node", + "request": "launch", + "name": "Play", + "program": "${workspaceFolder}\\libraries\\botbuilder\\tests\\teams\\integrationBot\\lib\\index.js", + "sourceMaps": false, + "env": {"TEST_MODE":"PLAY", "PROXY_HOST":"http://localhost:3979"}, + "cwd": "${workspaceFolder}\\libraries\\botbuilder\\tests\\teams\\integrationBot", + "outFiles": [ + "${workspaceFolder}/**/*.js" + ] + }, + { + "type": "node", + "request": "launch", + "name": "Record", + "program": "${workspaceFolder}\\libraries\\botbuilder\\tests\\teams\\integrationBot\\lib\\index.js", + "sourceMaps": false, + "env": {"TEST_MODE":"RECORD"}, + "cwd": "${workspaceFolder}\\libraries\\botbuilder\\tests\\teams\\integrationBot", + "outFiles": [ + "${workspaceFolder}/**/*.js" + ] + }, + { + "type": "node", + "request": "launch", + "name": "Proxy Host", + "program": "${workspaceFolder}\\libraries\\botbuilder\\tests\\teams\\integrationBot\\lib\\index.js", + "sourceMaps": false, + "env": {"TEST_MODE":"PROXY_HOST"}, + "cwd": "${workspaceFolder}\\libraries\\botbuilder\\tests\\teams\\integrationBot", + "outFiles": [ + "${workspaceFolder}/**/*.js" + ] + }, + + ] +} +``` \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/integrationBot/_images/1569017810114.png b/libraries/botbuilder/tests/teams/integrationBot/_images/1569017810114.png new file mode 100644 index 0000000000..aca975e18e Binary files /dev/null and b/libraries/botbuilder/tests/teams/integrationBot/_images/1569017810114.png differ diff --git a/libraries/botbuilder/tests/teams/integrationBot/_images/proxy_recordings.jpg b/libraries/botbuilder/tests/teams/integrationBot/_images/proxy_recordings.jpg new file mode 100644 index 0000000000..c25689eb1f Binary files /dev/null and b/libraries/botbuilder/tests/teams/integrationBot/_images/proxy_recordings.jpg differ diff --git a/libraries/botbuilder/tests/teams/integrationBot/files/teams-logo.png b/libraries/botbuilder/tests/teams/integrationBot/files/teams-logo.png new file mode 100644 index 0000000000..78b0a0c308 Binary files /dev/null and b/libraries/botbuilder/tests/teams/integrationBot/files/teams-logo.png differ diff --git a/libraries/botbuilder/tests/teams/integrationBot/package.json b/libraries/botbuilder/tests/teams/integrationBot/package.json new file mode 100644 index 0000000000..24d0fe4034 --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/package.json @@ -0,0 +1,34 @@ +{ + "name": "integration-bot", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "build": "tsc --build", + "lint": "tslint -c tslint.json 'src/**/*.ts'", + "start": "tsc --build && node ./lib/index.js", + "watch": "nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "botbuilder": "file:../../../", + "dotenv": "^8.1.0", + "node-fetch": "^2.6.0", + "node-mocks-http": "^1.8.0", + "restify": "^8.4.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^12.7.1", + "@types/node-fetch": "^2.5.0", + "@types/request": "^2.48.1", + "@types/restify": "^7.2.7", + "nodemon": "^1.19.1", + "ts-node": "^7.0.1", + "tslint": "~5.18.0", + "typescript": "^3.2.4", + "node-mocks-http": "^1.8.0" + } +} diff --git a/libraries/botbuilder/tests/teams/integrationBot/recordings/20191030203551783-request-integrationBot.json b/libraries/botbuilder/tests/teams/integrationBot/recordings/20191030203551783-request-integrationBot.json new file mode 100644 index 0000000000..f27a73c10a --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/recordings/20191030203551783-request-integrationBot.json @@ -0,0 +1 @@ +{"type":"request","url":"/api/messages","method":"POST","headers":{"host":"localhost:3978","user-agent":"Microsoft-SkypeBotApi (Microsoft-BotFramework/3.0)","content-length":"1102","authorization":"Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IktwSVdSVWxnZmlObGQxRFR4WkFoZTRpTm1rQSIsInR5cCI6IkpXVCIsIng1dCI6IktwSVdSVWxnZmlObGQxRFR4WkFoZTRpTm1rQSJ9.eyJzZXJ2aWNldXJsIjoiaHR0cHM6Ly9zbWJhLnRyYWZmaWNtYW5hZ2VyLm5ldC9hbWVyLyIsIm5iZiI6MTU3MjQ2Nzc1MSwiZXhwIjoxNTcyNDcxMzUxLCJpc3MiOiJodHRwczovL2FwaS5ib3RmcmFtZXdvcmsuY29tIiwiYXVkIjoiZWFjNWU1YzYtYWIxNC00ZjUwLTgwY2ItNDkyMTNmZTUxYjFhIn0.h2tXiTt7yKN7zbJuy46kL_TgulmhHMWRCrRrZ9mChMrxN7iiKTvfmwg5VD3gka45hYyxqMQRtGmsu3spsEdTJButVsiHu0pLwVlip6wxbqYcqonA0Ai42UxCoHpqm8W-Byd4E13jyLapBMkQyOhrluqH8J87_Re1Je9TTGSfPilMB-ZltAuSXsGsTFJPFuLKY8qKFH6MhxMPr4-w85Kz__beRnNK_5TtuW2dhpxZAd3tpIe1l-xxafIvTTlLr8vgL3-R9mlOXxoQC3y0NZOPyGZoUH0ucB71GB6e0Z-ec2mBh-LsIT6ZM3pr1JyBxyAlppPsVvFbjKHH915TgmyGXg","content-type":"application/json; charset=utf-8","contextid":"tcid=1587811588051382128, server=BL2PEPF000001F3","ms-cv":"Ipsn4/p9tU2Ch14HJZPGQg.1","x-forwarded-for":"13.68.16.246","x-forwarded-proto":"https","x-original-host":"5bc93bb7.ngrok.io"},"body":{"name":"composeExtension/fetchTask","type":"invoke","timestamp":"2019-10-30T20:35:50.909Z","localTimestamp":"2019-10-30T20:35:50.909Z","id":"f:1587811588051382128","channelId":"msteams","serviceUrl":"https://smba.trafficmanager.net/amer/","from":{"id":"29:1CFIxRJBLjT_io3iNsDZxMk_4YPWQmr3DZUM1I6ihf5L2ocCDqg9S1vkHK6kRdGfK1P_2LFE2nQqFIvdu-GBOZA","name":"Dave Taniguchi","aadObjectId":"6b703ce3-6d14-4fe6-b90c-dc5f03657ae4"},"conversation":{"isGroup":true,"conversationType":"channel","tenantId":"72f988bf-86f1-41af-91ab-2d7cd011db47","id":"19:9d3c71104f8c49128368b23a5eaeb86d@thread.skype"},"recipient":{"id":"28:eac5e5c6-ab14-4f50-80cb-49213fe51b1a","name":"teams2"},"entities":[{"locale":"en-US","country":"US","platform":"Windows","type":"clientInfo"}],"channelData":{"channel":{"id":"19:9d3c71104f8c49128368b23a5eaeb86d@thread.skype"},"team":{"id":"19:097e6717fd7245bdbeba6baa13840db8@thread.skype"},"tenant":{"id":"72f988bf-86f1-41af-91ab-2d7cd011db47"},"source":{"name":"compose"}},"value":{"commandId":"createWithPreview","commandContext":"","context":{"theme":"default"}},"locale":"en-US"}} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/integrationBot/recordings/20191030203553176-request-integrationBot.json b/libraries/botbuilder/tests/teams/integrationBot/recordings/20191030203553176-request-integrationBot.json new file mode 100644 index 0000000000..8c39a12ed6 --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/recordings/20191030203553176-request-integrationBot.json @@ -0,0 +1 @@ +{"type":"request","url":"/api/messages","method":"POST","headers":{"host":"localhost:3978","user-agent":"Microsoft-SkypeBotApi (Microsoft-BotFramework/3.0)","content-length":"1235","authorization":"Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IktwSVdSVWxnZmlObGQxRFR4WkFoZTRpTm1rQSIsInR5cCI6IkpXVCIsIng1dCI6IktwSVdSVWxnZmlObGQxRFR4WkFoZTRpTm1rQSJ9.eyJzZXJ2aWNldXJsIjoiaHR0cHM6Ly9zbWJhLnRyYWZmaWNtYW5hZ2VyLm5ldC9hbWVyLyIsIm5iZiI6MTU3MjQ2Nzc1MywiZXhwIjoxNTcyNDcxMzUzLCJpc3MiOiJodHRwczovL2FwaS5ib3RmcmFtZXdvcmsuY29tIiwiYXVkIjoiZWFjNWU1YzYtYWIxNC00ZjUwLTgwY2ItNDkyMTNmZTUxYjFhIn0.ZVX_xa3et-2XAvCZkLxDgMU3BvrQbD3P-5xCi4bJAJVrjys-vGTcQRFaw2x4tkKA94OJnvli0grti8Fqkij5AegN1JvTXHjTnOMbRn1ExbV30IGlZ6pvLOjiwOU4MxB4cd-PhNZJibvEIskQzykbDy4Q1D6MRsoKGDfpbuQxNGJlVTkI2DpLMzXCRRCDDONaFhGmzb0CYJe_QED2z-9ZAZohAl5ewZyR_7f-3ghwMjiAX89ip6QqnrJZVqq5lzlJXKT97rG_ta12OFsTEr4nwCas72eLUsL7IRt1kQiSFnstzeP_N8tGOyjId7mN8td3jEjaKcoYj449VS_0KGgW8w","content-type":"application/json; charset=utf-8","contextid":"tcid=9154388441650815283, server=BL2PEPF000001C0","ms-cv":"kr7W7MXWhEqEkh165JJ1Ow.1","x-forwarded-for":"13.68.16.246","x-forwarded-proto":"https","x-original-host":"5bc93bb7.ngrok.io"},"body":{"name":"composeExtension/submitAction","type":"invoke","timestamp":"2019-10-30T20:35:53.092Z","localTimestamp":"2019-10-30T20:35:53.092Z","id":"f:9154388441650815283","channelId":"msteams","serviceUrl":"https://smba.trafficmanager.net/amer/","from":{"id":"29:1CFIxRJBLjT_io3iNsDZxMk_4YPWQmr3DZUM1I6ihf5L2ocCDqg9S1vkHK6kRdGfK1P_2LFE2nQqFIvdu-GBOZA","name":"Dave Taniguchi","aadObjectId":"6b703ce3-6d14-4fe6-b90c-dc5f03657ae4"},"conversation":{"isGroup":true,"conversationType":"channel","tenantId":"72f988bf-86f1-41af-91ab-2d7cd011db47","id":"19:9d3c71104f8c49128368b23a5eaeb86d@thread.skype"},"recipient":{"id":"28:eac5e5c6-ab14-4f50-80cb-49213fe51b1a","name":"teams2"},"entities":[{"locale":"en-US","country":"US","platform":"Windows","type":"clientInfo"}],"channelData":{"channel":{"id":"19:9d3c71104f8c49128368b23a5eaeb86d@thread.skype"},"team":{"id":"19:097e6717fd7245bdbeba6baa13840db8@thread.skype"},"tenant":{"id":"72f988bf-86f1-41af-91ab-2d7cd011db47"},"source":{"name":"compose"}},"value":{"commandId":"createWithPreview","commandContext":"","context":{"theme":"default"},"data":{"submitLocation":"messagingExtensionFetchTask","Question":"","MultiSelect":"true","Option1":"","Option2":"","Option3":""}},"locale":"en-US"}} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/integrationBot/recordings/20191030203554638-request-integrationBot.json b/libraries/botbuilder/tests/teams/integrationBot/recordings/20191030203554638-request-integrationBot.json new file mode 100644 index 0000000000..8d8862fd7a --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/recordings/20191030203554638-request-integrationBot.json @@ -0,0 +1 @@ +{"type":"request","url":"/api/messages","method":"POST","headers":{"host":"localhost:3978","user-agent":"Microsoft-SkypeBotApi (Microsoft-BotFramework/3.0)","content-length":"2144","authorization":"Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IktwSVdSVWxnZmlObGQxRFR4WkFoZTRpTm1rQSIsInR5cCI6IkpXVCIsIng1dCI6IktwSVdSVWxnZmlObGQxRFR4WkFoZTRpTm1rQSJ9.eyJzZXJ2aWNldXJsIjoiaHR0cHM6Ly9zbWJhLnRyYWZmaWNtYW5hZ2VyLm5ldC9hbWVyLyIsIm5iZiI6MTU3MjQ2Nzc1NCwiZXhwIjoxNTcyNDcxMzU0LCJpc3MiOiJodHRwczovL2FwaS5ib3RmcmFtZXdvcmsuY29tIiwiYXVkIjoiZWFjNWU1YzYtYWIxNC00ZjUwLTgwY2ItNDkyMTNmZTUxYjFhIn0.lzirystlvbi7YUOITaHVHfsxqkzZl6vm7xI7LBIfWcLJuDYUuDVeu0AABn_F0eMJP5moNuHgccAp84ZWSlhAD_qy0CfZaD-FfO5AcV5PkBm2mlx4F1bwO0t8oFK30Z6Ln9KrSd0QV9GgKfTD60LlRR9jvI2_R2I5vq2pJwRgHsBWyZnf6J2pAbkD_7ekH2riWiv0AJwasUVoM7CkgZSENBLzeVimsk8zX_6yCTp7G9phD5NSCOoaVHrEjEspl9zznk6FLFOCTe1yy143ZeTpA8c2me_Bnl_rBFBFvYIglogCbc8juicOlT2l5mZ81Y4MQsqsvMhv5W5s8zI337h-rQ","content-type":"application/json; charset=utf-8","contextid":"tcid=755751340556186369, server=BL2PEPF00000EB9","ms-cv":"lrgw1DBJV0Kybr7S/K9Usw.1","x-forwarded-for":"13.68.16.246","x-forwarded-proto":"https","x-original-host":"5bc93bb7.ngrok.io"},"body":{"name":"composeExtension/submitAction","type":"invoke","timestamp":"2019-10-30T20:35:54.562Z","localTimestamp":"2019-10-30T20:35:54.562Z","id":"f:755751340556186369","channelId":"msteams","serviceUrl":"https://smba.trafficmanager.net/amer/","from":{"id":"29:1CFIxRJBLjT_io3iNsDZxMk_4YPWQmr3DZUM1I6ihf5L2ocCDqg9S1vkHK6kRdGfK1P_2LFE2nQqFIvdu-GBOZA","name":"Dave Taniguchi","aadObjectId":"6b703ce3-6d14-4fe6-b90c-dc5f03657ae4"},"conversation":{"isGroup":true,"conversationType":"channel","tenantId":"72f988bf-86f1-41af-91ab-2d7cd011db47","id":"19:9d3c71104f8c49128368b23a5eaeb86d@thread.skype"},"recipient":{"id":"28:eac5e5c6-ab14-4f50-80cb-49213fe51b1a","name":"teams2"},"entities":[{"locale":"en-US","country":"US","platform":"Windows","type":"clientInfo"}],"channelData":{"channel":{"id":"19:9d3c71104f8c49128368b23a5eaeb86d@thread.skype"},"team":{"id":"19:097e6717fd7245bdbeba6baa13840db8@thread.skype"},"tenant":{"id":"72f988bf-86f1-41af-91ab-2d7cd011db47"},"source":{"name":"compose"}},"value":{"commandId":"createWithPreview","commandContext":"","botMessagePreviewAction":"send","botActivityPreview":[{"type":"message/card","attachments":[{"content":{"type":"AdaptiveCard","body":[{"color":null,"horizontalAlignment":null,"isSubtle":false,"maxLines":0,"size":null,"text":"Adaptive Card from Task Module","weight":"bolder","wrap":false,"separator":false,"type":"TextBlock"},{"color":null,"horizontalAlignment":null,"isSubtle":false,"maxLines":0,"size":null,"text":"","weight":null,"wrap":false,"id":"Question","separator":false,"type":"TextBlock"},{"isMultiline":false,"maxLength":0,"placeholder":"Answer here...","style":null,"isRequired":false,"id":"Answer","separator":false,"type":"Input.Text"},{"choices":[{"title":"","value":""},{"title":"","value":""},{"title":"","value":""}],"isMultiSelect":true,"style":"expanded","isRequired":false,"id":"Choices","separator":false,"type":"Input.ChoiceSet"}],"actions":[{"data":{"submitLocation":"messagingExtensionSubmit"},"title":"Submit","type":"Action.Submit"}],"version":"1.0"},"contentType":"application/vnd.microsoft.card.adaptive"}]}],"context":{"theme":"default"}},"locale":"en-US"}} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/integrationBot/recordings/20191030203555603-reply-integrationBot.json b/libraries/botbuilder/tests/teams/integrationBot/recordings/20191030203555603-reply-integrationBot.json new file mode 100644 index 0000000000..be21dc6472 --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/recordings/20191030203555603-reply-integrationBot.json @@ -0,0 +1 @@ +{"scope":"https://smba.trafficmanager.net:443","method":"POST","path":"/amer/v3/conversations","body":{"isGroup":true,"activity":{"type":"message","attachments":[{"contentType":"application/vnd.microsoft.card.adaptive","content":{"actions":[{"type":"Action.Submit","title":"Submit","data":{"submitLocation":"messagingExtensionSubmit"}}],"body":[{"text":"Adaptive Card from Task Module","type":"TextBlock","weight":"bolder"},{"text":"","type":"TextBlock","id":"Question"},{"id":"Answer","placeholder":"Answer here...","type":"Input.Text"},{"choices":[{"title":"","value":""},{"title":"","value":""},{"title":"","value":""}],"id":"Choices","isMultiSelect":true,"style":"expanded","type":"Input.ChoiceSet"}],"type":"AdaptiveCard","version":"1.0"}}]},"channelData":{"channel":{"id":"19:9d3c71104f8c49128368b23a5eaeb86d@thread.skype"}}},"status":201,"response":{"id":"19:9d3c71104f8c49128368b23a5eaeb86d@thread.skype;messageid=1572467755453","activityId":"1:1CI01BT6FVaEEoYB6ObZRwpz2kfEw79BhvYdhKqSp_UI"},"rawHeaders":["Content-Length","143","Content-Type","application/json; charset=utf-8","Server","Microsoft-HTTPAPI/2.0","Date","Wed, 30 Oct 2019 20:35:55 GMT","Connection","close"],"reqheaders":{"accept":"application/json, text/plain, */*","authorization":"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiJodHRwczovL2FwaS5ib3RmcmFtZXdvcmsuY29tIiwiaXNzIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvZDZkNDk0MjAtZjM5Yi00ZGY3LWExZGMtZDU5YTkzNTg3MWRiLyIsImlhdCI6MTU3MjQ2NzQ1NCwibmJmIjoxNTcyNDY3NDU0LCJleHAiOjE1NzI0NzEzNTQsImFpbyI6IjQyVmdZRmlnd0hZNGkrdlY2V2N2YnpxdnViZnlNZ0E9IiwiYXBwaWQiOiJlYWM1ZTVjNi1hYjE0LTRmNTAtODBjYi00OTIxM2ZlNTFiMWEiLCJhcHBpZGFjciI6IjEiLCJpZHAiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9kNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIvIiwidGlkIjoiZDZkNDk0MjAtZjM5Yi00ZGY3LWExZGMtZDU5YTkzNTg3MWRiIiwidXRpIjoiUlhPemhhUlFNVXFWbFIycE03TVVBQSIsInZlciI6IjEuMCJ9.m1MyrACZsTsJ0X1B14_EeOVJnipETqhvrk0mtWRm-o-8wfC7HJkfyV6QU9xjhYo9w0pJ_clKUmk1gJUZ1SzxRM98-YAAxowhCGCbhEEofhk2nreInIQTML_ipZeVC45DAjDcFxiSkftfKq5heQR2u1vNLgGg5swSe4_f5RNXIpCGRSJCp9PN3B9DkVQHH6sKiZ9g1iuDVsghjt-ykMe-08sjVbjW9jWW5Walu1qRLiePLRTrgXCB7u9XtojaEfnW9gMI0zTlocdBCP5_LqOB9HA6mhQ--bv2a_OealuOnLFD_dmpL98RzH-6JoEYLB-nRCijJ84TN6OWtEZTDyG_YQ","cookie":"","content-type":"application/json; charset=utf-8","content-length":729,"host":"smba.trafficmanager.net"}} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/integrationBot/src/activityLog.ts b/libraries/botbuilder/tests/teams/integrationBot/src/activityLog.ts new file mode 100644 index 0000000000..8764f53576 --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/src/activityLog.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Storage, +} from 'botbuilder'; + +import{ + Activity +} from 'botframework-schema' + + +export class ActivityLog { + private readonly _storage: Storage; + + public constructor(storage: Storage) { + this._storage = storage; + } + + public async append(activityId: string, activity: Partial): Promise { + if (activityId == null) + { + throw new TypeError("activityId is required for ActivityLog.append"); + } + + if (activity == null) + { + throw new TypeError("activity is required for ActivityLog.append"); + } + + let obj = { }; + obj[activityId] = { activity }; + + await this._storage.write( obj ); + + return; + } + + public async find(activityId: string): Promise + { + if (activityId == null) + { + throw new TypeError("activityId is required for ActivityLog.find"); + } + + var items = await this._storage.read( [ activityId ] ); + return (items && items[activityId]) ? items[activityId].activity : null; + } +} diff --git a/libraries/botbuilder/tests/teams/integrationBot/src/adaptiveCardHelper.ts b/libraries/botbuilder/tests/teams/integrationBot/src/adaptiveCardHelper.ts new file mode 100644 index 0000000000..4250e209bb --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/src/adaptiveCardHelper.ts @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Attachment, + CardFactory, + MessagingExtensionAction, + MessagingExtensionActionResponse, + TaskModuleContinueResponse, + TaskModuleTaskInfo +} from 'botbuilder'; + +import { SubmitExampleData } from './submitExampleData'; + +export class AdaptiveCardHelper { + public static toSubmitExampleData(action: MessagingExtensionAction): SubmitExampleData { + const activityPreview = action.botActivityPreview[0]; + const attachmentContent = activityPreview.attachments[0].content; + + const userText = attachmentContent.body[1].text as string; + const choiceSet = attachmentContent.body[3]; + + return { + MultiSelect: choiceSet.isMultiSelect ? 'true' : 'false', + Option1: choiceSet.choices[0].title, + Option2: choiceSet.choices[1].title, + Option3: choiceSet.choices[2].title, + Question: userText + } as SubmitExampleData; + } + + public static createTaskModuleAdaptiveCardResponse( + userText: string = null, + isMultiSelect: boolean = true, + option1: string = null, + option2: string = null, + option3: string = null): MessagingExtensionActionResponse { + + const responseCard = CardFactory.adaptiveCard({ + actions: [ + { + data: { + submitLocation: 'messagingExtensionFetchTask' + }, + title: 'Submit', + type: 'Action.Submit' + } + ], + body: [ + { + text: 'This is an Adaptive Card within a Task Module', + type: 'TextBlock', + weight: 'bolder' + }, + { type: 'TextBlock', text: 'Enter text for Question:' }, + { + id: 'Question', + placeholder: 'Question text here', + type: 'Input.Text', + value: userText + }, + { type: 'TextBlock', text: 'Options for Question:' }, + { type: 'TextBlock', text: 'Is Multi-Select:' }, + { + choices: [{title: 'True', value: 'true'}, {title: 'False', value: 'false'}], + id: 'MultiSelect', + isMultiSelect: false, + style: 'expanded', + type: 'Input.ChoiceSet', + value: isMultiSelect ? 'true' : 'false' + }, + { + id: 'Option1', + placeholder: 'Option 1 here', + type: 'Input.Text', + value: option1 + }, + { + id: 'Option2', + placeholder: 'Option 2 here', + type: 'Input.Text', + value: option2 + }, + { + id: 'Option3', + placeholder: 'Option 3 here', + type: 'Input.Text', + value: option3 + } + ], + type: 'AdaptiveCard', + version : '1.0' + }); + + return { + task: { + type: 'continue', + value: { + card: responseCard as Attachment, + height: 450, + title: 'Task Module Fetch Example', + url: null, + width: 500 + } as TaskModuleTaskInfo + } as TaskModuleContinueResponse + } as MessagingExtensionActionResponse; + } + + public static toAdaptiveCardAttachment(data: SubmitExampleData): Attachment { + return CardFactory.adaptiveCard({ + actions: [ + { type: 'Action.Submit', title: 'Submit', data: { submitLocation: 'messagingExtensionSubmit'} } + ], + body: [ + { text: 'Adaptive Card from Task Module', type: 'TextBlock', weight: 'bolder' }, + { text: `${ data.Question }`, type: 'TextBlock', id: 'Question' }, + { id: 'Answer', placeholder: 'Answer here...', type: 'Input.Text' }, + { + choices: [ + {title: data.Option1, value: data.Option1}, + {title: data.Option2, value: data.Option2}, + {title: data.Option3, value: data.Option3} + ], + id: 'Choices', + isMultiSelect: Boolean(data.MultiSelect), + style: 'expanded', + type: 'Input.ChoiceSet' + } + ], + type: 'AdaptiveCard', + version: '1.0' + }); + } +} diff --git a/libraries/botbuilder/tests/teams/integrationBot/src/cardResponseHelpers.ts b/libraries/botbuilder/tests/teams/integrationBot/src/cardResponseHelpers.ts new file mode 100644 index 0000000000..cb138c3c59 --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/src/cardResponseHelpers.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Activity, + Attachment, + InputHints, + MessageFactory, + MessagingExtensionActionResponse, + MessagingExtensionAttachment, + MessagingExtensionResult, + TaskModuleContinueResponse, + TaskModuleTaskInfo +} from 'botbuilder'; + +export class CardResponseHelpers { + public static toTaskModuleResponse(cardAttachment: Attachment): MessagingExtensionActionResponse { + return { + task: { + height: 450, + title: 'Task Module Fetch Example', + value: { + card: cardAttachment + } as TaskModuleTaskInfo, + width: 500 + } as TaskModuleContinueResponse + } as MessagingExtensionActionResponse; + } + + public static toComposeExtensionResultResponse(cardAttachment: Attachment): MessagingExtensionActionResponse { + + return { + composeExtension: { + attachmentLayout: 'list', + attachments: [cardAttachment as MessagingExtensionAttachment], + type: 'result' + + } as MessagingExtensionResult + } as MessagingExtensionActionResponse; + } + + public static toMessagingExtensionBotMessagePreviewResponse(cardAttachment: Attachment): MessagingExtensionActionResponse { + return { + composeExtension: { + activityPreview: MessageFactory.attachment(cardAttachment, null, null, InputHints.ExpectingInput) as Activity, + type: 'botMessagePreview' + } as MessagingExtensionResult + } as MessagingExtensionActionResponse; + } +} diff --git a/libraries/botbuilder/tests/teams/integrationBot/src/index.ts b/libraries/botbuilder/tests/teams/integrationBot/src/index.ts new file mode 100644 index 0000000000..39b53c7408 --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/src/index.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { BotFrameworkAdapter, MemoryStorage, UserState } from 'botbuilder'; + +import { IntegrationBot } from './integrationBot'; + +// Import middleware for filtering messages based on Teams Tenant Id +import { TeamsTenantFilteringMiddleware } from './teamsTenantFilteringMiddleware'; + +// Set up Nock +import * as nockHelper from './../src/nock-helper/nock-helper'; +import { ActivityLog } from './activityLog'; +nockHelper.nockHttp('integrationBot') + + +// Note: Ensure you have a .env file and include MicrosoftAppId and MicrosoftAppPassword. +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// Use the TeamsTenantFilteringMiddleware IF there is an AllowedTeamsTenantId +if(process.env.AllowedTeamsTenantId){ + let teamsTenantFilteringMiddleware = new TeamsTenantFilteringMiddleware(process.env.AllowedTeamsTenantId); + adapter.use(teamsTenantFilteringMiddleware); +} + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error('[onTurnError]:'); + console.error(error); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`); +}; + +const activityIds: string[] = []; + +// Define a state store for your bot. See https://aka.ms/about-bot-state to learn more about using MemoryStorage. +// A bot requires a state store to persist the dialog and user state between messages. + +// For local development, in-memory storage is used. +// CAUTION: The Memory Storage used here is for local bot debugging only. When the bot +// is restarted, anything stored in memory will be gone. +const memoryStorage = new MemoryStorage(); +const userState = new UserState(memoryStorage); +const activityLog = new ActivityLog(memoryStorage); + +// Create the bot. +const myBot = new IntegrationBot(userState, activityIds, activityLog); + +if (nockHelper.isRecording()) { + // Create HTTP server. + const server = restify.createServer(); + + server.get('/*', restify.plugins.serveStatic({ + directory: path.join(__dirname, '../static'), + appendRequestPath: false + })); + + server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${ server.name } listening to ${ server.url }`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); + }); + + // Listen for incoming requests. + server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + if (req.body.text == 'exit') { + //graceful shutdown + process.exit(); + } + nockHelper.logRequest(req, 'integrationBot'); + + // Route to bot + await myBot.run(context); + }); + }); +} +else if (nockHelper.isPlaying()) { + nockHelper.processRecordings('integrationBot', adapter, myBot) + .then(result => console.log(result)) + .catch(err => { + console.error(JSON.stringify(err)); + }); +} +else if (nockHelper.isProxyHost()) { + // Create HTTP proxy server. + nockHelper.proxyRecordings(); +} +else if (nockHelper.isProxyPlay()) { + nockHelper.proxyPlay(myBot); +} diff --git a/libraries/botbuilder/tests/teams/integrationBot/src/integrationBot.ts b/libraries/botbuilder/tests/teams/integrationBot/src/integrationBot.ts new file mode 100644 index 0000000000..4c379adce7 --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/src/integrationBot.ts @@ -0,0 +1,1228 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Activity, + ActivityTypes, + ActionTypes, + AppBasedLinkQuery, + Attachment, + BotFrameworkAdapter, + BotState, + CardAction, + CardFactory, + ChannelAccount, + ChannelInfo, + ConversationParameters, + ConversationReference, + ConversationResourceResponse, + FileInfoCard, + FileConsentCard, + FileConsentCardResponse, + HeroCard, + InvokeResponse, + MessageFactory, + MessageReaction, + MessagingExtensionAction, + MessagingExtensionActionResponse, + MessagingExtensionAttachment, + MessagingExtensionQuery, + MessagingExtensionResponse, + MessagingExtensionResult, + MessagingExtensionSuggestedAction, + O365ConnectorCard, + O365ConnectorCardActionBase, + O365ConnectorCardActionCard, + O365ConnectorCardActionQuery, + O365ConnectorCardDateInput, + O365ConnectorCardHttpPOST, + O365ConnectorCardMultichoiceInput, + O365ConnectorCardMultichoiceInputChoice, + O365ConnectorCardOpenUri, + O365ConnectorCardTextInput, + O365ConnectorCardViewAction, + TaskModuleContinueResponse, + TaskModuleMessageResponse, + TaskModuleRequest, + TaskModuleResponse, + TaskModuleTaskInfo, + TeamsActivityHandler, + TeamsChannelData, + teamsGetChannelId, + TeamDetails, + TeamInfo, + TeamsInfo, + TurnContext, +} from 'botbuilder'; + +import { AdaptiveCardHelper } from './adaptiveCardHelper'; +import { CardResponseHelpers } from './cardResponseHelpers'; +import { SubmitExampleData } from './submitExampleData'; +import { ActivityLog } from './activityLog'; + +const RICH_CARD_PROPERTY = 'richCardConfig'; +const HeroCard = 'Hero'; +const ThumbnailCard = 'Thumbnail'; +const ReceiptCard = 'Receipt'; +const SigninCard = 'Signin'; +const Carousel = 'Carousel'; +const List = 'List'; + +/** + * We need to change the key for the user state because the bot might not be in the conversation, which means they get a 403 error. + * @param userState + */ +export class IntegrationBot extends TeamsActivityHandler { + protected activityIds: string[]; + // NOT SUPPORTED ON TEAMS: AnimationCard, AudioCard, VideoCard, OAuthCard + protected cardTypes: string[]; + protected _log: ActivityLog; + + + /* + * See README.md on what this bot supports. + */ + constructor(public userState: BotState, activityIds: string[], activityLog: ActivityLog) { + super(); + this.activityIds = activityIds; + this.cardTypes = [ HeroCard, ThumbnailCard, ReceiptCard, SigninCard, Carousel, List]; + this._log = activityLog + + // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + this.onMessage(async (context, next) => { + TurnContext.removeRecipientMention(context.activity); + let text = context.activity.text; + if (text && text.length > 0) { + text = text.trim(); + await this.handleBotCommand(text, context, next); + } + else { + await context.sendActivity('App sent a message with empty text'); + const activityValue = context.activity.value; + if (activityValue) { + await context.sendActivity(`but with value ${JSON.stringify(activityValue)}`); + } + } + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + + this.onTeamsChannelRenamedEvent(async (channelInfo: ChannelInfo, teamInfo: TeamInfo, context: TurnContext, next: () => Promise): Promise => { + const card = CardFactory.heroCard('Channel Renamed', `${channelInfo.name} is the new Channel name`); + const message = MessageFactory.attachment(card); + await context.sendActivity(message); + await next(); + }); + + this.onTeamsChannelCreatedEvent(async (channelInfo: ChannelInfo, teamInfo: TeamInfo, context: TurnContext, next: () => Promise): Promise => { + const card = CardFactory.heroCard('Channel Created', `${channelInfo.name} is the Channel created`); + const message = MessageFactory.attachment(card); + await context.sendActivity(message); + await next(); + }); + + this.onTeamsChannelDeletedEvent(async (channelInfo: ChannelInfo, teamInfo: TeamInfo, context: TurnContext, next: () => Promise): Promise => { + const card = CardFactory.heroCard('Channel Deleted', `${channelInfo.name} is the Channel deleted`); + const message = MessageFactory.attachment(card); + await context.sendActivity(message); + await next(); + }); + + this.onTeamsTeamRenamedEvent(async (teamInfo: TeamInfo, context: TurnContext, next: () => Promise): Promise => { + const card = CardFactory.heroCard('Team Renamed', `${teamInfo.name} is the new Team name`); + const message = MessageFactory.attachment(card); + await context.sendActivity(message); + await next(); + }); + + this.onTeamsMembersAddedEvent(async (membersAdded: ChannelAccount[], teamInfo: TeamInfo, context: TurnContext, next: () => Promise): Promise => { + var newMembers: string = ''; + membersAdded.forEach((account) => { + newMembers.concat(account.id,' '); + }); + const card = CardFactory.heroCard('Account Added', `${newMembers} joined ${teamInfo.name}.`); + const message = MessageFactory.attachment(card); + await context.sendActivity(message); + await next(); + }); + + this.onTeamsMembersRemovedEvent(async (membersRemoved: ChannelAccount[], teamInfo: TeamInfo, context: TurnContext, next: () => Promise): Promise => { + var removedMembers: string = ''; + membersRemoved.forEach((account) => { + removedMembers += account.id + ' '; + }); + const card = CardFactory.heroCard('Account Removed', `${removedMembers} removed from ${teamInfo.name}.`); + const message = MessageFactory.attachment(card); + await context.sendActivity(message); + await next(); + }); + + this.onMembersAdded(async (context: TurnContext, next: () => Promise): Promise => { + var newMembers: string = ''; + context.activity.membersAdded.forEach((account) => { + newMembers += account.id + ' '; + }); + const card = CardFactory.heroCard('Member Added', `${newMembers} joined ${context.activity.conversation.conversationType}.`); + const message = MessageFactory.attachment(card); + await context.sendActivity(message); + await next(); + }); + + this.onMembersRemoved(async (context: TurnContext, next: () => Promise): Promise => { + var removedMembers: string = ''; + context.activity.membersRemoved.forEach((account) => { + removedMembers += account.id + ' '; + }); + const card = CardFactory.heroCard('Member Removed', `${removedMembers} removed from ${context.activity.conversation.conversationType}.`); + const message = MessageFactory.attachment(card); + await context.sendActivity(message); + await next(); + }); + } + + protected async handleTeamsMessagingExtensionFetchTask(context: TurnContext, action: MessagingExtensionAction): Promise { + const response = AdaptiveCardHelper.createTaskModuleAdaptiveCardResponse(); + return response; + } + + protected async handleTeamsMessagingExtensionSubmitAction(context: TurnContext, action: MessagingExtensionAction): Promise { + const submittedData = action.data as SubmitExampleData; + const adaptiveCard = AdaptiveCardHelper.toAdaptiveCardAttachment(submittedData); + const response = CardResponseHelpers.toMessagingExtensionBotMessagePreviewResponse(adaptiveCard); + return response; + } + + protected async handleTeamsMessagingExtensionBotMessagePreviewEdit(context: TurnContext, action: MessagingExtensionAction): Promise { + const submitData = AdaptiveCardHelper.toSubmitExampleData(action); + const response = AdaptiveCardHelper.createTaskModuleAdaptiveCardResponse( + submitData.Question, + (submitData.MultiSelect.toLowerCase() === 'true'), + submitData.Option1, + submitData.Option2, + submitData.Option3); + return response; + } + + protected async handleTeamsMessagingExtensionBotMessagePreviewSend(context: TurnContext, action: MessagingExtensionAction): Promise { + const submitData: SubmitExampleData = AdaptiveCardHelper.toSubmitExampleData(action); + const adaptiveCard: Attachment = AdaptiveCardHelper.toAdaptiveCardAttachment(submitData); + + const responseActivity = {type: 'message', attachments: [adaptiveCard] } as Activity; + + // try { + // // Send to channel where messaging extension invoked. + // let results = await this.teamsCreateConversation(context, context.activity.channelData.channel.id, responseActivity); + // } catch(ex) { + // console.error('ERROR Sending to Channel:'); + // } + + try { + // Send card to "General" channel. + //const teamDetails: TeamDetails = await TeamsInfo.getTeamDetails(context); + const channelId = teamsGetChannelId(context.activity); + const adapter = context.adapter; + const connectorClient = adapter.createConnectorClient(context.activity.serviceUrl); + let results = await connectorClient.conversations.createConversation( + { + isGroup: true, + channelData: {channel: { id: channelId } as ChannelInfo } as TeamsChannelData, + activity: responseActivity + } as ConversationParameters); + } catch(ex) { + console.error('ERROR Sending to General channel:' + JSON.stringify(ex)); + } + + // Send card to compose box for the current user. + const response = CardResponseHelpers.toComposeExtensionResultResponse(adaptiveCard); + return response; + } + + protected async handleTeamsMessagingExtensionCardButtonClicked(context: TurnContext, obj) { + const reply = MessageFactory.text('onTeamsMessagingExtensionCardButtonClicked Value: ' + JSON.stringify(context.activity.value)); + await context.sendActivity(reply); + } + + protected async handleTeamsFileConsentAccept(context: TurnContext, fileConsentCardResponse: FileConsentCardResponse): Promise { + try { + await this.sendFile(fileConsentCardResponse); + await this.fileUploadCompleted(context, fileConsentCardResponse); + } + catch (err) { + await this.fileUploadFailed(context, err.toString()); + } + } + + protected async handleTeamsFileConsentDecline(context: TurnContext, fileConsentCardResponse: FileConsentCardResponse): Promise { + let reply = this.createReply(context.activity); + reply.textFormat = "xml"; + reply.text = `Declined. We won't upload file ${fileConsentCardResponse.context["filename"]}.`; + await context.sendActivity(reply); + } + + protected async handleTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { + var reply = MessageFactory.text("handleTeamsTaskModuleFetchAsync TaskModuleRequest" + JSON.stringify(taskModuleRequest)); + await context.sendActivity(reply); + + return { + task: { + type: "continue", + value: { + card: this.getTaskModuleAdaptiveCard(), + height: 200, + width: 400, + title: "Adaptive Card: Inputs", + } as TaskModuleTaskInfo, + } as TaskModuleContinueResponse + } as TaskModuleResponse; + } + + protected async handleTeamsTaskModuleSubmit(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { + var reply = MessageFactory.text("handleTeamsTaskModuleFetchAsync Value: " + JSON.stringify(taskModuleRequest)); + await context.sendActivity(reply); + + return { + task: { + type: "message", + value: "Thanks!", + } as TaskModuleMessageResponse + } as TaskModuleResponse; + } + + protected async handleTeamsCardActionInvoke(context: TurnContext): Promise { + await context.sendActivity(MessageFactory.text(`handleTeamsCardActionInvoke value: ${JSON.stringify(context.activity.value)}`)); + return { status: 200 } as InvokeResponse; + } + + protected async onReactionsAddedActivity(reactionsAdded: MessageReaction[], context: TurnContext): Promise { + for (var i = 0, len = reactionsAdded.length; i < len; i++) { + var activity = await this._log.find(context.activity.replyToId); + if (activity == null) { + // If we had sent the message from the error handler we wouldn't have recorded the Activity Id and so we shouldn't expect to see it in the log. + await this.sendMessageAndLogActivityId(context, `Activity ${context.activity.replyToId} not found in the log.`); + } + else { + await this.sendMessageAndLogActivityId(context, `You added '${reactionsAdded[i].type}' regarding '${activity.text}'`); + } + }; + + return; + } + + protected async onReactionsRemovedActivity(reactionsAdded: MessageReaction[], context: TurnContext): Promise { + for (var i = 0, len = reactionsAdded.length; i < len; i++) { + // The ReplyToId property of the inbound MessageReaction Activity will correspond to a Message Activity that was previously sent from this bot. + var activity = await this._log.find(context.activity.replyToId); + if (activity == null) { + // If we had sent the message from the error handler we wouldn't have recorded the Activity Id and so we shouldn't expect to see it in the log. + await this.sendMessageAndLogActivityId(context, `Activity ${context.activity.replyToId} not found in the log.`); + } + else { + await this.sendMessageAndLogActivityId(context, `You removed '${reactionsAdded[i].type}' regarding '${activity.text}'`); + } + }; + + return; + } + + protected async handleTeamsO365ConnectorCardAction(context: TurnContext, query: O365ConnectorCardActionQuery): Promise { + await context.sendActivity(MessageFactory.text(`O365ConnectorCardActionQuery event value: ${JSON.stringify(query)}`)); + } + + + protected async handleTeamsMessagingExtensionQuery(context: TurnContext, query: MessagingExtensionQuery): Promise{ + const searchQuery = query.parameters[0].value; + const composeExtension = this.createMessagingExtensionResult([ + this.createSearchResultAttachment(searchQuery), + this.createDummySearchResultAttachment(), + this.createSelectItemsResultAttachment(searchQuery) + ]); + + return { + composeExtension: composeExtension + }; + } + + protected async handleTeamsMessagingExtensionSelectItem(context: TurnContext, query: any): Promise { + const searchQuery = query.query; + const bfLogo = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU"; + const card = CardFactory.heroCard('You selected a search result!', `You searched for "${searchQuery}"`, [bfLogo]); + + return { + composeExtension: this.createMessagingExtensionResult([card]) + }; + } + + protected async handleTeamsAppBasedLinkQuery(context: TurnContext, query: AppBasedLinkQuery): Promise{ + console.log("HANDLETEAMSAPPBASEDLINKQUERY\nCONTEXT:\n" + JSON.stringify(context) + '\nQUERY:\n' + JSON.stringify(query)); + + const accessor = this.userState.createProperty<{ useHeroCard: boolean }>(RICH_CARD_PROPERTY); + const config = await accessor.get(context, { useHeroCard: true }); + + const url = query.url; + const cardText = `You entered "${url}"`; + let composeExtensionResponse: MessagingExtensionResponse; + + const bfLogo = 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU'; + const button = { type: 'openUrl', title: 'Click for more Information', value: "https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/bots/bots-overview" }; + + if (config.useHeroCard) { + const heroCard = CardFactory.heroCard('HeroCard for Link Unfurling:', cardText, [bfLogo], [button]); + const preview = CardFactory.heroCard('HeroCard for Link Unfurling:', cardText, [bfLogo]); + + composeExtensionResponse = { + composeExtension: { + type: 'result', + attachmentLayout: 'list', + attachments: [ + { ...heroCard, preview } + ] + } + } + } else { + const thumbnailCard = CardFactory.thumbnailCard('ThumbnailCard for Link Unfurling:', cardText, [bfLogo], [button]); + const preview = CardFactory.thumbnailCard('ThumbnailCard for Link Unfurling:', cardText, [bfLogo]); + + composeExtensionResponse = { + composeExtension: { + type: 'result', + attachmentLayout: 'list', + attachments: [ + { ...thumbnailCard, preview } + ] + } + } + } + + return composeExtensionResponse; + } + + protected async handleTeamsMessagingExtensionConfigurationQuerySettingUrl(context: TurnContext, query: MessagingExtensionQuery){ + + return + { + composeExtension: { + type: 'config', + suggestedActions: { + actions: [ + { + type: ActionTypes.OpenUrl, + title: 'Config', + value: process.env.host + '/composeExtensionSettings.html', + }, + ] + } + } + } + } + + protected async handleTeamsMessagingExtensionConfigurationSetting(context: TurnContext, settings: MessagingExtensionQuery){ + // This event is fired when the settings page is submitted + const accessor = this.userState.createProperty<{ useHeroCard: boolean }>(RICH_CARD_PROPERTY); + const config = await accessor.get(context, { useHeroCard: true }); + + if (settings.state === 'hero') { + config.useHeroCard = true; + } + else if (settings.state === 'thumbnail') { + config.useHeroCard = false; + } + else { + await context.sendActivity(`handleTeamsMessagingExtensionSetting event fired with ${ JSON.stringify(settings) }`); + } + + // We should save it after we send the message back to Teams. + await this.userState.saveChanges(context); + } + + private async sendO365CardAttachment(context: TurnContext): Promise { + const card = CardFactory.o365ConnectorCard({ + "title": "card title", + "text": "card text", + "summary": "O365 card summary", + "themeColor": "#E67A9E", + "sections": [ + { + "title": "**section title**", + "text": "section text", + "activityTitle": "activity title", + "activitySubtitle": "activity subtitle", + "activityText": "activity text", + "activityImage": "http://connectorsdemo.azurewebsites.net/images/MSC12_Oscar_002.jpg", + "activityImageType": "avatar", + "markdown": true, + "facts": [ + { + "name": "Fact name 1", + "value": "Fact value 1" + }, + { + "name": "Fact name 2", + "value": "Fact value 2" + } + ], + "images": [ + { + "image": "http://connectorsdemo.azurewebsites.net/images/MicrosoftSurface_024_Cafe_OH-06315_VS_R1c.jpg", + "title": "image 1" + }, + { + "image": "http://connectorsdemo.azurewebsites.net/images/WIN12_Scene_01.jpg", + "title": "image 2" + }, + { + "image": "http://connectorsdemo.azurewebsites.net/images/WIN12_Anthony_02.jpg", + "title": "image 3" + } + ], + "potentialAction": null + } + ], + "potentialAction": [ + { + "@type": "ActionCard", + "inputs": [ + { + "@type": "multichoiceInput", + "choices": [ + { + "display": "Choice 1", + "value": "1" + }, + { + "display": "Choice 2", + "value": "2" + }, + { + "display": "Choice 3", + "value": "3" + } + ], + "style": "expanded", + "isMultiSelect": true, + "id": "list-1", + "isRequired": true, + "title": "Pick multiple options", + "value": null + }, + { + "@type": "multichoiceInput", + "choices": [ + { + "display": "Choice 4", + "value": "4" + }, + { + "display": "Choice 5", + "value": "5" + }, + { + "display": "Choice 6", + "value": "6" + } + ], + "style": "compact", + "isMultiSelect": true, + "id": "list-2", + "isRequired": true, + "title": "Pick multiple options", + "value": null + }, + { + "@type": "multichoiceInput", + "choices": [ + { + "display": "Choice a", + "value": "a" + }, + { + "display": "Choice b", + "value": "b" + }, + { + "display": "Choice c", + "value": "c" + } + ], + "style": "expanded", + "isMultiSelect": false, + "id": "list-3", + "isRequired": false, + "title": "Pick an option", + "value": null + }, + { + "@type": "multichoiceInput", + "choices": [ + { + "display": "Choice x", + "value": "x" + }, + { + "display": "Choice y", + "value": "y" + }, + { + "display": "Choice z", + "value": "z" + } + ], + "style": "compact", + "isMultiSelect": false, + "id": "list-4", + "isRequired": false, + "title": "Pick an option", + "value": null + } + ], + "actions": [ + { + "@type": "HttpPOST", + "body": "{\"text1\":\"{{text-1.value}}\", \"text2\":\"{{text-2.value}}\", \"text3\":\"{{text-3.value}}\", \"text4\":\"{{text-4.value}}\"}", + "name": "Send", + "@id": "card-1-btn-1" + } + ], + "name": "Multiple Choice", + "@id": "card-1" + }, + { + "@type": "ActionCard", + "inputs": [ + { + "@type": "textInput", + "isMultiline": true, + "maxLength": null, + "id": "text-1", + "isRequired": false, + "title": "multiline, no maxLength", + "value": null + }, + { + "@type": "textInput", + "isMultiline": false, + "maxLength": null, + "id": "text-2", + "isRequired": false, + "title": "single line, no maxLength", + "value": null + }, + { + "@type": "textInput", + "isMultiline": true, + "maxLength": 10.0, + "id": "text-3", + "isRequired": true, + "title": "multiline, max len = 10, isRequired", + "value": null + }, + { + "@type": "textInput", + "isMultiline": false, + "maxLength": 10.0, + "id": "text-4", + "isRequired": true, + "title": "single line, max len = 10, isRequired", + "value": null + } + ], + "actions": [ + { + "@type": "HttpPOST", + "body": "{\"text1\":\"{{text-1.value}}\", \"text2\":\"{{text-2.value}}\", \"text3\":\"{{text-3.value}}\", \"text4\":\"{{text-4.value}}\"}", + "name": "Send", + "@id": "card-2-btn-1" + } + ], + "name": "Text Input", + "@id": "card-2" + }, + { + "@type": "ActionCard", + "inputs": [ + { + "@type": "dateInput", + "includeTime": true, + "id": "date-1", + "isRequired": true, + "title": "date with time", + "value": null + }, + { + "@type": "dateInput", + "includeTime": false, + "id": "date-2", + "isRequired": false, + "title": "date only", + "value": null + } + ], + "actions": [ + { + "@type": "HttpPOST", + "body": "{\"date1\":\"{{date-1.value}}\", \"date2\":\"{{date-2.value}}\"}", + "name": "Send", + "@id": "card-3-btn-1" + } + ], + "name": "Date Input", + "@id": "card-3" + }, + { + "@type": "ViewAction", + "target": ["http://microsoft.com"], + "name": "View Action", + "@id": null + }, + { + "@type": "OpenUri", + "targets": [ + { + "os": "default", + "uri": "http://microsoft.com" + }, + { + "os": "iOS", + "uri": "http://microsoft.com" + }, + { + "os": "android", + "uri": "http://microsoft.com" + }, + { + "os": "windows", + "uri": "http://microsoft.com" + } + ], + "name": "Open Uri", + "@id": "open-uri" + } + ] + }); + await context.sendActivity(MessageFactory.attachment(card)); + } + + private async handleBotCommand(text, context, next) : Promise { + switch (text.toLowerCase()) { + case "delete": + await this.handleDeleteActivities(context); + break; + case "update": + await this.handleUpdateActivities(context); + break; + + case '1': + await this.sendAdaptiveCard1(context); + break; + + case '2': + await this.sendAdaptiveCard2(context); + break; + + case '3': + await this.sendAdaptiveCard3(context); + break; + + case HeroCard.toLowerCase(): + await context.sendActivity(MessageFactory.attachment(this.getHeroCard())); + break; + case ThumbnailCard.toLowerCase(): + await context.sendActivity(MessageFactory.attachment(this.getThumbnailCard())); + break; + case ReceiptCard.toLowerCase(): + await context.sendActivity(MessageFactory.attachment(this.getReceiptCard())); + break; + case SigninCard.toLowerCase(): + await context.sendActivity(MessageFactory.attachment(this.getSigninCard())); + break; + case Carousel.toLowerCase(): + // NOTE: if cards are NOT the same height in a carousel, Teams will instead display as AttachmentLayoutTypes.List + await context.sendActivity(MessageFactory.carousel([this.getHeroCard(), this.getHeroCard(), this.getHeroCard()])); + break; + case List.toLowerCase(): + // NOTE: MessageFactory.Attachment with multiple attachments will default to AttachmentLayoutTypes.List + await context.sendActivity(MessageFactory.list([this.getHeroCard(), this.getHeroCard(), this.getHeroCard()])); + break; + case "o365": + await this.sendO365CardAttachment(context); + break; + case "file": + await this.sendFileCard(context); + break; + case "show members": + await this.showMembers(context); + break; + case "show channels": + await this.showChannels(context); + break; + case "show details": + await this.showDetails(context); + break; + case "task module": + await context.sendActivity(MessageFactory.attachment(this.getTaskModuleHeroCard())); + break; + default: + await this.sendMessageAndLogActivityId(context, text); + break; + } + } + + private getTaskModuleHeroCard() : Attachment { + return CardFactory.heroCard("Task Module Invocation from Hero Card", + "This is a hero card with a Task Module Action button. Click the button to show an Adaptive Card within a Task Module.", + null, // No images + [{type: "invoke", title:"Adaptive Card", value: {type:"task/fetch", data:"adaptivecard"} }] + ); + } + + private getTaskModuleAdaptiveCard(): Attachment { + return CardFactory.adaptiveCard({ + version: '1.0.0', + type: 'AdaptiveCard', + body: [ + { + type: 'TextBlock', + text: `Enter Text Here`, + }, + { + type: 'Input.Text', + id: 'usertext', + placeholder: 'add some text and submit', + IsMultiline: true, + } + ], + actions: [ + { + type: 'Action.Submit', + title: 'Submit', + } + ] + }); + } + + private async sendFile(fileConsentCardResponse: FileConsentCardResponse): Promise { + const request = require("request"); + const fs = require('fs'); + let context = fileConsentCardResponse.context; + let path = require('path'); + let filePath = path.join('files', context["filename"]); + let stats = fs.statSync(filePath); + let fileSizeInBytes = stats['size']; + fs.createReadStream(filePath).pipe(request.put(fileConsentCardResponse.uploadInfo.uploadUrl)); + } + + private async sendFileCard(context: TurnContext): Promise { + let filename = "teams-logo.png"; + let fs = require('fs'); + let path = require('path'); + let stats = fs.statSync(path.join('files', filename)); + let fileSizeInBytes = stats['size']; + + let fileContext = { + filename: filename + }; + + let attachment = { + content: { + description: 'This is the file I want to send you', + fileSizeInBytes: fileSizeInBytes, + acceptContext: fileContext, + declineContext: fileContext + }, + contentType: 'application/vnd.microsoft.teams.card.file.consent', + name: filename + } as Attachment; + + var replyActivity = this.createReply(context.activity); + replyActivity.attachments = [ attachment ]; + await context.sendActivity(replyActivity); + } + + private async fileUploadCompleted(context: TurnContext, fileConsentCardResponse: FileConsentCardResponse): Promise { + let fileUploadInfoName = fileConsentCardResponse.uploadInfo.name; + let downloadCard = { + uniqueId: fileConsentCardResponse.uploadInfo.uniqueId, + fileType: fileConsentCardResponse.uploadInfo.fileType, + }; + + let attachment = { + content: downloadCard, + contentType: 'application/vnd.microsoft.teams.card.file.info', + name: fileUploadInfoName, + contentUrl: fileConsentCardResponse.uploadInfo.contentUrl, + }; + + let reply = this.createReply(context.activity, `File uploaded. Your file ${fileUploadInfoName} is ready to download`); + reply.textFormat = 'xml'; + reply.attachments = [attachment]; + await context.sendActivity(reply); + } + + private async fileUploadFailed(context: TurnContext, error: string): Promise { + let reply = this.createReply(context.activity, `File upload failed. Error:
${error}
`); + reply.textFormat = 'xml'; + await context.sendActivity(reply); + } + + private createReply(activity, text = null, locale = null) : Activity { + return { + type: 'message', + from: { id: activity.recipient.id, name: activity.recipient.name }, + recipient: { id: activity.from.id, name: activity.from.name }, + replyToId: activity.id, + serviceUrl: activity.serviceUrl, + channelId: activity.channelId, + conversation: { isGroup: activity.conversation.isGroup, id: activity.conversation.id, name: activity.conversation.name }, + text: text || '', + locale: locale || activity.locale + } as Activity; + } + + private async sendMessageAndLogActivityId(context: TurnContext, text: string): Promise { + var replyActivity = MessageFactory.text(`You said '${text}'`); + var resourceResponse = await context.sendActivity(replyActivity); + await this.activityIds.push(resourceResponse.id); + await this._log.append(resourceResponse.id, replyActivity); + } + + private async sendAdaptiveCard1(context: TurnContext): Promise { + /* tslint:disable:quotemark object-literal-key-quotes */ + const card = CardFactory.adaptiveCard({ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "actions": [ + { + "data": { + "msteams": { + "type": "imBack", + "value": "text" + } + }, + "title": "imBack", + "type": "Action.Submit" + }, + { + "data": { + "msteams": { + "type": "messageBack", + "value": { "key": "value" } + } + }, + "title": "message back", + "type": "Action.Submit" + }, + { + "data": { + "msteams": { + "displayText": "display text message back", + "text": "text received by bots", + "type": "messageBack", + "value": { "key": "value" } + } + }, + "title": "message back local echo", + "type": "Action.Submit" + }, + { + "data": { + "msteams": { + "type": "invoke", + "value": { "key": "value" } + } + }, + "title": "invoke", + "type": "Action.Submit" + } + ], + "body": [ + { + "text": "Bot Builder actions", + "type": "TextBlock" + } + ], + "type": "AdaptiveCard", + "version": "1.0" + }); + /* tslint:enable:quotemark object-literal-key-quotes */ + await context.sendActivity(MessageFactory.attachment(card)); + } + + private async handleDeleteActivities(context): Promise { + for (const activityId of this.activityIds) { + await context.deleteActivity(activityId); + } + + this.activityIds = []; + } + + private async handleUpdateActivities(context: TurnContext): Promise { + for (const id of this.activityIds) { + await context.updateActivity({ id, text: context.activity.text, type: ActivityTypes.Message }); + } + } + + private async sendAdaptiveCard2(context: TurnContext): Promise { + /* tslint:disable:quotemark object-literal-key-quotes */ + const card = CardFactory.adaptiveCard({ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "actions": [ + { + "data": { + "msteams": { + "type": "invoke", + "value": { + "hiddenKey": "hidden value from task module launcher", + "type": "task/fetch" + } + } + }, + "title": "Launch Task Module", + "type": "Action.Submit" + } + ], + "body": [ + { + "text": "Task Module Adaptive Card", + "type": "TextBlock" + } + ], + "type": "AdaptiveCard", + "version": "1.0" + }); + /* tslint:enable:quotemark object-literal-key-quotes */ + await context.sendActivity(MessageFactory.attachment(card)); + } + + private async sendAdaptiveCard3(context: TurnContext): Promise { + /* tslint:disable:quotemark object-literal-key-quotes */ + const card = CardFactory.adaptiveCard({ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "actions": [ + { + "data": { + "key": "value" + }, + "title": "Action.Submit", + "type": "Action.Submit" + } + ], + "body": [ + { + "text": "Bot Builder actions", + "type": "TextBlock" + }, + { + "id": "x", + "type": "Input.Text" + } + ], + "type": "AdaptiveCard", + "version": "1.0" + }); + /* tslint:enable:quotemark object-literal-key-quotes */ + await context.sendActivity(MessageFactory.attachment(card)); + } + + private getHeroCard() { + return CardFactory.heroCard('BotFramework Hero Card', + 'Build and connect intelligent bots to interact with your users naturally wherever they are,' + + ' from text/sms to Skype, Slack, Office 365 mail and other popular services.', + ['https://sec.ch9.ms/ch9/7ff5/e07cfef0-aa3b-40bb-9baa-7c9ef8ff7ff5/buildreactionbotframework_960.jpg'], + [{ type: ActionTypes.OpenUrl, title: 'Get Started', value: 'https://docs.microsoft.com/bot-framework' }]); + } + + private getThumbnailCard() { + return CardFactory.thumbnailCard('BotFramework Thumbnail Card', + 'Build and connect intelligent bots to interact with your users naturally wherever they are,' + + ' from text/sms to Skype, Slack, Office 365 mail and other popular services.', + ['https://sec.ch9.ms/ch9/7ff5/e07cfef0-aa3b-40bb-9baa-7c9ef8ff7ff5/buildreactionbotframework_960.jpg'], + [{ type: ActionTypes.OpenUrl, title: 'Get Started', value: 'https://docs.microsoft.com/bot-framework' }]); + } + + private getReceiptCard() { + return CardFactory.receiptCard({ + buttons: [ + { + image: 'https://account.windowsazure.com/content/6.10.1.38-.8225.160809-1618/aux-pre/images/offer-icon-freetrial.png', + title: 'More information', + type: ActionTypes.OpenUrl, + value: 'https://azure.microsoft.com/en-us/pricing/' + } + ], + facts: [ + { key: 'Order Number', value: '1234' }, + { key: 'Payment Method', value: 'VISA 5555-****' } + ], + items: [ + { + image: { url: 'https://github.com/amido/azure-vector-icons/raw/master/renders/traffic-manager.png' }, + price: '$ 38.45', + quantity: '368', + subtitle: '', + tap: { title: '', type: '', value: null }, + text: '', + title: 'Data Transfer' + }, + { + image: { url: 'https://github.com/amido/azure-vector-icons/raw/master/renders/cloud-service.png' }, + price: '$ 45.00', + quantity: '720', + subtitle: '', + tap: { title: '', type: '', value: null }, + text: '', + title: 'App Service' + } + ], + tap: { title: '', type: '', value: null }, + tax: '$ 7.50', + title: 'John Doe', + total: '$ 90.95', + vat: '' + }); + } + + private getSigninCard() { + return CardFactory.signinCard('BotFramework Sign-in Card', 'https://login.microsoftonline.com/', 'Sign-in'); + } + + private getChoices() { + const actions = this.cardTypes.map((cardType) => ({ type: ActionTypes.MessageBack, title: cardType, text: cardType })) as CardAction[]; + return CardFactory.heroCard('Task Module Invocation from Hero Card', null, actions); + } + + private async showMembers(context: TurnContext): Promise { + let teamsChannelAccounts = await TeamsInfo.getMembers(context); + await context.sendActivity(MessageFactory.text(`Total of ${teamsChannelAccounts.length} members are currently in team`)); + let messages = teamsChannelAccounts.map(function(teamsChannelAccount) { + return `${teamsChannelAccount.aadObjectId} --> ${teamsChannelAccount.name} --> ${teamsChannelAccount.userPrincipalName}`; + }); + await this.sendInBatches(context, messages); + } + + private async showChannels(context: TurnContext): Promise { + let channels = await TeamsInfo.getTeamChannels(context); + await context.sendActivity(MessageFactory.text(`Total of ${channels.length} channels are currently in team`)); + let messages = channels.map(function(channel) { + return `${channel.id} --> ${channel.name ? channel.name : 'General'}`; + }); + await this.sendInBatches(context, messages); + } + + private async showDetails(context: TurnContext): Promise { + let teamDetails = await TeamsInfo.getTeamDetails(context); + await this.sendMessageAndLogActivityId(context, `The team name is ${teamDetails.name}. The team ID is ${teamDetails.id}. The AAD GroupID is ${teamDetails.aadGroupId}.`); + } + + private async sendInBatches(context: TurnContext, messages: string[]): Promise { + let batch: string[] = []; + messages.forEach(async (msg: string) => { + batch.push(msg); + if (batch.length == 10) { + await this.sendMessageAndLogActivityId(context, batch.join('
')); + batch = []; + } + }); + + if (batch.length > 0) { + await this.sendMessageAndLogActivityId(context, batch.join('
')); + } + } + + private createMessagingExtensionResult(attachments: Attachment[]) : MessagingExtensionResult { + return { + type: "result", + attachmentLayout: "list", + attachments: attachments + }; + } + + private createSearchResultAttachment(searchQuery: string) : MessagingExtensionAttachment { + const cardText = `You said \"${searchQuery}\"`; + const bfLogo = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU"; + + const button = { + type: "openUrl", + title: "Click for more Information", + value: "https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/bots/bots-overview" + }; + + const heroCard = CardFactory.heroCard("You searched for:", cardText, [bfLogo], [button]); + const preview = CardFactory.heroCard("You searched for:", cardText, [bfLogo]); + + return { + contentType: CardFactory.contentTypes.heroCard, + content: heroCard, + preview: preview + }; + } + + private createDummySearchResultAttachment() : MessagingExtensionAttachment { + const cardText = "https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/bots/bots-overview"; + const bfLogo = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU"; + + const button = { + type: "openUrl", + title: "Click for more Information", + value: "https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/bots/bots-overview" + }; + + const heroCard = CardFactory.heroCard("Learn more about Teams:", cardText, [bfLogo], [button]); + const preview = CardFactory.heroCard("Learn more about Teams:", cardText, [bfLogo]); + + return { + contentType: CardFactory.contentTypes.heroCard, + content: heroCard, + preview: preview + }; + } + + private createSelectItemsResultAttachment(searchQuery: string): MessagingExtensionAttachment { + const bfLogo = 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU'; + const cardText = `You said: "${searchQuery}"`; + const heroCard = CardFactory.heroCard(cardText, cardText, [bfLogo]); + + const selectItemTap = { + type: "invoke", + value: { query: searchQuery } + }; + + const preview = CardFactory.heroCard(cardText, cardText, [bfLogo], null, null); + const card: Partial = preview.content; + card.tap = selectItemTap; + + return { + contentType: CardFactory.contentTypes.heroCard, + content: heroCard, + preview: preview, + }; + } + + private async teamsCreateConversation(context: TurnContext, teamsChannelId: string, message: Partial): Promise<[ConversationReference, string]> { + if (!teamsChannelId) { + throw new Error('Missing valid teamsChannelId argument'); + } + + if (!message) { + throw new Error('Missing valid message argument'); + } + + const conversationParameters = { + isGroup: true, + channelData: { + channel: { + id: teamsChannelId + } + }, + + activity: message, + }; + + const adapter = context.adapter; + const connectorClient = adapter.createConnectorClient(context.activity.serviceUrl); + + // This call does NOT send the outbound Activity is not being sent through the middleware stack. + const conversationResourceResponse: ConversationResourceResponse = await connectorClient.conversations.createConversation(conversationParameters); + const conversationReference = TurnContext.getConversationReference(context.activity); + conversationReference.conversation.id = conversationResourceResponse.id; + return [conversationReference, conversationResourceResponse.activityId]; + } +} diff --git a/libraries/botbuilder/tests/teams/integrationBot/src/nock-helper/nock-helper-play.js b/libraries/botbuilder/tests/teams/integrationBot/src/nock-helper/nock-helper-play.js new file mode 100644 index 0000000000..fdacf3d279 --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/src/nock-helper/nock-helper-play.js @@ -0,0 +1,259 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Implements TEST_MODE=PLAY +// +// Loads all the recordings (json files) in the ./recordings directory +// and drives traffic to the bot. It intercepts all external service calls +// (captured in the recordings) and responds based on what's in the recording. +// +// Validates the traffic matches. + +/* eslint-disable @typescript-eslint/no-var-requires */ + +var nockhelper = require('./nock-helper'); +var nock = require('nock'); +var httpMocks = require('node-mocks-http'); + +// Dynamically sets up interceptors based on recorded reply. +// This is called preceding an invocation of a bot request. +// It sets up all anticipated "replies" (http calls to external service(s)) +// that are made during the bot request. +// +// We could explore making this more mechanical (ie, just compare raw stringify) +// later. +function setupInterceptorReplies(replies) { + if (replies == null || replies.length <= 0) { + return null; + } + var response = []; + replies.forEach((item) => { + var code = ``; + itemScopeNoPort = item.scope.substring(0, item.scope.lastIndexOf(':')); + code += `return nock('${ item.scope }')\n`; + // Uncomment to debug matching logic. + //code += ` .log(console.log)\n`; + + + // Set up interceptor with some validation on properties. + code += ` .matchHeader('content-type', '${ item.reqheaders['content-type'] }')\n`; + if ('content-length' in item.reqheaders) { + code += ` .matchHeader('accept', '${ item.reqheaders.accept }')\n`; + } + + // Prepare URL + code = prepareUrl(item, code); + + if (item.method.toLowerCase() == 'put') { + code += `,\n function(body) {\n`; + // Validate contents + var recordedBody = Object.getOwnPropertyNames(item.body); + + //console.log('ALL PROPERTIES: ' + JSON.stringify(recordedBody, null, 1)); + var excludedProperties = ['serviceUrl', 'replyToId', 'id', 'text']; // Filter proxy-altered properties + recordedBody.forEach(function(prop) { + if (excludedProperties.includes(prop)) { + return; + } + + if (typeof item.body[prop] == 'string') { + //console.log('ALL PROPERTIES: PROCESSING: ' + prop + ' - \n' + item.body[prop]); + code += ` console.log('PROCESSING ${ prop }.');\n`; + code += ` if (${ JSON.stringify(item.body[prop]) } != body.${ prop }) {\n`; + code += ` console.error('Body ${ prop } does not match ${ JSON.stringify(item.body[prop]) } != ' + JSON.stringify(body.${ prop }));\n`; + code += ` return false;\n`; + code += ` }\n`; + } + }); + code += ` console.log('DONE PROCESSING PROPERTIES!');\n`; + code += ` return true;\n`; + code += ` })\n`; + code += ` .reply(${ item.status }, ${ JSON.stringify(item.response) }, ${ formatHeaders(item.rawHeaders) });\n`; + } else if (item.method.toLowerCase() == 'post') { + code += `,\n function(body) {\n`; + // code += ` console.log('INSIDE BODY EVALUATION!!');\n`; + + // Validate body type + if (item.body.hasOwnProperty('type')) { + code += ` if ('${ item.body.type }' != body.type) {\n`; + code += ` console.log('Body type does not match ${ item.body.type } != ' + body.type);\n`; + code += ` return false;\n`; + code += ` }\n`; + } + // Validate Activity + if (item.body.hasOwnProperty('activity')) { + code += ` if (${ item.body.activity.hasOwnProperty('type') } && '${ item.body.activity.type }' != body.activity.type) {\n`; + code += ` console.log('Activity type does not match ${ item.body.activity.type } != ' + body.activity.type);\n`; + code += ` return false;\n`; + code += ` }\n`; + // Validate Activity attachments + if (item.body.activity.hasOwnProperty('attachments')) { + code += ` if ('${ JSON.stringify(item.body.activity.attachments) }' != JSON.stringify(body.activity.attachments)) {\n`; + code += ` console.log('Activity attachments do not match ${ JSON.stringify(item.body.activity.attachments) } != ' + JSON.stringify(body.activity.attachments));\n`; + code += ` return false;\n`; + code += ` }\n`; + } + } + + // Validate ChannelData + if (item.body.hasOwnProperty('channelData') && item.body.channelData.hasOwnProperty('channel') + && item.body.channelData.channel.hasOwnProperty('id')) { + code += ` if ('${ item.body.channelData.channel.id }' != body.channelData.channel.id) {\n`; + code += ` console.error('Channel data/channel id does not match ${ JSON.stringify(item.body.channelData) } != ' + JSON.stringify(body.channelData));\n`; + code += ` return false;\n`; + code += ` }\n`; + } + + // Validate from.name + if (item.body.hasOwnProperty('from') && item.body.from.hasOwnProperty('name')) { + code += ` if ('${ item.body.from.name }' != body.from.name) {\n`; + code += ` console.error('From name does not match');\n`; + code += ` return false;\n`; + code += ` }\n`; + } + code += ` return true;\n`; + code += ` })\n`; + code += ` .reply(${ item.status }, ${ JSON.stringify(item.response) }, ${ formatHeaders(item.rawHeaders) });\n`; + } + else { + code += `)\n`; + code += ` .reply(${ item.status }, ${ JSON.stringify(item.response) }, ${ formatHeaders(item.rawHeaders) })\n`; + } + + // Uncomment to see generated Interceptor code. + if (item.method.toLowerCase() == 'put') { + console.log('NOCK INTERCEPTOR CODE (replies count = ' + replies.length + '):\n' + code); + } + var interceptor = null; + try { + interceptor = new Function('nock', code); + } + catch(ex) { + console.error('NOCK INTERCEPTOR CODE (replies count = ' + replies.length + '):\n' + code); + console.error(JSON.stringify(ex, null, 1)); + + throw ex; + } + response.push(interceptor(nock)); + }); + return response; +} + +// Process invoking bot locally. +// First sets up all the anticipated external calls (interceptors) +// and then calls the adapter to invoke the bot. +async function playRecordings(activityBundle, adapter, myBot) { + // eslint-disable-next-line @typescript-eslint/camelcase + //await sleep(1000); + const activityRecordingPth = activityBundle.activityPath; + console.log('****PLAY RECORDINGS - activity contains ' + activityBundle.replies.length + ' replies. - ' + activityRecordingPth); + nock_interceptors = setupInterceptorReplies(activityBundle.replies); + const activity = activityBundle.activity; + console.error('CURRENT INTERCEPTORS : ' + JSON.stringify(nock.pendingMocks(), null, 1)); + + // Call bot + var request = httpMocks.createRequest({ + method: activity.method, + url: activity.url, + headers: activity.headers, + body: activity.body, + }); + var response = httpMocks.createResponse(); + await sleep(1000); + var adapt = new nockhelper.AdapterDisableAuth(); + await adapt.processActivity(request, response, async (context) => { + // Route to main dialog. + await myBot.run(context); + + }); + // Tear down interceptors + + + await sleep(5000); + if(!nock.isDone()) { + + console.error('NOT ALL NOCK INTERCEPTORS USED : ' + JSON.stringify(nock.pendingMocks())); + } + nock.cleanAll(); + console.log('****PLAY RECORDINGS - complete! - ' + activityRecordingPth); + +} + + + +// Parse all recordings (bundled up in activity/replies) +// and the play each bot invocation. +exports.processRecordings = async function(testName, adapter = null, myBot = null) { + const activityBundles = nockhelper.parseActivityBundles(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + activityBundles.forEach(async (activityBundle) => { + await playRecordings(activityBundle, adapter, myBot, activityBundle.activityPath); + }); + await sleep(10000); + return 'Completed processing recordings.'; + +}; + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function prepareUrl(item, code) { + const method = item.method.toLowerCase(); + switch(method) { + case 'post': + // Handle post token + // ie, `/amer/v3/conversations/../1569442142365` + // Last token (1569442142365) is variable, must be pulled off. + const lastToken = item.path.substring(item.path.lastIndexOf('/')); + const truncateLastToken = (/^\d+$/.test(lastToken) && lastToken.length == 13); + const pathNoLastElement = truncateLastToken ? item.path.substring(0, item.path.lastIndexOf('/')) : item.path; + + if (truncateLastToken) { + code += ` .${ method }(uri => uri.includes('${ pathNoLastElement }'),\n`; + } + else { + code += ` .${ method }('${ pathNoLastElement }'`; + } + break; + + case 'get': + code += ` .${ method }('${ item.path }'`; + break; + + case 'put': + // /amer/v3/conversations/19%3A097e6717fd7245bdbeba6baa13840db8%40thread.skype%3Bmessageid%3D1571537152320/activities/1%3A1Zu8wK3u9-8LohH10diLSicoeHNSBwMtz2VNKrDCqPQc + const levels = item.path.split('/'); + var path = item.path; + if (levels.length == 7 && levels[5] == 'activities') { + //levels[4] = '*'; + //path = RegExp('\/amer\/v3\/conversations\/.*\/activities\/.*$', 'g'); + //path = /amer\/v3\/conversations\/.*\/activities\/.*$/; + } + code += ` .${ method }(/amer\\/v3\\/conversations\\/.*\\/activities\\/.*$/g`; + break; + + default: + throw new Exception('ERROR unsupported HTTP verb : ' + method); + } + + return code; + +} +// Format headers from recordings +function formatHeaders(rawHeaders) { + var headers = '{'; + for (let i=0; i< rawHeaders.length-2; i=i+2) { + if (rawHeaders[i] == 'Content-Length') { + continue; + } + headers += '"' + rawHeaders[i] + '"' + ':' + '"' + rawHeaders[i+1] + '"'; + if (i < rawHeaders.length - 4) { + headers += ','; + } + } + headers += '}'; + return headers; +} + diff --git a/libraries/botbuilder/tests/teams/integrationBot/src/nock-helper/nock-helper-proxyhost.js b/libraries/botbuilder/tests/teams/integrationBot/src/nock-helper/nock-helper-proxyhost.js new file mode 100644 index 0000000000..9c4f68f46e --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/src/nock-helper/nock-helper-proxyhost.js @@ -0,0 +1,299 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Implements TEST_MODE=PROXY_HOST +// +// Emulates the Teams Server to initiate traffic to a remote bot. +// Since traffic is normally initiated from the Teams->Server to the +// bot somethign is required to begin the bot traffic. This is done by +// exposing a GET /api/runtests REST call which synchronously runs all +// the tests. +// +// This proxy host simply loads the JSON recordings and invokes the same +// traffic pattern in order. Also validates the traffic matches. +// +// The proxy also exposes all the endpoints that the Teams Server does +// to capture and respond to replies from the Bot with the recorded data. +// +// Note: Authentication not supported. +// + +/* eslint-disable @typescript-eslint/no-var-requires */ + +const assert = require('assert'); +var restify = require('restify'); +var nockhelper = require('./nock-helper'); + +const clientSessions = {}; + +// Start the http server and listen for test run requests. +// Also expose all endpoints to simulate the Teams server. +exports.proxyRecordings = function() { + const server = restify.createServer(); + server.use(restify.plugins.queryParser()); + server.use(restify.plugins.bodyParser()); + server.listen(3979, '0.0.0.0', () => { + console.log(`\n${ server.name } listening to ${ server.url }`); + console.log(`\nHosting recordings.\n To trigger begin of testing, hit ${ server.url }/api/runtests.`); + console.log(` In another console, start test with TEST_MODE=PLAY_SERVER, PLAY_HOST=.`); + console.log(' You may have collision on https port. To mitigate that, ngrok http -host-header=rewrite 3979'); + }); + + // Listen for incoming requests. + // GET /api/runtests?TestName=link-unfurl + server.get({ + path: '/api/runtests', + contentType: 'application/json' + }, + async (req, res, next) => { + console.log('RECEIVED NEW TEST RUN FROM: ' + req.connection.remoteAddress); + const testName = req.params.TestName; + const remoteAddress = req.connection.remoteAddress; + + // Only one test running per client host. + if (remoteAddress in clientSessions) { + const errorMsg = `FAIL: Session already running from client ${ removeAddress }.`; + console.log(errorMsg); + + // Send back to client. + res.send(errorMsg); + return next(false); + } + + // eslint-disable-next-line @typescript-eslint/no-use-before-define + const session = new ProxySession(testName, remoteAddress); + clientSessions[req.connection.remoteAddress] = session; + await processHostRecordings(session); + const response = { id: '1'}; + res.send(response); + + delete clientSessions[req.connection.remoteAddress]; + console.log('---------------------------'); + console.log('TEST RUN SESSION COMPLETED!'); + console.log('---------------------------'); + return next(false); + }); + + server.post({ + path: '/v3/conversations', + contentType: 'application/json' + }, + (req, res, next) => { + const session = clientSessions[req.connection.remoteAddress]; + const response = session.response(); + processPostReply(req, res, session); + res.send(response); + return next(false); + }); + + server.post({ + path: '/v3/conversations/*', + contentType: 'application/json; charset=utf-8' + }, + (req, res, next) => { + const session = clientSessions[req.connection.remoteAddress]; + const response = session.response(); + processPostReply(req, res, session, true); + res.send(response); + return next(false); + }); + + server.get({ + path: '/v3/teams/*', + contentType: 'application/json; charset=utf-8' + }, + (req, res, next) => { + const session = clientSessions[req.connection.remoteAddress]; + const response = session.response(); + processGetReply(req, res, session); + res.send(response); + return next(false); + }); + + server.get({ + path: '/v3/conversations/*', + contentType: 'application/json; charset=utf-8' + }, + (req, res, next) => { + const session = clientSessions[req.connection.remoteAddress]; + const response = session.response(); + processGetConversationReply(req, res, session); + res.send(response); + return next(false); + }); + + + server.put({ + path: '/v3/conversations/*', + contentType: 'application/json; charset=utf-8' + }, + (req, res, next) => { + const session = clientSessions[req.connection.remoteAddress]; + const response = session.response(); + processPutReply(req, res, session, true); + res.send(response); + return next(false); + }); + +}; + +// Represents an active test session that's executing. +// Stores how far we've progressed in the recordings. +class ProxySession { + constructor(testName, clientAddress) { + this.testName = testName; + this.clientAddress = clientAddress; + this.recordedActivities = null; + this.activityIndex = 0; + this.replyIndex = 0; + this.startDate = Date(); + } + response() { + const recordedActivity = this.recordedActivities[this.activityIndex]; + const reply = recordedActivity.replies[this.replyIndex]; + return reply.response; + } + reply() { + const recordedActivity = this.recordedActivities[this.activityIndex]; + const reply = recordedActivity.replies[this.replyIndex]; + return reply; + } + activity() { + return this.recordedActivities[this.activityIndex]; + } +} + +function processPostReply(req, res, clientSession, session = false) { + + console.log(`Processing reply ${ clientSession.replyIndex + 1 } of ${ clientSession.activity().replies.length }`); + const reply = clientSession.reply(); + + if (session) { + // Validating a session requires a little bit more finesse + validatePostReply(req, reply); + } + else { + const incomingReply = req.body; + assert(JSON.stringify(incomingReply), JSON.stringify(reply)); + } + + + // Increment for next reply + clientSession.replyIndex = clientSession.replyIndex + 1; +} + +function processGetReply(req, res, clientSession) { + + console.log(`Processing reply ${ clientSession.replyIndex + 1 } of ${ clientSession.activity().replies.length }`); + const reply = clientSession.reply(); + + // Not much to validate here + assert(reply.method.toLowerCase() == 'get'); + + // Increment for next reply + clientSession.replyIndex = clientSession.replyIndex + 1; +} + +function processGetConversationReply(req, res, clientSession) { + const recordedActivity = clientSession.recordedActivities[clientSession.activityIndex]; + console.log(`Processing reply ${ clientSession.replyIndex + 1 } of ${ recordedActivity.replies.length }`); + const reply = recordedActivity.replies[clientSession.replyIndex]; + + // Not much to validate here + assert(reply.method.toLowerCase() == 'get'); + + + // Increment for next reply + clientSession.replyIndex = clientSession.replyIndex + 1; +} + +function processPutReply(req, res, clientSession) { + const recordedActivity = clientSession.recordedActivities[clientSession.activityIndex]; + console.log(`Processing reply ${ clientSession.replyIndex + 1 } of ${ recordedActivity.replies.length }`); + const reply = recordedActivity.replies[clientSession.replyIndex]; + + // Validate contents + var recordedBody = Object.getOwnPropertyNames(reply.body); + var excludedProperties = ['id', 'serviceUrl']; // Filter proxy-altered properties + for (prop in recordedBody) { + if (prop in excludedProperties) { + continue; + } + assert(JSON.stringify(req.body[prop]) == JSON.stringify(reply.body[prop])); + } + + // Increment for next reply + clientSession.replyIndex = clientSession.replyIndex + 1; +} + + +// Validate "reply" (which is the incoming request - confusing!) +function validatePostReply(req, replyFromRecording) { + // console.log('VALIDATE REPLY: INCOMING REPLY: ' + JSON.stringify(req.body, null, 1) ); + // console.log('VALIDATE REPLY: RECORDING: ' + JSON.stringify(replyFromRecording.body, null, 1) ); + const reply = req.body; + const recordedReply = replyFromRecording.body; + assert(reply.type == recordedReply.type); + assert(reply.channelId == recordedReply.channelId); + assert(reply.from.id == recordedReply.from.id); + assert(reply.from.name == recordedReply.from.name); + assert(reply.conversation.isGroup == recordedReply.conversation.isGroup); + assert(reply.conversation.conversationType == recordedReply.conversation.conversationType); + assert(reply.conversation.id == recordedReply.conversation.id); + assert(reply.conversation.tenantId == recordedReply.conversation.tenantId); + assert(reply.recipient.id == recordedReply.recipient.id); + assert(reply.recipient.name == recordedReply.recipient.name); + assert(reply.recipient.aadObjectId == recordedReply.recipient.aadObjectId); + if (reply.text != recordedReply.text) { + console.log('ERROR: Text does not match:'); + console.log(' EXPECTED:' + recordedReply.text); + console.log(' OBSERVED:' + reply.text); + } + assert(reply.text == recordedReply.text); + assert(reply.inputHint == recordedReply.inputHint); + assert(reply.replyToId == recordedReply.replyToId); +} + + + + +async function processHostRecordings(clientSession) { + const recordedActivities = nockhelper.parseActivityBundles(); + clientSession.recordedActivities = recordedActivities; + + await sleep(1000); // Give client time to set up. + + for (const recordedActivity of recordedActivities) { + console.log(`\n - Processing Activity (${ clientSession.clientAddress }) # ${ clientSession.activityIndex+1 } of ${ recordedActivities.length } ---------\n`); + + const requestUrl = 'http://' + clientSession.clientAddress + ':3978/api/messages'; + console.log('Invoking: ' + requestUrl); + + // Modify the service URL to point at the proxy + const tweakedActivity = recordedActivity.activity.body; + tweakedActivity.serviceUrl = 'http://localhost:3979'; + + // Reset reply index for this request + clientSession.replyIndex = 0; + + res = await fetch(requestUrl, { + method: 'POST', + headers: {'content-type': 'application/json'}, + body: JSON.stringify(tweakedActivity), + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .then(async response => { + // Bundle complete - make sure we processed all the replies. + assert(recordedActivity.replies.length == clientSession.replyIndex); + console.log('SUCCESS: Activity Request and replies validated.' ); + }) + .catch(err => console.log(`FAIL: ${ err }`, null, 1)); + + // Bump to process the next activity in the recorded activities. + clientSession.activityIndex = clientSession.activityIndex + 1; + } +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/libraries/botbuilder/tests/teams/integrationBot/src/nock-helper/nock-helper-proxyplay.js b/libraries/botbuilder/tests/teams/integrationBot/src/nock-helper/nock-helper-proxyplay.js new file mode 100644 index 0000000000..e908a33f0c --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/src/nock-helper/nock-helper-proxyplay.js @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Implements TEST_MODE=PROXY_PLAY +// +// Initiates a REST call against the proxy which runs all the tests. This +// essentially is just the plain bot running that happens to call a REST API +// after the bot is up and running. +// + +/* eslint-disable @typescript-eslint/no-var-requires */ + +var restify = require('restify'); +var nockhelper = require('./nock-helper'); +var fetch = require('node-fetch'); + +exports.proxyPlay = async function(bot) { + console.log('PLAY against proxy'); + const activityBundles = nockhelper.parseActivityBundles(); + if (activityBundles.length <= 0) { + console.log('Nothing to replay, no recordings found.'); + return; + } + + setupBot(bot); + const requestUrl = process.env.PROXY_HOST + '/api/runtests'; + console.log('Using PROXY_HOST : ' + requestUrl); + fetch(requestUrl, { + method: 'GET', + headers: { + contentType: 'application/json', + }, + params: { + TestName: 'mytest', + }, + }) + .then(response => response.json()) + .then(data => { + console.log(data); + }) + .catch(err => console.log(err)); +}; + +function setupBot(bot) { + // Create HTTP server. + const server = restify.createServer(); + server.use(restify.plugins.queryParser()); + server.use(restify.plugins.bodyParser({ mapParams: true })); + server.listen(process.env.port || process.env.PORT || 3978, '0.0.0.0', () => { + console.log(`\n${ server.name } listening to ${ server.url }`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); + console.log(`\nTo test your bot, see: https://aka.ms/debug-with-emulator`); + }); + let adapter = new nockhelper.AdapterDisableAuth({ channelService: 'https://localhost:3979'}); + + // Listen for incoming requests. + server.post({ + path: '/api/messages', + contentType: 'application/json' + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async (req, res, next) => { + console.log('RECEIVED BOT HIT.. '); + + // Uncomment to see incomine requests. + // console.log(JSON.stringify(req.body)) + + await adapter.processActivity(req, res, async (context) => { + if (req.body.text == 'exit') { + //graceful shutdown + console.log('Exit received.'); + process.exit(); + } + // Route to main dialog. + await bot.run(context); + }); + }); + console.log('BOT SETUP COMPLETE.'); +} diff --git a/libraries/botbuilder/tests/teams/integrationBot/src/nock-helper/nock-helper.js b/libraries/botbuilder/tests/teams/integrationBot/src/nock-helper/nock-helper.js new file mode 100644 index 0000000000..0b3ef241a3 --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/src/nock-helper/nock-helper.js @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Implements TEST_MODE=RECORD +// +// Sets up nock http hooks to intercept and record the traffic (as json files) +// in the ./recordings directory and runs the bot as normal. +// +// You can send the bot "exit" and the bot will exit gracefully. +// +// The bottom of this file contains some common helper functions/class. +// +/* eslint-disable @typescript-eslint/camelcase */ +/* eslint-disable @typescript-eslint/no-var-requires */ + +var https = require('https'); +var http = require('http'); +var nock = require('nock'); +var fs = require('fs'); +var OriginalClientRequest = http.ClientRequest; // HTTP ClientRequest before mocking by Nock +var OriginalHttpsRequest = https.request; +var OriginalHttpRequest = http.request; +var NockClientRequest = http.ClientRequest; // HTTP ClientRequest mocked by Nock +var NockHttpsRequest = https.request; +var NockHttpRequest = http.request; +var botbuilder = require('botbuilder'); +var proxyhost = require('./nock-helper-proxyhost'); +var proxyplay = require('./nock-helper-proxyplay'); +var play = require('./nock-helper-play'); + +exports.nock = nock; +exports.testName = ''; +exports.testMode = ''; // RECORD | PLAY | PROXY_HOST | PROXY_PLAY +exports.proxyRecordings = proxyhost.proxyRecordings; +exports.proxyPlay = proxyplay.proxyPlay; +exports.processRecordings = play.processRecordings; + +exports.isRecording = function() { + return process.env.TEST_MODE === 'RECORD' ? true : false; +}; +exports.isPlaying = function() { + return process.env.TEST_MODE === 'PLAY' ? true : false; +}; +exports.isProxyHost = function() { + return process.env.TEST_MODE === 'PROXY_HOST' ? true : false; +}; +exports.isProxyPlay = function() { + return process.env.TEST_MODE === 'PROXY_PLAY' ? true : false; +}; + +function fileName(reqType, testName) { + var utcDate = new Date(); + return utcDate.getUTCFullYear().toString() + (utcDate.getUTCMonth()+1).toString().padStart(2, '0') + + utcDate.getUTCDate().toString().padStart(2, '0') + utcDate.getUTCHours().toString().padStart(2, '0') + + utcDate.getUTCMinutes().toString().padStart(2, '0') + utcDate.getUTCSeconds().toString().padStart(2, '0') + + utcDate.getUTCMilliseconds().toString().padStart(3, '0') + '-' + reqType + '-' + testName + '.json'; +} + +function validateTestMode() { + TEST_MODES = ['RECORD', 'PLAY', 'PROXY_HOST', 'PROXY_PLAY']; + testMode = process.env.TEST_MODE; + if (testMode) { + if (!TEST_MODES.includes(testMode.toUpperCase())) { + console.log(`ERROR: ${ testMode } is not a valid TEST_MODE.`); + console.log(`' Valid modes: ${ TEST_MODES }.`); + throw 'Invalid mode set.'; + } + } + else { + // Default to RECORD + testMode = 'RECORD'; + process.env.TEST_MODE = testMode; + } + console.log(`TEST_MODE: ${ testMode }`); +} + +exports.nockHttp = function(testNameDefault, recordingsPathRoot = './recordings') { + validateTestMode(); + testName = testNameDefault; + http.ClientRequest = NockClientRequest; + http.request = NockHttpRequest; + https.request = NockHttpsRequest; + recordingsPathRoot = recordingsPathRoot; + testName = testName; + + // Follow autorest environment variables + // https://github.com/microsoft/botbuilder-js/blob/master/tools/framework/suite-base.js#L66 + if (exports.isRecording()) { + const nock_output_recording = content => { + const filterScopes = ['https://login.microsoftonline.com:443', 'https://login.botframework.com:443']; + //const filter_scopes = []; + if (filterScopes.indexOf(content.scope) > -1) { + return; + } + fs.appendFileSync(recordingsPathRoot + '/' + fileName('reply', testName), JSON.stringify(content) ); + }; + + nock.recorder.rec({output_objects: true, + dont_print: false, + enable_reqheaders_recording: true, + logging: nock_output_recording, + use_separator: false, + }); + } +}; + +exports.logRequest = function(req, testName, recordingsPathRoot = './recordings') { + if (exports.isRecording()) { + var record = { 'type': 'request', 'url': req.url, 'method': req.method, 'headers': req.headers, 'body': req.body }; + // console.log('HEADERS:'+Object.getOwnPropertyNames(req)); + fs.appendFileSync(recordingsPathRoot + '/' + fileName('request', testName), JSON.stringify(record)); + } +}; + +function isIncomingActivityRequest(req) { + if ('type' in req && req.type == 'request') { + return true; + } + return false; +} + + +exports.unNockHttp = function() { + http.ClientRequest = OriginalClientRequest; + http.request = OriginalHttpRequest; + https.request = OriginalHttpsRequest; +}; + +// Parse all the sorted files and bundle them into Request/Replies +// Used in proxy and local playback.. +exports.parseActivityBundles = function() { + const sortedRecordings = fs.readdirSync('./recordings', 'utf8') + .map(item => { + const path = `./recordings/${ item }`; + return { name: item, path: path, }; + }) + .sort((a, b) => a.name > b.name ? 1 : -1); + var isFirstActivity = true; + var currentActivity = null; + var activityPath = null; // The file path of the current Activity + var replies = []; + var activities = []; + + async function processFile(data, index, recordingPath) { + const req = JSON.parse(data); + // Handle main activities coming into the bot (from Teams service) + if (isIncomingActivityRequest(req)) { + if (isFirstActivity == false) { + // Process previous activity. + activities.push({activity: currentActivity, replies: replies, activityPath: recordingPath}); + } + else { + isFirstActivity = false; + } + currentActivity = req; + replies = []; + activityPath = recordingPath; + } + // Handle replies from the bot back to the Teams service + else { + // Buffer the replies + replies.push(req); + } + // If last request or reply, then drain. + if (index >= sortedRecordings.length - 1 ) { + activities.push({activity: currentActivity, replies: replies, activityPath: activityPath}); + } + } + + sortedRecordings.forEach(async (item, index) => { + data = fs.readFileSync(item.path, 'utf8'); + await processFile(data, index, item.path); + }); + return activities; +}; + +// Adapter which disables authentication. +// Used in proxy and local playback.. +exports.AdapterDisableAuth = class AdapterDisableAuth extends botbuilder.BotFrameworkAdapter { + constructor(settings) { + super(settings); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + authenticateRequest(request, authHeader) { + // Skip authentication + return true; + } +}; + + +exports.unNockHttp(); // Revert the nock change so that tests by default run with the original, unmocked http request objects + + diff --git a/libraries/botbuilder/tests/teams/integrationBot/src/submitExampleData.ts b/libraries/botbuilder/tests/teams/integrationBot/src/submitExampleData.ts new file mode 100644 index 0000000000..ed33fe6aa9 --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/src/submitExampleData.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export class SubmitExampleData { + public MultiSelect: string; + public Option1: string; + public Option2: string; + public Option3: string; + public Question: string; + public submitLocation: string; +} diff --git a/libraries/botbuilder/tests/teams/integrationBot/src/teamsTenantFilteringMiddleware.ts b/libraries/botbuilder/tests/teams/integrationBot/src/teamsTenantFilteringMiddleware.ts new file mode 100644 index 0000000000..d0f6a500c0 --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/src/teamsTenantFilteringMiddleware.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + TurnContext, +} from 'botbuilder'; + +import { + Middleware, + TeamsChannelData, +} from 'botbuilder-core'; + +// +// Teams specific middleware for filtering messages by Tenant Id. +// +export class TeamsTenantFilteringMiddleware implements Middleware { + // tslint:disable:variable-name + private readonly _tenantSet = new Set(); + // tslint:enable:variable-name + + /** + * Initializes a new instance of the TeamsTenantFilteringMiddleware class. + * * @param allowedTenants either a single Tenant Id or array of Tenant Ids. + */ + constructor(allowedTenants: string | string[]) { + if(Array.isArray(allowedTenants)){ + for (let elem of allowedTenants) { + this._tenantSet.add(elem); + } + } + else { + this._tenantSet.add(allowedTenants); + } + } + + /** + * Store the incoming activity on the App Insights Correlation Context and optionally calls the TelemetryLoggerMiddleware + * @param context The context object for this turn. + * @param next The delegate to call to continue the bot middleware pipeline + */ + public async onTurn(context: TurnContext, next: () => Promise): Promise { + if (context === null) { + throw new Error('context is null'); + } + + if (context.activity.channelId !== 'msteams') { + // If the goal is to NOT process messages from other channels, comment out the following line + // and message processing will be 'short circuited'. + if (next !== null) { + await next(); + } + return; + } + + const channelData = context.activity.channelData as TeamsChannelData; + const tenant = channelData && channelData.tenant ? channelData.tenant : undefined; + const tenantId = tenant && typeof(tenant.id) === 'string' ? tenant.id : undefined; + + if (!tenantId) { + throw new Error("Tenant Id is missing."); + } + + if (!this._tenantSet.has(tenantId)) { + throw new Error(`Tenant Id '${tenantId}' is not allowed access.`); + } + + if (next !== null) { + await next(); + } + } +} diff --git a/libraries/botbuilder/tests/teams/integrationBot/static/composeExtensionSettings.html b/libraries/botbuilder/tests/teams/integrationBot/static/composeExtensionSettings.html new file mode 100644 index 0000000000..2efc693b98 --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/static/composeExtensionSettings.html @@ -0,0 +1,31 @@ + + + + Bot Info + + + + + + + +

I prefer:

+ + + + + \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/integrationBot/teams-app-manifest/icon-color.png b/libraries/botbuilder/tests/teams/integrationBot/teams-app-manifest/icon-color.png new file mode 100644 index 0000000000..bd9928dfc8 Binary files /dev/null and b/libraries/botbuilder/tests/teams/integrationBot/teams-app-manifest/icon-color.png differ diff --git a/libraries/botbuilder/tests/teams/integrationBot/teams-app-manifest/icon-outline.png b/libraries/botbuilder/tests/teams/integrationBot/teams-app-manifest/icon-outline.png new file mode 100644 index 0000000000..45e7a7c56e Binary files /dev/null and b/libraries/botbuilder/tests/teams/integrationBot/teams-app-manifest/icon-outline.png differ diff --git a/libraries/botbuilder/tests/teams/integrationBot/teams-app-manifest/manifest.json b/libraries/botbuilder/tests/teams/integrationBot/teams-app-manifest/manifest.json new file mode 100644 index 0000000000..0691ed93a7 --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/teams-app-manifest/manifest.json @@ -0,0 +1,179 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.3/MicrosoftTeams.schema.json", + "manifestVersion": "1.5", + "version": "1.0", + "id": "00000000-0000-0000-0000-000000000000", + "packageName": "com.teams.sample.integrationbot", + "developer": { + "name": "Microsoft", + "websiteUrl": "https://www.microsoft.com", + "privacyUrl": "https://www.teams.com/privacy", + "termsOfUseUrl": "https://www.teams.com/termsofuser" + }, + "icons": { + "color": "icon-color.png", + "outline": "icon-outline.png" + }, + "name": { + "short": "Integration Bot", + "full": "Integration Bot that exercises all of teams<=>bot integration." + }, + "description": { + "short": "Integration Bot", + "full": "Integration Bot that exercises all of teams<=>bot integration." + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "scopes": [ + "groupchat", + "team", + "personal" + ], + "supportsFiles": true, + "isNotificationOnly": false + } + ], + "composeExtensions": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "canUpdateConfiguration": true, + "commands": [ + { + "id": "createCard", + "type": "action", + "description": "Test command to run action to create a card from Compose Box or Command Bar", + "title": "Create card - manifest params", + "parameters": [ + { + "name": "title", + "title": "Title parameter", + "description": "Text for title in Hero Card", + "inputType": "text" + }, + { + "name": "subtitle", + "title": "Subtitle parameter", + "description": "Text for subtitle in Hero Card", + "inputType": "text" + }, + { + "name": "text", + "title": "Body text", + "description": "Text for body in Hero Card", + "inputType": "text" + } + ] + }, + { + "id": "createWithPreview", + "type": "action", + "title": "Create cards with preview", + "description": "Test command to run action to create a card with preview before sending", + "initialRun": false, + "fetchTask": true, + "context": [ + "commandBox", + "compose", + "message" + ], + "parameters": [ + { + "name": "param", + "title": "param", + "description": "param" + } + ] + }, + { + "id": "shareMessage", + "type": "action", + "context": [ "message" ], + "description": "Test command to run action on message context (message sharing)", + "title": "Share Message", + "parameters": [ + { + "name": "includeImage", + "title": "Include Image", + "description": "Include image in Hero Card", + "inputType": "toggle" + } + ] + }, + { + "id": "searchQuery", + "context": [ "compose", "commandBox" ], + "description": "Test command to run query", + "title": "Search", + "type": "query", + "parameters": [ + { + "name": "searchQuery", + "title": "Search Query", + "description": "Your search query", + "inputType": "text" + } + ] + }, + { + "id": "loginCommand", + "type": "action", + "title": "Log In", + "description": "Bot Service Auth flow in a Messaging Extension", + "initialRun": false, + "fetchTask": true, + "context": [ + "commandBox", + "compose", + "message" + ], + "parameters": [ + { + "name": "param", + "title": "param", + "description": "" + } + ] + }, + { + "id": "testCommand", + "type": "action", + "title": "Test", + "description": "Config extension test command", + "initialRun": false, + "fetchTask": false, + "context": [ + "commandBox", + "compose", + "message" + ], + "parameters": [ + { + "name": "param", + "title": "param", + "description": "" + } + ] + } + ], + "messageHandlers": [ + { + "type": "link", + "value": { + "domains": [ + "*.com" + ] + } + } + ] + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [ + "*.azurewebsites.net" + ] +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/integrationBot/tsconfig.json b/libraries/botbuilder/tests/teams/integrationBot/tsconfig.json new file mode 100644 index 0000000000..a168d60662 --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "composite": true, + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + } +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/integrationBot/tslint.json b/libraries/botbuilder/tests/teams/integrationBot/tslint.json new file mode 100644 index 0000000000..ad00715f85 --- /dev/null +++ b/libraries/botbuilder/tests/teams/integrationBot/tslint.json @@ -0,0 +1,18 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "interface-name" : [true, "never-prefix"], + "max-line-length": [false], + "no-console": [false, "log", "error"], + "no-var-requires": false, + "quotemark": [true, "single"], + "one-variable-per-declaration": false, + "curly": [true, "ignore-same-line"], + "trailing-comma": [true, {"multiline": "never", "singleline": "never"}] + }, + "rulesDirectory": [] +} diff --git a/libraries/botbuilder/tests/teams/link-unfurling/.env b/libraries/botbuilder/tests/teams/link-unfurling/.env new file mode 100644 index 0000000000..a695b3bf05 --- /dev/null +++ b/libraries/botbuilder/tests/teams/link-unfurling/.env @@ -0,0 +1,2 @@ +MicrosoftAppId= +MicrosoftAppPassword= diff --git a/libraries/botbuilder/tests/teams/link-unfurling/.gitignore b/libraries/botbuilder/tests/teams/link-unfurling/.gitignore new file mode 100644 index 0000000000..a6d1e8839d --- /dev/null +++ b/libraries/botbuilder/tests/teams/link-unfurling/.gitignore @@ -0,0 +1,4 @@ +lib/ +node_modules/ +.vscode/ +**/*.zip \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/link-unfurling/package.json b/libraries/botbuilder/tests/teams/link-unfurling/package.json new file mode 100644 index 0000000000..8cf8e224ce --- /dev/null +++ b/libraries/botbuilder/tests/teams/link-unfurling/package.json @@ -0,0 +1,30 @@ +{ + "name": "teams-link-unfurling", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "start": "tsc --build && node ./lib/index.js", + "build": "tsc --build", + "watch": "nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "botbuilder": "file:../../../", + "dotenv": "^8.1.0", + "node-fetch": "^2.6.0", + "restify": "^8.4.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^12.7.1", + "@types/node-fetch": "^2.5.0", + "@types/request": "^2.48.1", + "@types/restify": "^7.2.7", + "nodemon": "^1.19.1", + "ts-node": "^7.0.1", + "typescript": "^3.2.4" + } +} diff --git a/libraries/botbuilder/tests/teams/link-unfurling/src/index.ts b/libraries/botbuilder/tests/teams/link-unfurling/src/index.ts new file mode 100644 index 0000000000..e5a3ce3b29 --- /dev/null +++ b/libraries/botbuilder/tests/teams/link-unfurling/src/index.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { BotFrameworkAdapter } from 'botbuilder'; + +// This bot's main dialog. +import { LinkUnfurlingBot } from './linkUnfurling'; + +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${server.name} listening to ${server.url}`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); + console.log(`\nTo test your bot, see: https://aka.ms/debug-with-emulator`); +}); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error('[onTurnError]:'); + console.error(error); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`); +}; + +// Create the main dialog. +const myBot = new LinkUnfurlingBot(); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route to main dialog. + await myBot.run(context); + }); +}); diff --git a/libraries/botbuilder/tests/teams/link-unfurling/src/linkUnfurling.ts b/libraries/botbuilder/tests/teams/link-unfurling/src/linkUnfurling.ts new file mode 100644 index 0000000000..cea1b31259 --- /dev/null +++ b/libraries/botbuilder/tests/teams/link-unfurling/src/linkUnfurling.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + AppBasedLinkQuery, + CardFactory, + MessagingExtensionResponse, + MessagingExtensionResult, + TeamsActivityHandler, + TurnContext } +from 'botbuilder'; + +export class LinkUnfurlingBot extends TeamsActivityHandler { + constructor() { + super(); + + // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + this.onMessage(async (context, next) => { + await context.sendActivity(`You said '${context.activity.text}'`); + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } + + // "Link Unfurling" + // This handler is used for the processing of "composeExtension/queryLink" activities from Teams. + // https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/messaging-extensions/search-extensions#receive-requests-from-links-inserted-into-the-compose-message-box + // By specifying domains under the messageHandlers section in the manifest, the bot can receive + // events when a user enters in a domain in the compose box. + protected async handleTeamsAppBasedLinkQuery(context: TurnContext, query: AppBasedLinkQuery): Promise { + const attachment = CardFactory.thumbnailCard('Thumbnail Card', query.url, ["https://raw.githubusercontent.com/microsoft/botframework-sdk/master/icon.png"]); + + const result: MessagingExtensionResult = { + attachmentLayout: 'list', + type: 'result', + attachments: [attachment], + text: 'test unfurl', + } + const response: MessagingExtensionResponse = { + composeExtension: result, + } + + return response; + } +} diff --git a/libraries/botbuilder/tests/teams/link-unfurling/teams-app-manifest/color.png b/libraries/botbuilder/tests/teams/link-unfurling/teams-app-manifest/color.png new file mode 100644 index 0000000000..48a2de1330 Binary files /dev/null and b/libraries/botbuilder/tests/teams/link-unfurling/teams-app-manifest/color.png differ diff --git a/libraries/botbuilder/tests/teams/link-unfurling/teams-app-manifest/manifest.json b/libraries/botbuilder/tests/teams/link-unfurling/teams-app-manifest/manifest.json new file mode 100644 index 0000000000..7bc6591d99 --- /dev/null +++ b/libraries/botbuilder/tests/teams/link-unfurling/teams-app-manifest/manifest.json @@ -0,0 +1,64 @@ +{ + "$schema": "https://github.com/OfficeDev/microsoft-teams-app-schema/blob/preview/DevPreview/MicrosoftTeams.schema.json", + "manifestVersion": "1.5", + "version": "1.0", + "id": "00000000-0000-0000-0000-000000000000", + "packageName": "com.teams.sample.linkunfurling", + "developer": { + "name": "Link Unfurling1", + "websiteUrl": "https://www.microsoft.com", + "privacyUrl": "https://www.teams.com/privacy", + "termsOfUseUrl": "https://www.teams.com/termsofuser" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "Link Unfurling1", + "full": "Link Unfurling" + }, + "description": { + "short": "Link Unfurling", + "full": "Link Unfurling" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "scopes": [ "personal", "team" ] + } + ], + "composeExtensions": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "commands": [ + { + "id": "searchQuery", + "context": [ "compose", "commandBox" ], + "description": "Superfluous Test command to enable link unfurling", + "title": "Search", + "type": "query", + "parameters": [ + { + "name": "searchQuery", + "title": "Search Query", + "description": "Your search query", + "inputType": "text" + } + ] + } + ], + "messageHandlers": [ + { + "type": "link", + "value": { + "domains": [ + "*.com" + ] + } + } + ] + } + ] +} diff --git a/libraries/botbuilder/tests/teams/link-unfurling/teams-app-manifest/outline.png b/libraries/botbuilder/tests/teams/link-unfurling/teams-app-manifest/outline.png new file mode 100644 index 0000000000..dbfa927729 Binary files /dev/null and b/libraries/botbuilder/tests/teams/link-unfurling/teams-app-manifest/outline.png differ diff --git a/libraries/botbuilder/tests/teams/link-unfurling/tsconfig.json b/libraries/botbuilder/tests/teams/link-unfurling/tsconfig.json new file mode 100644 index 0000000000..a168d60662 --- /dev/null +++ b/libraries/botbuilder/tests/teams/link-unfurling/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "composite": true, + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + } +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/mentionsBot/.env b/libraries/botbuilder/tests/teams/mentionsBot/.env new file mode 100644 index 0000000000..a695b3bf05 --- /dev/null +++ b/libraries/botbuilder/tests/teams/mentionsBot/.env @@ -0,0 +1,2 @@ +MicrosoftAppId= +MicrosoftAppPassword= diff --git a/libraries/botbuilder/tests/teams/mentionsBot/package.json b/libraries/botbuilder/tests/teams/mentionsBot/package.json new file mode 100644 index 0000000000..f094cd00ae --- /dev/null +++ b/libraries/botbuilder/tests/teams/mentionsBot/package.json @@ -0,0 +1,30 @@ +{ + "name": "mentions-bot", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "start": "tsc --build && node ./lib/index.js", + "build": "tsc --build", + "watch": "nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "botbuilder": "file:../../../", + "dotenv": "^8.1.0", + "node-fetch": "^2.6.0", + "restify": "^8.4.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^12.7.1", + "@types/node-fetch": "^2.5.0", + "@types/request": "^2.48.1", + "@types/restify": "^7.2.7", + "nodemon": "^1.19.1", + "ts-node": "^7.0.1", + "typescript": "^3.2.4" + } +} diff --git a/libraries/botbuilder/tests/teams/mentionsBot/src/index.ts b/libraries/botbuilder/tests/teams/mentionsBot/src/index.ts new file mode 100644 index 0000000000..9f8c08889f --- /dev/null +++ b/libraries/botbuilder/tests/teams/mentionsBot/src/index.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { BotFrameworkAdapter } from 'botbuilder'; + +// This bot's main dialog. +import { MentionsBot } from './mentionsBot'; + +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${server.name} listening to ${server.url}`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); + console.log(`\nTo test your bot, see: https://aka.ms/debug-with-emulator`); +}); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// adapter.use(new TranscriptLoggerMiddleware(new FileTranscriptStore('./transcripts'))); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error('[onTurnError]:'); + console.error(error); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`); +}; + +// Create the main dialog. +const myBot = new MentionsBot(); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route to main dialog. + await myBot.run(context); + }); +}); diff --git a/libraries/botbuilder/tests/teams/mentionsBot/src/mentionsBot.ts b/libraries/botbuilder/tests/teams/mentionsBot/src/mentionsBot.ts new file mode 100644 index 0000000000..aef1ff2abd --- /dev/null +++ b/libraries/botbuilder/tests/teams/mentionsBot/src/mentionsBot.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Mention, + MessageFactory, + TeamsActivityHandler, +} from 'botbuilder'; + +export class MentionsBot extends TeamsActivityHandler { + /* + * You can @mention the bot from any scope and it will reply with the mention. + */ + constructor() { + super(); + + // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + this.onMessage(async (context, next) => { + var mention = { mentioned: context.activity.from, text:`${context.activity.from.name}` }; + + // Against Teams having a Mention in the Entities but not including that + // mention Text in the Activity Text will result in a BadRequest. + var replyActivity = MessageFactory.text(`Hello ${mention.text}.`); + replyActivity.entities = [ mention ]; + + await context.sendActivity(replyActivity); + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } +} diff --git a/libraries/botbuilder/tests/teams/mentionsBot/teams-app-manifest/icon-color.png b/libraries/botbuilder/tests/teams/mentionsBot/teams-app-manifest/icon-color.png new file mode 100644 index 0000000000..bd9928dfc8 Binary files /dev/null and b/libraries/botbuilder/tests/teams/mentionsBot/teams-app-manifest/icon-color.png differ diff --git a/libraries/botbuilder/tests/teams/mentionsBot/teams-app-manifest/icon-outline.png b/libraries/botbuilder/tests/teams/mentionsBot/teams-app-manifest/icon-outline.png new file mode 100644 index 0000000000..45e7a7c56e Binary files /dev/null and b/libraries/botbuilder/tests/teams/mentionsBot/teams-app-manifest/icon-outline.png differ diff --git a/libraries/botbuilder/tests/teams/mentionsBot/teams-app-manifest/manifest.json b/libraries/botbuilder/tests/teams/mentionsBot/teams-app-manifest/manifest.json new file mode 100644 index 0000000000..3058661d7d --- /dev/null +++ b/libraries/botbuilder/tests/teams/mentionsBot/teams-app-manifest/manifest.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.3/MicrosoftTeams.schema.json", + "manifestVersion": "1.3", + "version": "1.0.0", + "id": "00000000-0000-0000-0000-000000000000", + "packageName": "com.teams.sample.mentionsbot", + "developer": { + "name": "MentionsBot", + "websiteUrl": "https://www.microsoft.com", + "privacyUrl": "https://www.teams.com/privacy", + "termsOfUseUrl": "https://www.teams.com/termsofuser" + }, + "icons": { + "color": "icon-color.png", + "outline": "icon-outline.png" + }, + "name": { + "short": "MentionsBot", + "full": "MentionsBot" + }, + "description": { + "short": "MentionsBot", + "full": "MentionsBot" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "scopes": [ + "groupchat", + "team", + "personal" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/mentionsBot/tsconfig.json b/libraries/botbuilder/tests/teams/mentionsBot/tsconfig.json new file mode 100644 index 0000000000..a168d60662 --- /dev/null +++ b/libraries/botbuilder/tests/teams/mentionsBot/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "composite": true, + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + } +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/messageReaction/.env b/libraries/botbuilder/tests/teams/messageReaction/.env new file mode 100644 index 0000000000..a695b3bf05 --- /dev/null +++ b/libraries/botbuilder/tests/teams/messageReaction/.env @@ -0,0 +1,2 @@ +MicrosoftAppId= +MicrosoftAppPassword= diff --git a/libraries/botbuilder/tests/teams/messageReaction/package.json b/libraries/botbuilder/tests/teams/messageReaction/package.json new file mode 100644 index 0000000000..37c36a0d94 --- /dev/null +++ b/libraries/botbuilder/tests/teams/messageReaction/package.json @@ -0,0 +1,30 @@ +{ + "name": "reactions-bot", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "start": "tsc --build && node ./lib/index.js", + "build": "tsc --build", + "watch": "nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "botbuilder": "file:../../../", + "dotenv": "^8.1.0", + "node-fetch": "^2.6.0", + "restify": "^8.4.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^12.7.1", + "@types/node-fetch": "^2.5.0", + "@types/request": "^2.48.1", + "@types/restify": "^7.2.7", + "nodemon": "^1.19.1", + "ts-node": "^7.0.1", + "typescript": "^3.2.4" + } +} diff --git a/libraries/botbuilder/tests/teams/messageReaction/src/activityLog.ts b/libraries/botbuilder/tests/teams/messageReaction/src/activityLog.ts new file mode 100644 index 0000000000..8764f53576 --- /dev/null +++ b/libraries/botbuilder/tests/teams/messageReaction/src/activityLog.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Storage, +} from 'botbuilder'; + +import{ + Activity +} from 'botframework-schema' + + +export class ActivityLog { + private readonly _storage: Storage; + + public constructor(storage: Storage) { + this._storage = storage; + } + + public async append(activityId: string, activity: Partial): Promise { + if (activityId == null) + { + throw new TypeError("activityId is required for ActivityLog.append"); + } + + if (activity == null) + { + throw new TypeError("activity is required for ActivityLog.append"); + } + + let obj = { }; + obj[activityId] = { activity }; + + await this._storage.write( obj ); + + return; + } + + public async find(activityId: string): Promise + { + if (activityId == null) + { + throw new TypeError("activityId is required for ActivityLog.find"); + } + + var items = await this._storage.read( [ activityId ] ); + return (items && items[activityId]) ? items[activityId].activity : null; + } +} diff --git a/libraries/botbuilder/tests/teams/messageReaction/src/index.ts b/libraries/botbuilder/tests/teams/messageReaction/src/index.ts new file mode 100644 index 0000000000..9d330a86a5 --- /dev/null +++ b/libraries/botbuilder/tests/teams/messageReaction/src/index.ts @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { BotFrameworkAdapter, MemoryStorage } from 'botbuilder'; + +// This bot's main dialog. +import { MessageReactionBot } from './messageReactionBot'; +import { ActivityLog } from './activityLog'; + +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${server.name} listening to ${server.url}`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); + console.log(`\nTo test your bot, see: https://aka.ms/debug-with-emulator`); +}); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error('[onTurnError]:'); + console.error(error); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`); +}; + +const memoryStorage = new MemoryStorage(); +const activityLog = new ActivityLog(memoryStorage); + +// Create the main dialog. +const myBot = new MessageReactionBot(activityLog); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route to main dialog.5 + await myBot.run(context); + }); +}); diff --git a/libraries/botbuilder/tests/teams/messageReaction/src/messageReactionBot.ts b/libraries/botbuilder/tests/teams/messageReaction/src/messageReactionBot.ts new file mode 100644 index 0000000000..ecba3f5292 --- /dev/null +++ b/libraries/botbuilder/tests/teams/messageReaction/src/messageReactionBot.ts @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + MessageFactory, + MessageReaction, + ActivityHandler, + TurnContext, +} from 'botbuilder'; + +import { + ActivityLog +} from './activityLog'; + +export class MessageReactionBot extends ActivityHandler { + _log: ActivityLog; + + /* + * From the UI you need to @mention the bot, then add a message reaction to the message the bot sent. + */ + constructor(activityLog: ActivityLog) { + super(); + + this._log = activityLog; + + // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + this.onMessage(async (context, next) => { + await this.sendMessageAndLogActivityId(context, `echo: ${context.activity.text}`); + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } + + protected async onReactionsAddedActivity(reactionsAdded: MessageReaction[], context: TurnContext): Promise { + for (var i = 0, len = reactionsAdded.length; i < len; i++) { + var activity = await this._log.find(context.activity.replyToId); + if (activity == null) { + // If we had sent the message from the error handler we wouldn't have recorded the Activity Id and so we shouldn't expect to see it in the log. + await this.sendMessageAndLogActivityId(context, `Activity ${context.activity.replyToId} not found in the log.`); + } + else { + await this.sendMessageAndLogActivityId(context, `You added '${reactionsAdded[i].type}' regarding '${activity.text}'`); + } + }; + + return; + } + + protected async onReactionsRemovedActivity(reactionsAdded: MessageReaction[], context: TurnContext): Promise { + for (var i = 0, len = reactionsAdded.length; i < len; i++) { + // The ReplyToId property of the inbound MessageReaction Activity will correspond to a Message Activity that was previously sent from this bot. + var activity = await this._log.find(context.activity.replyToId); + if (activity == null) { + // If we had sent the message from the error handler we wouldn't have recorded the Activity Id and so we shouldn't expect to see it in the log. + await this.sendMessageAndLogActivityId(context, `Activity ${context.activity.replyToId} not found in the log.`); + } + else { + await this.sendMessageAndLogActivityId(context, `You removed '${reactionsAdded[i].type}' regarding '${activity.text}'`); + } + }; + + return; + } + + async sendMessageAndLogActivityId(context: TurnContext, text: string): Promise { + var replyActivity = MessageFactory.text(text); + var resourceResponse = await context.sendActivity(replyActivity); + await this._log.append(resourceResponse.id, replyActivity); + + return; + } +} diff --git a/libraries/botbuilder/tests/teams/messageReaction/teams-app-manifest/icon-color.png b/libraries/botbuilder/tests/teams/messageReaction/teams-app-manifest/icon-color.png new file mode 100644 index 0000000000..bd9928dfc8 Binary files /dev/null and b/libraries/botbuilder/tests/teams/messageReaction/teams-app-manifest/icon-color.png differ diff --git a/libraries/botbuilder/tests/teams/messageReaction/teams-app-manifest/icon-outline.png b/libraries/botbuilder/tests/teams/messageReaction/teams-app-manifest/icon-outline.png new file mode 100644 index 0000000000..45e7a7c56e Binary files /dev/null and b/libraries/botbuilder/tests/teams/messageReaction/teams-app-manifest/icon-outline.png differ diff --git a/libraries/botbuilder/tests/teams/messageReaction/teams-app-manifest/manifest.json b/libraries/botbuilder/tests/teams/messageReaction/teams-app-manifest/manifest.json new file mode 100644 index 0000000000..3058661d7d --- /dev/null +++ b/libraries/botbuilder/tests/teams/messageReaction/teams-app-manifest/manifest.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.3/MicrosoftTeams.schema.json", + "manifestVersion": "1.3", + "version": "1.0.0", + "id": "00000000-0000-0000-0000-000000000000", + "packageName": "com.teams.sample.mentionsbot", + "developer": { + "name": "MentionsBot", + "websiteUrl": "https://www.microsoft.com", + "privacyUrl": "https://www.teams.com/privacy", + "termsOfUseUrl": "https://www.teams.com/termsofuser" + }, + "icons": { + "color": "icon-color.png", + "outline": "icon-outline.png" + }, + "name": { + "short": "MentionsBot", + "full": "MentionsBot" + }, + "description": { + "short": "MentionsBot", + "full": "MentionsBot" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "scopes": [ + "groupchat", + "team", + "personal" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/messageReaction/tsconfig.json b/libraries/botbuilder/tests/teams/messageReaction/tsconfig.json new file mode 100644 index 0000000000..a168d60662 --- /dev/null +++ b/libraries/botbuilder/tests/teams/messageReaction/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "composite": true, + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + } +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/messagingExtensionAuth/.env b/libraries/botbuilder/tests/teams/messagingExtensionAuth/.env new file mode 100644 index 0000000000..cb01b37ccc --- /dev/null +++ b/libraries/botbuilder/tests/teams/messagingExtensionAuth/.env @@ -0,0 +1,3 @@ +MicrosoftAppId= +MicrosoftAppPassword= +ConnectionName= \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/messagingExtensionAuth/package.json b/libraries/botbuilder/tests/teams/messagingExtensionAuth/package.json new file mode 100644 index 0000000000..d96a615576 --- /dev/null +++ b/libraries/botbuilder/tests/teams/messagingExtensionAuth/package.json @@ -0,0 +1,30 @@ +{ + "name": "messagingextension-auth", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "start": "tsc --build && node ./lib/index.js", + "build": "tsc --build", + "watch": "nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "botbuilder": "file:../../../", + "dotenv": "^8.1.0", + "node-fetch": "^2.6.0", + "restify": "^8.4.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^12.7.1", + "@types/node-fetch": "^2.5.0", + "@types/request": "^2.48.1", + "@types/restify": "^7.2.7", + "nodemon": "^1.19.1", + "ts-node": "^7.0.1", + "typescript": "^3.2.4" + } +} diff --git a/libraries/botbuilder/tests/teams/messagingExtensionAuth/src/index.ts b/libraries/botbuilder/tests/teams/messagingExtensionAuth/src/index.ts new file mode 100644 index 0000000000..00ed0d294f --- /dev/null +++ b/libraries/botbuilder/tests/teams/messagingExtensionAuth/src/index.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { BotFrameworkAdapter } from 'botbuilder'; + +// This bot's main dialog. +import { MessagingExtensionAuthBot } from './messagingExtensionAuthBot'; + +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${server.name} listening to ${server.url}`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); + console.log(`\nTo test your bot, see: https://aka.ms/debug-with-emulator`); +}); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error('[onTurnError]:'); + console.error(error); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`); +}; + +// Create the main dialog. +const myBot = new MessagingExtensionAuthBot(process.env.ConnectionName); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route to main dialog. + await myBot.run(context); + }); +}); diff --git a/libraries/botbuilder/tests/teams/messagingExtensionAuth/src/messagingExtensionAuthBot.ts b/libraries/botbuilder/tests/teams/messagingExtensionAuth/src/messagingExtensionAuthBot.ts new file mode 100644 index 0000000000..33e07f80ba --- /dev/null +++ b/libraries/botbuilder/tests/teams/messagingExtensionAuth/src/messagingExtensionAuthBot.ts @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Attachment, + CardFactory, + MessagingExtensionActionResponse, + MessagingExtensionAction, + TaskModuleContinueResponse, + TaskModuleResponse, + TaskModuleTaskInfo, + TaskModuleRequest, + TeamsActivityHandler, + TurnContext, + BotFrameworkAdapter, +} from 'botbuilder'; + +import { + IUserTokenProvider, +} from 'botbuilder-core'; + +/* +* This Bot requires an Azure Bot Service OAuth connection name in appsettings.json +* see: https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-authentication +* +* Clicking this bot's Task Menu will retrieve the login dialog, if the user is not already signed in. +*/ +export class MessagingExtensionAuthBot extends TeamsActivityHandler { + connectionName: string; + constructor(authConnectionName: string) { + super(); + + this.connectionName = authConnectionName; + + // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + this.onMessage(async (context, next) => { + + // Hack around weird behavior of RemoveRecipientMention (it alters the activity.Text) + const originalText = context.activity.text; + TurnContext.removeRecipientMention(context.activity); + const text = context.activity.text.replace(' ', '').toUpperCase(); + context.activity.text = originalText; + + if (text === 'LOGOUT' || text === 'SIGNOUT') + { + const adapter: IUserTokenProvider = context.adapter as BotFrameworkAdapter; + + await adapter.signOutUser(context, this.connectionName); + await context.sendActivity(`Signed Out: ${context.activity.from.name}`); + + return; + } + + await context.sendActivity(`You said '${context.activity.text}'`); + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + + this.onMembersAdded(async (context, next) => { + const membersAdded = context.activity.membersAdded; + for (const member of membersAdded) { + if (member.id !== context.activity.recipient.id) { + await context.sendActivity('Hello and welcome!'); + } + } + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } + + protected async handleTeamsMessagingExtensionFetchTask(context: TurnContext, action: MessagingExtensionAction): Promise { + const adapter: IUserTokenProvider = context.adapter as BotFrameworkAdapter; + const userToken = await adapter.getUserToken(context, this.connectionName); + if (!userToken) + { + // There is no token, so the user has not signed in yet. + + // Retrieve the OAuth Sign in Link to use in the MessagingExtensionResult Suggested Actions + const signInLink = await adapter.getSignInLink(context, this.connectionName); + + const response : MessagingExtensionActionResponse = { + composeExtension: { + type: 'auth', + suggestedActions: { + actions: [{ + type: 'openUrl', + value: signInLink, + title: 'Bot Service OAuth' + }] + } + } + }; + return response; + }; + + // User is already signed in. + const continueResponse : TaskModuleContinueResponse = { + type: 'continue', + value: this.CreateSignedInTaskModuleTaskInfo(), + }; + + const response : MessagingExtensionActionResponse = { + task: continueResponse + }; + + return response; + } + + protected async handleTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { + var data = context.activity.value; + if (data && data.state) + { + const adapter: IUserTokenProvider = context.adapter as BotFrameworkAdapter; + const tokenResponse = await adapter.getUserToken(context, this.connectionName, data.state); + + const continueResponse : TaskModuleContinueResponse = { + type: 'continue', + value: this.CreateSignedInTaskModuleTaskInfo(tokenResponse.token), + }; + + const response : MessagingExtensionActionResponse = { + task: continueResponse + }; + + return response; + } + else + { + await context.sendActivity("handleTeamsTaskModuleFetchAsync called without 'state' in Activity.Value"); + return null; + } + } + + protected async handleTeamsMessagingExtensionSubmitAction(context, action: MessagingExtensionAction): Promise { + if (action.data != null && action.data.key && action.data.key == "signout") + { + // User clicked the Sign Out button from a Task Module + await (context.adapter as IUserTokenProvider).signOutUser(context, this.connectionName); + await context.sendActivity(`Signed Out: ${context.activity.from.name}`); + } + + return null; + } + + private CreateSignedInTaskModuleTaskInfo(token?: string): TaskModuleTaskInfo { + const attachment = this.GetTaskModuleAdaptiveCard(); + let width = 350; + let height = 160; + if(token){ + + const subCard = CardFactory.adaptiveCard({ + version: '1.0.0', + type: 'AdaptiveCard', + body: [ + { + type: 'TextBlock', + text: `Your token is ` + token, + wrap: true, + } + ] + }); + + const card = attachment.content; + card.actions.push( + { + type: 'Action.ShowCard', + title: 'Show Token', + card: subCard.content, + } + ); + width = 500; + height = 300; + } + return { + card: attachment, + height: height, + width: width, + title: 'Compose Extension Auth Example', + }; + } + + private GetTaskModuleAdaptiveCard(): Attachment { + return CardFactory.adaptiveCard({ + version: '1.0.0', + type: 'AdaptiveCard', + body: [ + { + type: 'TextBlock', + text: `You are signed in!`, + }, + { + type: 'TextBlock', + text: `Send 'Log out' or 'Sign out' to start over.`, + }, + { + type: 'TextBlock', + text: `(Or click the Sign Out button below.)`, + }, + ], + actions: [ + { + type: 'Action.Submit', + title: 'Close', + data: { + key: 'close', + } + }, + { + type: 'Action.Submit', + title: 'Sign Out', + data: { + key: 'signout', + } + } + ] + }); + } +} diff --git a/libraries/botbuilder/tests/teams/messagingExtensionAuth/teams-app-manifest/color.png b/libraries/botbuilder/tests/teams/messagingExtensionAuth/teams-app-manifest/color.png new file mode 100644 index 0000000000..48a2de1330 Binary files /dev/null and b/libraries/botbuilder/tests/teams/messagingExtensionAuth/teams-app-manifest/color.png differ diff --git a/libraries/botbuilder/tests/teams/messagingExtensionAuth/teams-app-manifest/manifest.json b/libraries/botbuilder/tests/teams/messagingExtensionAuth/teams-app-manifest/manifest.json new file mode 100644 index 0000000000..dba3ce3675 --- /dev/null +++ b/libraries/botbuilder/tests/teams/messagingExtensionAuth/teams-app-manifest/manifest.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", + "manifestVersion": "1.5", + "version": "1.0", + "id": "00000000-0000-0000-0000-000000000000", + "packageName": "com.teams.sample.compose.extension", + "developer": { + "name": "Microsoft", + "websiteUrl": "https://www.microsoft.com", + "privacyUrl": "https://www.teams.com/privacy", + "termsOfUseUrl": "https://www.teams.com/termsofuser" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "Messaging Extension Auth", + "full": "Messaging Extension Auth Example" + }, + "description": { + "short": "Bot Service Auth in Messaging Extension", + "full": "Demonstrates Bot Service Auth in a Messaging Extension" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "scopes": [ + "personal", + "team", + "groupchat" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "composeExtensions": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "canUpdateConfiguration": false, + "commands": [ + { + "id": "loginCommand", + "type": "action", + "title": "Log In", + "description": "Bot Service Auth flow in a Messaging Extension", + "initialRun": false, + "fetchTask": true, + "context": [ + "commandBox", + "compose", + "message" + ], + "parameters": [ + { + "name": "param", + "title": "param", + "description": "" + } + ] + } + ] + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [ + "*.botframework.com" + ] +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/messagingExtensionAuth/teams-app-manifest/outline.png b/libraries/botbuilder/tests/teams/messagingExtensionAuth/teams-app-manifest/outline.png new file mode 100644 index 0000000000..dbfa927729 Binary files /dev/null and b/libraries/botbuilder/tests/teams/messagingExtensionAuth/teams-app-manifest/outline.png differ diff --git a/libraries/botbuilder/tests/teams/messagingExtensionAuth/tsconfig.json b/libraries/botbuilder/tests/teams/messagingExtensionAuth/tsconfig.json new file mode 100644 index 0000000000..a168d60662 --- /dev/null +++ b/libraries/botbuilder/tests/teams/messagingExtensionAuth/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "composite": true, + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + } +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/messagingExtensionConfig/.env b/libraries/botbuilder/tests/teams/messagingExtensionConfig/.env new file mode 100644 index 0000000000..660828e3e8 --- /dev/null +++ b/libraries/botbuilder/tests/teams/messagingExtensionConfig/.env @@ -0,0 +1,2 @@ +MicrosoftAppId= +MicrosoftAppPassword= \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/messagingExtensionConfig/package.json b/libraries/botbuilder/tests/teams/messagingExtensionConfig/package.json new file mode 100644 index 0000000000..00de13aa1e --- /dev/null +++ b/libraries/botbuilder/tests/teams/messagingExtensionConfig/package.json @@ -0,0 +1,30 @@ +{ + "name": "messaging-extension-bot", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "start": "tsc --build && node ./lib/index.js", + "build": "tsc --build", + "watch": "nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "botbuilder": "file:../../../", + "dotenv": "^8.1.0", + "node-fetch": "^2.6.0", + "restify": "^8.4.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^12.7.1", + "@types/node-fetch": "^2.5.0", + "@types/request": "^2.48.1", + "@types/restify": "^7.2.7", + "nodemon": "^1.19.1", + "ts-node": "^7.0.1", + "typescript": "^3.2.4" + } +} diff --git a/libraries/botbuilder/tests/teams/messagingExtensionConfig/src/index.ts b/libraries/botbuilder/tests/teams/messagingExtensionConfig/src/index.ts new file mode 100644 index 0000000000..7124faf10a --- /dev/null +++ b/libraries/botbuilder/tests/teams/messagingExtensionConfig/src/index.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { BotFrameworkAdapter } from 'botbuilder'; + +// This bot's main dialog. +import { MessagingExtensionConfigBot } from './messagingExtensionConfigBot'; + +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${server.name} listening to ${server.url}`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); + console.log(`\nTo test your bot, see: https://aka.ms/debug-with-emulator`); +}); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// adapter.use(new TranscriptLoggerMiddleware(new FileTranscriptStore('./transcripts'))); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error('[onTurnError]:'); + console.error(error); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`); +}; + +// Create the main dialog. +const myBot = new MessagingExtensionConfigBot(); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route to main dialog. + await myBot.run(context); + }); +}); diff --git a/libraries/botbuilder/tests/teams/messagingExtensionConfig/src/messagingExtensionConfigBot.ts b/libraries/botbuilder/tests/teams/messagingExtensionConfig/src/messagingExtensionConfigBot.ts new file mode 100644 index 0000000000..3cbb3b0791 --- /dev/null +++ b/libraries/botbuilder/tests/teams/messagingExtensionConfig/src/messagingExtensionConfigBot.ts @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + MessagingExtensionActionResponse, + MessagingExtensionResult, + MessagingExtensionSuggestedAction, + MessagingExtensionQuery, + TeamsActivityHandler, + TurnContext, +} from 'botbuilder'; + +import { + ActionTypes, +} from 'botframework-schema' + +export class MessagingExtensionConfigBot extends TeamsActivityHandler { + constructor() { + super(); + + // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + this.onMessage(async (context, next) => { + await context.sendActivity(`echo: '${context.activity.text}'`); + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + + this.onMembersAdded(async (context, next) => { + const membersAdded = context.activity.membersAdded; + for (const member of membersAdded) { + if (member.id !== context.activity.recipient.id) { + await context.sendActivity('Hello and welcome!'); + } + } + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } + + protected async handleTeamsMessagingExtensionConfigurationQuerySettingUrl(context: TurnContext, query: MessagingExtensionQuery){ + return + { + composeExtension: { + type: 'config', + suggestedActions: { + actions: [ + { + type: ActionTypes.OpenUrl, + value: 'https://teamssettingspagescenario.azurewebsites.net', + }, + ] + } + } + } + } + + protected async handleTeamsMessagingExtensionConfigurationSetting(context: TurnContext, settings){ + // This event is fired when the settings page is submitted + await context.sendActivity(`onTeamsMessagingExtensionSettings event fired with ${ JSON.stringify(settings) }`); + } +} diff --git a/libraries/botbuilder/tests/teams/messagingExtensionConfig/teams-app-manifest/color.png b/libraries/botbuilder/tests/teams/messagingExtensionConfig/teams-app-manifest/color.png new file mode 100644 index 0000000000..48a2de1330 Binary files /dev/null and b/libraries/botbuilder/tests/teams/messagingExtensionConfig/teams-app-manifest/color.png differ diff --git a/libraries/botbuilder/tests/teams/messagingExtensionConfig/teams-app-manifest/manifest.json b/libraries/botbuilder/tests/teams/messagingExtensionConfig/teams-app-manifest/manifest.json new file mode 100644 index 0000000000..2c7986614f --- /dev/null +++ b/libraries/botbuilder/tests/teams/messagingExtensionConfig/teams-app-manifest/manifest.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", + "manifestVersion": "1.5", + "version": "1.0", + "id": "00000000-0000-0000-0000-000000000000", + "packageName": "com.teams.sample.messagingextension", + "developer": { + "name": "Microsoft", + "websiteUrl": "https://www.microsoft.com", + "privacyUrl": "https://www.teams.com/privacy", + "termsOfUseUrl": "https://www.teams.com/termsofuser" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "MessagingExtensionConfig", + "full": "MessagingExtensionConfig" + }, + "description": { + "short": "MessagingExtensionConfig", + "full": "MessagingExtensionConfig" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "scopes": [ + "personal", + "team", + "groupchat" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "composeExtensions": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "canUpdateConfiguration": true, + "commands": [ + { + "id": "testCommand", + "type": "action", + "title": "Test", + "description": "Config extension test command", + "initialRun": false, + "fetchTask": false, + "context": [ + "commandBox", + "compose", + "message" + ], + "parameters": [ + { + "name": "param", + "title": "param", + "description": "" + } + ] + } + ] + } + ], + "validDomains": [ + "*.azurewebsites.net" + ] +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/messagingExtensionConfig/teams-app-manifest/outline.png b/libraries/botbuilder/tests/teams/messagingExtensionConfig/teams-app-manifest/outline.png new file mode 100644 index 0000000000..dbfa927729 Binary files /dev/null and b/libraries/botbuilder/tests/teams/messagingExtensionConfig/teams-app-manifest/outline.png differ diff --git a/libraries/botbuilder/tests/teams/messagingExtensionConfig/tsconfig.json b/libraries/botbuilder/tests/teams/messagingExtensionConfig/tsconfig.json new file mode 100644 index 0000000000..a168d60662 --- /dev/null +++ b/libraries/botbuilder/tests/teams/messagingExtensionConfig/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "composite": true, + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + } +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/notificationOnly/.env b/libraries/botbuilder/tests/teams/notificationOnly/.env new file mode 100644 index 0000000000..660828e3e8 --- /dev/null +++ b/libraries/botbuilder/tests/teams/notificationOnly/.env @@ -0,0 +1,2 @@ +MicrosoftAppId= +MicrosoftAppPassword= \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/notificationOnly/package.json b/libraries/botbuilder/tests/teams/notificationOnly/package.json new file mode 100644 index 0000000000..ce71d149a2 --- /dev/null +++ b/libraries/botbuilder/tests/teams/notificationOnly/package.json @@ -0,0 +1,30 @@ +{ + "name": "notification-only", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "start": "tsc --build && node ./lib/index.js", + "build": "tsc --build", + "watch": "nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "botbuilder": "file:../../../", + "dotenv": "^8.1.0", + "node-fetch": "^2.6.0", + "restify": "^8.4.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^12.7.1", + "@types/node-fetch": "^2.5.0", + "@types/request": "^2.48.1", + "@types/restify": "^7.2.7", + "nodemon": "^1.19.1", + "ts-node": "^7.0.1", + "typescript": "^3.2.4" + } +} diff --git a/libraries/botbuilder/tests/teams/notificationOnly/src/index.ts b/libraries/botbuilder/tests/teams/notificationOnly/src/index.ts new file mode 100644 index 0000000000..fdbafb84e4 --- /dev/null +++ b/libraries/botbuilder/tests/teams/notificationOnly/src/index.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { BotFrameworkAdapter } from 'botbuilder'; + +// This bot's main dialog. +import { NotificationOnlyBot } from './notificationOnlyBot'; + +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${server.name} listening to ${server.url}`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); + console.log(`\nTo test your bot, see: https://aka.ms/debug-with-emulator`); +}); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// adapter.use(new TranscriptLoggerMiddleware(new FileTranscriptStore('./transcripts'))); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error('[onTurnError]:'); + console.error(error); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`); +}; + +// Create the main dialog. +const myBot = new NotificationOnlyBot(); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route to main dialog. + await myBot.run(context); + }); +}); diff --git a/libraries/botbuilder/tests/teams/notificationOnly/src/notificationOnlyBot.ts b/libraries/botbuilder/tests/teams/notificationOnly/src/notificationOnlyBot.ts new file mode 100644 index 0000000000..aac95e8d97 --- /dev/null +++ b/libraries/botbuilder/tests/teams/notificationOnly/src/notificationOnlyBot.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + ChannelAccount, + MessageFactory, + TeamsActivityHandler, + TeamInfo, + TurnContext, +} from 'botbuilder'; + +export class NotificationOnlyBot extends TeamsActivityHandler { + /* + * This bot needs to be installed in a team or group chat that you are an admin of. You can add/remove someone from that team and + * the bot will send that person a 1:1 message saying what happened. Also, yes, this scenario isn't the most up to date with the updated + * APIs for membersAdded/removed. Also you should NOT be able to @mention this bot. + */ + constructor() { + super(); + + // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + this.onMessage(async (context, next) => { + await context.sendActivity(`You said '${context.activity.text}'`); + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + + this.onTeamsMembersAddedEvent(async (membersAdded: ChannelAccount[], teamInfo: TeamInfo, context: TurnContext, next: () => Promise): Promise => { + for (const member of membersAdded) { + var replyActivity = MessageFactory.text(`${member.id} was added to the team.`); + replyActivity = TurnContext.applyConversationReference(replyActivity, TurnContext.getConversationReference(context.activity)); + await context.sendActivity(replyActivity); + } + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + this.onTeamsMembersRemovedEvent(async (membersRemoved: ChannelAccount[], teamInfo: TeamInfo, context: TurnContext, next: () => Promise): Promise => { + for (const member of membersRemoved) { + var replyActivity = MessageFactory.text(`${member.id} was removed from the team.`); + replyActivity = TurnContext.applyConversationReference(replyActivity, TurnContext.getConversationReference(context.activity)); + await context.sendActivity(replyActivity); + } + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + this.onMembersAdded(async (context: TurnContext, next: () => Promise): Promise => { + for (const member of context.activity.membersAdded) { + var replyActivity = MessageFactory.text(`${member.id} was added to the team.`); + + replyActivity = TurnContext.applyConversationReference(replyActivity, TurnContext.getConversationReference(context.activity)); + const channelId = context.activity.conversation.id.split(';')[0]; + replyActivity.conversation.id = channelId; + await context.sendActivity(replyActivity); + } + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + this.onMembersRemoved(async (context: TurnContext, next: () => Promise): Promise => { + for (const member of context.activity.membersRemoved) { + var replyActivity = MessageFactory.text(`${member.id} was removed from the team.`); + + replyActivity = TurnContext.applyConversationReference(replyActivity, TurnContext.getConversationReference(context.activity)); + const channelId = context.activity.conversation.id.split(';')[0]; + replyActivity.conversation.id = channelId; + await context.sendActivity(replyActivity); + } + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } + +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/notificationOnly/teams-app-manifest/color.png b/libraries/botbuilder/tests/teams/notificationOnly/teams-app-manifest/color.png new file mode 100644 index 0000000000..48a2de1330 Binary files /dev/null and b/libraries/botbuilder/tests/teams/notificationOnly/teams-app-manifest/color.png differ diff --git a/libraries/botbuilder/tests/teams/notificationOnly/teams-app-manifest/manifest.json b/libraries/botbuilder/tests/teams/notificationOnly/teams-app-manifest/manifest.json new file mode 100644 index 0000000000..061cd75b4c --- /dev/null +++ b/libraries/botbuilder/tests/teams/notificationOnly/teams-app-manifest/manifest.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.3/MicrosoftTeams.schema.json", + "manifestVersion": "1.3", + "version": "1.0", + "id": "00000000-0000-0000-0000-000000000000", + "packageName": "com.microsoft.teams.samples.notificationonly", + "developer": { + "name": "Microsoft Corp", + "websiteUrl": "https://example.azurewebsites.net", + "privacyUrl": "https://example.azurewebsites.net/privacy", + "termsOfUseUrl": "https://example.azurewebsites.net/termsofuse" + }, + "name": { + "short": "NotificationOnly", + "full": "NotificationOnly" + }, + "description": { + "short": "NotificationOnly", + "full": "NotificationOnly" + }, + "icons": { + "outline": "outline.png", + "color": "color.png" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "scopes": [ + "personal", + "team" + ], + "supportsFiles": false, + "isNotificationOnly": true, + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/notificationOnly/teams-app-manifest/outline.png b/libraries/botbuilder/tests/teams/notificationOnly/teams-app-manifest/outline.png new file mode 100644 index 0000000000..dbfa927729 Binary files /dev/null and b/libraries/botbuilder/tests/teams/notificationOnly/teams-app-manifest/outline.png differ diff --git a/libraries/botbuilder/tests/teams/notificationOnly/tsconfig.json b/libraries/botbuilder/tests/teams/notificationOnly/tsconfig.json new file mode 100644 index 0000000000..a168d60662 --- /dev/null +++ b/libraries/botbuilder/tests/teams/notificationOnly/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "composite": true, + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + } +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/office365Card/.env b/libraries/botbuilder/tests/teams/office365Card/.env new file mode 100644 index 0000000000..660828e3e8 --- /dev/null +++ b/libraries/botbuilder/tests/teams/office365Card/.env @@ -0,0 +1,2 @@ +MicrosoftAppId= +MicrosoftAppPassword= \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/office365Card/package.json b/libraries/botbuilder/tests/teams/office365Card/package.json new file mode 100644 index 0000000000..a1b3cc1cdf --- /dev/null +++ b/libraries/botbuilder/tests/teams/office365Card/package.json @@ -0,0 +1,30 @@ +{ + "name": "office365-cards", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "start": "tsc --build && node ./lib/index.js", + "build": "tsc --build", + "watch": "nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "botbuilder": "file:../../../", + "dotenv": "^8.1.0", + "node-fetch": "^2.6.0", + "restify": "^8.4.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^12.7.1", + "@types/node-fetch": "^2.5.0", + "@types/request": "^2.48.1", + "@types/restify": "^7.2.7", + "nodemon": "^1.19.1", + "ts-node": "^7.0.1", + "typescript": "^3.2.4" + } +} diff --git a/libraries/botbuilder/tests/teams/office365Card/src/index.ts b/libraries/botbuilder/tests/teams/office365Card/src/index.ts new file mode 100644 index 0000000000..f18ed6cca1 --- /dev/null +++ b/libraries/botbuilder/tests/teams/office365Card/src/index.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { BotFrameworkAdapter } from 'botbuilder'; + +// This bot's main dialog. +import { Office365CardsBot } from './office365CardsBot'; + +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${server.name} listening to ${server.url}`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); + console.log(`\nTo test your bot, see: https://aka.ms/debug-with-emulator`); +}); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error('[onTurnError]:'); + console.error(error); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`); +}; + +// Create the main dialog. +const myBot = new Office365CardsBot(); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route to main dialog. + await myBot.run(context); + }); +}); diff --git a/libraries/botbuilder/tests/teams/office365Card/src/office365CardsBot.ts b/libraries/botbuilder/tests/teams/office365Card/src/office365CardsBot.ts new file mode 100644 index 0000000000..1fd8ccd305 --- /dev/null +++ b/libraries/botbuilder/tests/teams/office365Card/src/office365CardsBot.ts @@ -0,0 +1,319 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CardFactory, + MessageFactory, + O365ConnectorCard, + O365ConnectorCardActionBase, + O365ConnectorCardActionCard, + O365ConnectorCardActionQuery, + O365ConnectorCardDateInput, + O365ConnectorCardHttpPOST, + O365ConnectorCardMultichoiceInput, + O365ConnectorCardMultichoiceInputChoice, + O365ConnectorCardOpenUri, + O365ConnectorCardTextInput, + O365ConnectorCardViewAction, + TeamsActivityHandler, + TurnContext +} from 'botbuilder'; + +/** + * You can install this bot in any scope. From the UI just @mention the bot. + */ +export class Office365CardsBot extends TeamsActivityHandler { + constructor() { + super(); + + // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + this.onMessage(async (context, next) => { + await this.sendO365CardAttachment(context); + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + + this.onMembersAdded(async (context, next) => { + const membersAdded = context.activity.membersAdded; + for (const member of membersAdded) { + if (member.id !== context.activity.recipient.id) { + await context.sendActivity('Hello and welcome!'); + } + } + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } + + protected async handleTeamsO365ConnectorCardAction(context: TurnContext, query: O365ConnectorCardActionQuery): Promise { + await context.sendActivity(MessageFactory.text(`O365ConnectorCardActionQuery event value: ${JSON.stringify(query)}`)); + } + + private async sendO365CardAttachment(context: TurnContext): Promise { + const card = CardFactory.o365ConnectorCard({ + "title": "card title", + "text": "card text", + "summary": "O365 card summary", + "themeColor": "#E67A9E", + "sections": [ + { + "title": "**section title**", + "text": "section text", + "activityTitle": "activity title", + "activitySubtitle": "activity subtitle", + "activityText": "activity text", + "activityImage": "http://connectorsdemo.azurewebsites.net/images/MSC12_Oscar_002.jpg", + "activityImageType": "avatar", + "markdown": true, + "facts": [ + { + "name": "Fact name 1", + "value": "Fact value 1" + }, + { + "name": "Fact name 2", + "value": "Fact value 2" + } + ], + "images": [ + { + "image": "http://connectorsdemo.azurewebsites.net/images/MicrosoftSurface_024_Cafe_OH-06315_VS_R1c.jpg", + "title": "image 1" + }, + { + "image": "http://connectorsdemo.azurewebsites.net/images/WIN12_Scene_01.jpg", + "title": "image 2" + }, + { + "image": "http://connectorsdemo.azurewebsites.net/images/WIN12_Anthony_02.jpg", + "title": "image 3" + } + ], + "potentialAction": null + } + ], + "potentialAction": [ + { + "@type": "ActionCard", + "inputs": [ + { + "@type": "multichoiceInput", + "choices": [ + { + "display": "Choice 1", + "value": "1" + }, + { + "display": "Choice 2", + "value": "2" + }, + { + "display": "Choice 3", + "value": "3" + } + ], + "style": "expanded", + "isMultiSelect": true, + "id": "list-1", + "isRequired": true, + "title": "Pick multiple options", + "value": null + }, + { + "@type": "multichoiceInput", + "choices": [ + { + "display": "Choice 4", + "value": "4" + }, + { + "display": "Choice 5", + "value": "5" + }, + { + "display": "Choice 6", + "value": "6" + } + ], + "style": "compact", + "isMultiSelect": true, + "id": "list-2", + "isRequired": true, + "title": "Pick multiple options", + "value": null + }, + { + "@type": "multichoiceInput", + "choices": [ + { + "display": "Choice a", + "value": "a" + }, + { + "display": "Choice b", + "value": "b" + }, + { + "display": "Choice c", + "value": "c" + } + ], + "style": "expanded", + "isMultiSelect": false, + "id": "list-3", + "isRequired": false, + "title": "Pick an option", + "value": null + }, + { + "@type": "multichoiceInput", + "choices": [ + { + "display": "Choice x", + "value": "x" + }, + { + "display": "Choice y", + "value": "y" + }, + { + "display": "Choice z", + "value": "z" + } + ], + "style": "compact", + "isMultiSelect": false, + "id": "list-4", + "isRequired": false, + "title": "Pick an option", + "value": null + } + ], + "actions": [ + { + "@type": "HttpPOST", + "body": "{\"text1\":\"{{text-1.value}}\", \"text2\":\"{{text-2.value}}\", \"text3\":\"{{text-3.value}}\", \"text4\":\"{{text-4.value}}\"}", + "name": "Send", + "@id": "card-1-btn-1" + } + ], + "name": "Multiple Choice", + "@id": "card-1" + }, + { + "@type": "ActionCard", + "inputs": [ + { + "@type": "textInput", + "isMultiline": true, + "maxLength": null, + "id": "text-1", + "isRequired": false, + "title": "multiline, no maxLength", + "value": null + }, + { + "@type": "textInput", + "isMultiline": false, + "maxLength": null, + "id": "text-2", + "isRequired": false, + "title": "single line, no maxLength", + "value": null + }, + { + "@type": "textInput", + "isMultiline": true, + "maxLength": 10.0, + "id": "text-3", + "isRequired": true, + "title": "multiline, max len = 10, isRequired", + "value": null + }, + { + "@type": "textInput", + "isMultiline": false, + "maxLength": 10.0, + "id": "text-4", + "isRequired": true, + "title": "single line, max len = 10, isRequired", + "value": null + } + ], + "actions": [ + { + "@type": "HttpPOST", + "body": "{\"text1\":\"{{text-1.value}}\", \"text2\":\"{{text-2.value}}\", \"text3\":\"{{text-3.value}}\", \"text4\":\"{{text-4.value}}\"}", + "name": "Send", + "@id": "card-2-btn-1" + } + ], + "name": "Text Input", + "@id": "card-2" + }, + { + "@type": "ActionCard", + "inputs": [ + { + "@type": "dateInput", + "includeTime": true, + "id": "date-1", + "isRequired": true, + "title": "date with time", + "value": null + }, + { + "@type": "dateInput", + "includeTime": false, + "id": "date-2", + "isRequired": false, + "title": "date only", + "value": null + } + ], + "actions": [ + { + "@type": "HttpPOST", + "body": "{\"date1\":\"{{date-1.value}}\", \"date2\":\"{{date-2.value}}\"}", + "name": "Send", + "@id": "card-3-btn-1" + } + ], + "name": "Date Input", + "@id": "card-3" + }, + { + "@type": "ViewAction", + "target": ["http://microsoft.com"], + "name": "View Action", + "@id": null + }, + { + "@type": "OpenUri", + "targets": [ + { + "os": "default", + "uri": "http://microsoft.com" + }, + { + "os": "iOS", + "uri": "http://microsoft.com" + }, + { + "os": "android", + "uri": "http://microsoft.com" + }, + { + "os": "windows", + "uri": "http://microsoft.com" + } + ], + "name": "Open Uri", + "@id": "open-uri" + } + ] + }); + await context.sendActivity(MessageFactory.attachment(card)); + } +} diff --git a/libraries/botbuilder/tests/teams/office365Card/teams-app-manifest/color.png b/libraries/botbuilder/tests/teams/office365Card/teams-app-manifest/color.png new file mode 100644 index 0000000000..48a2de1330 Binary files /dev/null and b/libraries/botbuilder/tests/teams/office365Card/teams-app-manifest/color.png differ diff --git a/libraries/botbuilder/tests/teams/office365Card/teams-app-manifest/manifest.json b/libraries/botbuilder/tests/teams/office365Card/teams-app-manifest/manifest.json new file mode 100644 index 0000000000..b15a530178 --- /dev/null +++ b/libraries/botbuilder/tests/teams/office365Card/teams-app-manifest/manifest.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.3/MicrosoftTeams.schema.json", + "manifestVersion": "1.3", + "version": "1.0.0", + "id": "00000000-0000-0000-0000-000000000000", + "packageName": "com.teams.sample.office365CardsBot", + "developer": { + "name": "Office365CardsBot", + "websiteUrl": "https://www.microsoft.com", + "privacyUrl": "https://www.teams.com/privacy", + "termsOfUseUrl": "https://www.teams.com/termsofuser" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "Office365CardsBot", + "full": "Office365CardsBot" + }, + "description": { + "short": "Office365CardsBot", + "full": "Office365CardsBot" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "scopes": [ + "groupchat", + "team", + "personal" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/office365Card/teams-app-manifest/outline.png b/libraries/botbuilder/tests/teams/office365Card/teams-app-manifest/outline.png new file mode 100644 index 0000000000..dbfa927729 Binary files /dev/null and b/libraries/botbuilder/tests/teams/office365Card/teams-app-manifest/outline.png differ diff --git a/libraries/botbuilder/tests/teams/office365Card/tsconfig.json b/libraries/botbuilder/tests/teams/office365Card/tsconfig.json new file mode 100644 index 0000000000..a168d60662 --- /dev/null +++ b/libraries/botbuilder/tests/teams/office365Card/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "composite": true, + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + } +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/replyToChannel/.env b/libraries/botbuilder/tests/teams/replyToChannel/.env new file mode 100644 index 0000000000..660828e3e8 --- /dev/null +++ b/libraries/botbuilder/tests/teams/replyToChannel/.env @@ -0,0 +1,2 @@ +MicrosoftAppId= +MicrosoftAppPassword= \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/replyToChannel/package.json b/libraries/botbuilder/tests/teams/replyToChannel/package.json new file mode 100644 index 0000000000..0386916c7a --- /dev/null +++ b/libraries/botbuilder/tests/teams/replyToChannel/package.json @@ -0,0 +1,30 @@ +{ + "name": "reply-to-channel", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "start": "tsc --build && node ./lib/index.js", + "build": "tsc --build", + "watch": "nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "botbuilder": "file:../../../", + "dotenv": "^8.1.0", + "node-fetch": "^2.6.0", + "restify": "^8.4.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^12.7.1", + "@types/node-fetch": "^2.5.0", + "@types/request": "^2.48.1", + "@types/restify": "^7.2.7", + "nodemon": "^1.19.1", + "ts-node": "^7.0.1", + "typescript": "^3.2.4" + } +} diff --git a/libraries/botbuilder/tests/teams/replyToChannel/src/index.ts b/libraries/botbuilder/tests/teams/replyToChannel/src/index.ts new file mode 100644 index 0000000000..0e6f77efca --- /dev/null +++ b/libraries/botbuilder/tests/teams/replyToChannel/src/index.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { BotFrameworkAdapter, MemoryStorage } from 'botbuilder'; + +// This bot's main dialog. +import { ReplyToChannelBot } from './replyToChannelBot'; + +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${server.name} listening to ${server.url}`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); + console.log(`\nTo test your bot, see: https://aka.ms/debug-with-emulator`); +}); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error('[onTurnError]:'); + console.error(error); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`); +}; + +// Create the main dialog. +const myBot = new ReplyToChannelBot(); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route to main dialog.5 + await myBot.run(context); + }); +}); diff --git a/libraries/botbuilder/tests/teams/replyToChannel/src/replyToChannelBot.ts b/libraries/botbuilder/tests/teams/replyToChannel/src/replyToChannelBot.ts new file mode 100644 index 0000000000..d63c3ac2b0 --- /dev/null +++ b/libraries/botbuilder/tests/teams/replyToChannel/src/replyToChannelBot.ts @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Activity, + MessageFactory, + BotFrameworkAdapter, + ChannelInfo, + ConversationParameters, + ConversationReference, + ConversationResourceResponse, + TeamsActivityHandler, + TeamsChannelData, + teamsGetChannelId, + TurnContext, +} from 'botbuilder'; +import { basename } from 'path'; + +export class ReplyToChannelBot extends TeamsActivityHandler { + botId: string; + + constructor() { + super(); + + // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + this.onMessage(async (context, next) => { + + const teamChannelId = teamsGetChannelId(context.activity); + const message = MessageFactory.text("good morning"); + const newConversation = await this.teamsCreateConversation(context, teamChannelId, message); + + const adapter = context.adapter as BotFrameworkAdapter; + + await adapter.continueConversation(newConversation[0], + async (t) => + { + await t.sendActivity(MessageFactory.text("good afternoon")); + await t.sendActivity(MessageFactory.text("good night")); + }); + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } + + + private async teamsCreateConversation(context: TurnContext, teamsChannelId: string, message: Partial): Promise<[ConversationReference, string]> { + if (!teamsChannelId) { + throw new Error('Missing valid teamsChannelId argument'); + } + + if (!message) { + throw new Error('Missing valid message argument'); + } + + const conversationParameters = { + isGroup: true, + channelData: { + channel: { + id: teamsChannelId + } + }, + + activity: message, + }; + + const adapter = context.adapter; + const connectorClient = adapter.createConnectorClient(context.activity.serviceUrl); + + // This call does NOT send the outbound Activity is not being sent through the middleware stack. + const conversationResourceResponse: ConversationResourceResponse = await connectorClient.conversations.createConversation(conversationParameters); + const conversationReference = TurnContext.getConversationReference(context.activity); + conversationReference.conversation.id = conversationResourceResponse.id; + return [conversationReference, conversationResourceResponse.activityId]; + } +} diff --git a/libraries/botbuilder/tests/teams/replyToChannel/teams-app-manifest/icon-color.png b/libraries/botbuilder/tests/teams/replyToChannel/teams-app-manifest/icon-color.png new file mode 100644 index 0000000000..bd9928dfc8 Binary files /dev/null and b/libraries/botbuilder/tests/teams/replyToChannel/teams-app-manifest/icon-color.png differ diff --git a/libraries/botbuilder/tests/teams/replyToChannel/teams-app-manifest/icon-outline.png b/libraries/botbuilder/tests/teams/replyToChannel/teams-app-manifest/icon-outline.png new file mode 100644 index 0000000000..45e7a7c56e Binary files /dev/null and b/libraries/botbuilder/tests/teams/replyToChannel/teams-app-manifest/icon-outline.png differ diff --git a/libraries/botbuilder/tests/teams/replyToChannel/teams-app-manifest/manifest.json b/libraries/botbuilder/tests/teams/replyToChannel/teams-app-manifest/manifest.json new file mode 100644 index 0000000000..0175acdfa7 --- /dev/null +++ b/libraries/botbuilder/tests/teams/replyToChannel/teams-app-manifest/manifest.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.3/MicrosoftTeams.schema.json", + "manifestVersion": "1.3", + "version": "1.0.0", + "id": "00000000-0000-0000-0000-000000000000", + "packageName": "com.teams.sample.replyToChannel", + "developer": { + "name": "ReplyToChannelBot", + "websiteUrl": "https://www.microsoft.com", + "privacyUrl": "https://www.teams.com/privacy", + "termsOfUseUrl": "https://www.teams.com/termsofuser" + }, + "icons": { + "color": "icon-color.png", + "outline": "icon-outline.png" + }, + "name": { + "short": "ReplyToChannelBot", + "full": "ReplyToChannelBot" + }, + "description": { + "short": "ReplyToChannelBot", + "full": "ReplyToChannelBot" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "scopes": [ + "groupchat", + "team", + "personal" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/replyToChannel/tsconfig.json b/libraries/botbuilder/tests/teams/replyToChannel/tsconfig.json new file mode 100644 index 0000000000..61761d2e32 --- /dev/null +++ b/libraries/botbuilder/tests/teams/replyToChannel/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "composite": true, + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src" + } +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/roster/.env b/libraries/botbuilder/tests/teams/roster/.env new file mode 100644 index 0000000000..53264abea7 --- /dev/null +++ b/libraries/botbuilder/tests/teams/roster/.env @@ -0,0 +1,3 @@ +MicrosoftAppId= +MicrosoftAppPassword= +AllowedTeamsTenantId= \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/roster/package.json b/libraries/botbuilder/tests/teams/roster/package.json new file mode 100644 index 0000000000..5968600f2f --- /dev/null +++ b/libraries/botbuilder/tests/teams/roster/package.json @@ -0,0 +1,30 @@ +{ + "name": "teams-roster-bot", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "start": "tsc --build && node ./lib/index.js", + "build": "tsc --build", + "watch": "nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "botbuilder": "file:../../../", + "dotenv": "^8.1.0", + "node-fetch": "^2.6.0", + "restify": "^8.4.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^12.7.1", + "@types/node-fetch": "^2.5.0", + "@types/request": "^2.48.1", + "@types/restify": "^7.2.7", + "nodemon": "^1.19.1", + "ts-node": "^7.0.1", + "typescript": "^3.2.4" + } +} diff --git a/libraries/botbuilder/tests/teams/roster/src/index.ts b/libraries/botbuilder/tests/teams/roster/src/index.ts new file mode 100644 index 0000000000..74ca6dae03 --- /dev/null +++ b/libraries/botbuilder/tests/teams/roster/src/index.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { BotFrameworkAdapter } from 'botbuilder'; + +// This bot's main dialog. +import { RosterBot } from './rosterBot'; + +// Import middleware for filtering messages based on Teams Tenant Id +import { TeamsTenantFilteringMiddleware } from './teamsTenantFilteringMiddleware'; + +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${server.name} listening to ${server.url}`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); + console.log(`\nTo test your bot, see: https://aka.ms/debug-with-emulator`); +}); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// Use the TeamsTenantFilteringMiddleware IF there is an AllowedTeamsTenantId +if(process.env.AllowedTeamsTenantId){ + let teamsTenantFilteringMiddleware = new TeamsTenantFilteringMiddleware(process.env.AllowedTeamsTenantId); + adapter.use(teamsTenantFilteringMiddleware); +} + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error('[onTurnError]:'); + console.error(error); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`); +}; + +// Create the main dialog. +const myBot = new RosterBot(); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route to main dialog. + await myBot.run(context); + }); +}); diff --git a/libraries/botbuilder/tests/teams/roster/src/rosterBot.ts b/libraries/botbuilder/tests/teams/roster/src/rosterBot.ts new file mode 100644 index 0000000000..54f82ee7b8 --- /dev/null +++ b/libraries/botbuilder/tests/teams/roster/src/rosterBot.ts @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Activity, + MessageFactory, + TeamsActivityHandler, + TeamsInfo, + TurnContext +} from 'botbuilder'; + +// +// You need to install this bot in a team. You can @mention the bot "show members", "show channels", or "show details" to get the +// members of the team, the channels of the team, or metadata about the team respectively. +// +export class RosterBot extends TeamsActivityHandler { + constructor() { + super(); + + // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + this.onMessage(async (context, next) => { + await context.sendActivity(MessageFactory.text(`Echo: ${context.activity.text}`)); + TurnContext.removeRecipientMention(context.activity); + const text = context.activity.text ? context.activity.text.trim() : ''; + switch (text) { + case "show members": + await this.showMembers(context); + break; + + case "show channels": + await this.showChannels(context); + break; + + case "show details": + await this.showDetails(context); + break; + + default: + await context.sendActivity( + 'Invalid command. Type "Show channels" to see a channel list. Type "Show members" to see a list of members in a team. ' + + 'Type "show group chat members" to see members in a group chat.'); + break; + } + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + + this.onMembersAdded(async (context, next) => { + const membersAdded = context.activity.membersAdded; + for (const member of membersAdded) { + if (member.id !== context.activity.recipient.id) { + await context.sendActivity('Hello and welcome!'); + } + } + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } + + private async showMembers(context: TurnContext): Promise { + let teamsChannelAccounts = await TeamsInfo.getMembers(context); + await context.sendActivity(MessageFactory.text(`Total of ${teamsChannelAccounts.length} members are currently in team`)); + let messages = teamsChannelAccounts.map(function(teamsChannelAccount) { + return `${teamsChannelAccount.aadObjectId} --> ${teamsChannelAccount.name} --> ${teamsChannelAccount.userPrincipalName}`; + }); + await this.sendInBatches(context, messages); + } + + private async showChannels(context: TurnContext): Promise { + let channels = await TeamsInfo.getTeamChannels(context); + await context.sendActivity(MessageFactory.text(`Total of ${channels.length} channels are currently in team`)); + let messages = channels.map(function(channel) { + return `${channel.id} --> ${channel.name ? channel.name : 'General'}`; + }); + await this.sendInBatches(context, messages); + } + + private async showDetails(context: TurnContext): Promise { + let teamDetails = await TeamsInfo.getTeamDetails(context); + await context.sendActivity(MessageFactory.text(`The team name is ${teamDetails.name}. The team ID is ${teamDetails.id}. The AAD GroupID is ${teamDetails.aadGroupId}.`)); + } + + private async sendInBatches(context: TurnContext, messages: string[]): Promise { + let batch: string[] = []; + messages.forEach(async (msg: string) => { + batch.push(msg); + if (batch.length == 10) { + await context.sendActivity(MessageFactory.text(batch.join('
'))); + batch = []; + } + }); + + if (batch.length > 0) { + await context.sendActivity(MessageFactory.text(batch.join('
'))); + } + } +} diff --git a/libraries/botbuilder/tests/teams/roster/src/teamsTenantFilteringMiddleware.ts b/libraries/botbuilder/tests/teams/roster/src/teamsTenantFilteringMiddleware.ts new file mode 100644 index 0000000000..d0f6a500c0 --- /dev/null +++ b/libraries/botbuilder/tests/teams/roster/src/teamsTenantFilteringMiddleware.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + TurnContext, +} from 'botbuilder'; + +import { + Middleware, + TeamsChannelData, +} from 'botbuilder-core'; + +// +// Teams specific middleware for filtering messages by Tenant Id. +// +export class TeamsTenantFilteringMiddleware implements Middleware { + // tslint:disable:variable-name + private readonly _tenantSet = new Set(); + // tslint:enable:variable-name + + /** + * Initializes a new instance of the TeamsTenantFilteringMiddleware class. + * * @param allowedTenants either a single Tenant Id or array of Tenant Ids. + */ + constructor(allowedTenants: string | string[]) { + if(Array.isArray(allowedTenants)){ + for (let elem of allowedTenants) { + this._tenantSet.add(elem); + } + } + else { + this._tenantSet.add(allowedTenants); + } + } + + /** + * Store the incoming activity on the App Insights Correlation Context and optionally calls the TelemetryLoggerMiddleware + * @param context The context object for this turn. + * @param next The delegate to call to continue the bot middleware pipeline + */ + public async onTurn(context: TurnContext, next: () => Promise): Promise { + if (context === null) { + throw new Error('context is null'); + } + + if (context.activity.channelId !== 'msteams') { + // If the goal is to NOT process messages from other channels, comment out the following line + // and message processing will be 'short circuited'. + if (next !== null) { + await next(); + } + return; + } + + const channelData = context.activity.channelData as TeamsChannelData; + const tenant = channelData && channelData.tenant ? channelData.tenant : undefined; + const tenantId = tenant && typeof(tenant.id) === 'string' ? tenant.id : undefined; + + if (!tenantId) { + throw new Error("Tenant Id is missing."); + } + + if (!this._tenantSet.has(tenantId)) { + throw new Error(`Tenant Id '${tenantId}' is not allowed access.`); + } + + if (next !== null) { + await next(); + } + } +} diff --git a/libraries/botbuilder/tests/teams/roster/teams-app-manifest/color.png b/libraries/botbuilder/tests/teams/roster/teams-app-manifest/color.png new file mode 100644 index 0000000000..48a2de1330 Binary files /dev/null and b/libraries/botbuilder/tests/teams/roster/teams-app-manifest/color.png differ diff --git a/libraries/botbuilder/tests/teams/roster/teams-app-manifest/manifest.json b/libraries/botbuilder/tests/teams/roster/teams-app-manifest/manifest.json new file mode 100644 index 0000000000..6d363ee2a7 --- /dev/null +++ b/libraries/botbuilder/tests/teams/roster/teams-app-manifest/manifest.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.3/MicrosoftTeams.schema.json", + "manifestVersion": "1.3", + "version": "1.0.0", + "id": "00000000-0000-0000-0000-000000000000", + "packageName": "com.teams.sample.rosterBot", + "developer": { + "name": "TeamsRosterBot", + "websiteUrl": "https://www.microsoft.com", + "privacyUrl": "https://www.teams.com/privacy", + "termsOfUseUrl": "https://www.teams.com/termsofuser" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "TeamsRosterBot", + "full": "TeamsRosterBot" + }, + "description": { + "short": "TeamsRosterBot", + "full": "TeamsRosterBot" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "scopes": [ + "groupchat", + "team" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/roster/teams-app-manifest/outline.png b/libraries/botbuilder/tests/teams/roster/teams-app-manifest/outline.png new file mode 100644 index 0000000000..dbfa927729 Binary files /dev/null and b/libraries/botbuilder/tests/teams/roster/teams-app-manifest/outline.png differ diff --git a/libraries/botbuilder/tests/teams/roster/tsconfig.json b/libraries/botbuilder/tests/teams/roster/tsconfig.json new file mode 100644 index 0000000000..a168d60662 --- /dev/null +++ b/libraries/botbuilder/tests/teams/roster/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "composite": true, + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + } +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/.env b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/.env new file mode 100644 index 0000000000..edaf1908bc --- /dev/null +++ b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/.env @@ -0,0 +1,3 @@ +MicrosoftAppId= +MicrosoftAppPassword= +host= \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/README.md b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/README.md new file mode 100644 index 0000000000..450a22c0f6 --- /dev/null +++ b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/README.md @@ -0,0 +1,84 @@ +# Teams Search-based Messaging Extensions Bot + +Search-based Messaging Extensions allow users to have bots search for information on the user's behalf in Teams. + +___ + +To create a Teams Bot that contains Search-based Messaging Extensions, users must first create a [Teams Manifest](https://docs.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) which will outline where the extension is available. + +> _Note_ +> +> _The name of the feature was changed from "compose extension" to "messaging extension" in November, 2017, but the manifest name remains the same so that existing extensions continue to function._ + +### Example Teams Manifest for Search-based Messaging Extensions bot + +```json +{ + "$schema": "https://github.com/OfficeDev/microsoft-teams-app-schema/blob/preview/DevPreview/MicrosoftTeams.schema.json", + "manifestVersion": "devPreview", + "version": "1.0", + "id": "<>", + "packageName": "com.microsoft.teams.samples.v4bot", + "developer": { + "name": "Microsoft Corp", + "websiteUrl": "https://example.azurewebsites.net", + "privacyUrl": "https://example.azurewebsites.net/privacy", + "termsOfUseUrl": "https://example.azurewebsites.net/termsofuse" + }, + "name": { + "short": "search-extension-settings", + "full": "Microsoft Teams V4 Search Messaging Extension Bot and settings" + }, + "description": { + "short": "Microsoft Teams V4 Search Messaging Extension Bot and settings", + "full": "Sample Search Messaging Extension Bot using V4 Bot Builder SDK and V4 Microsoft Teams Extension SDK" + }, + "icons": { + "outline": "icon-outline.png", + "color": "icon-color.png" + }, + "accentColor": "#abcdef", + "bots": [ + { + "botId": "<>", + "scopes": ["personal", "team"] + } + ], + "composeExtensions": [ + { + "botId": "<>", + "canUpdateConfiguration": true, + "commands": [ + { + "id": "searchQuery", + "context": ["compose", "commandBox"], + "description": "Test command to run query", + "title": "Search", + "type": "query", + "parameters": [ + { + "name": "searchQuery", + "title": "Search Query", + "description": "Your search query", + "inputType": "text" + } + ] + } + ], + "messageHandlers": [ + { + "type": "link", + "value": { + "domains": [ + "*.ngrok.io" + ] + } + } + ] + } + ], + "validDomains": [ + "*.ngrok.io" + ] +} +``` \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/package.json b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/package.json new file mode 100644 index 0000000000..e2e018620c --- /dev/null +++ b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/package.json @@ -0,0 +1,30 @@ +{ + "name": "search-extension-bot", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "start": "tsc --build && node ./lib/index.js", + "build": "tsc --build", + "watch": "nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "botbuilder": "file:../../../", + "dotenv": "^8.1.0", + "node-fetch": "^2.6.0", + "restify": "^8.4.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^12.7.1", + "@types/node-fetch": "^2.5.0", + "@types/request": "^2.48.1", + "@types/restify": "^7.2.7", + "nodemon": "^1.19.1", + "ts-node": "^7.0.1", + "typescript": "^3.2.4" + } +} diff --git a/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/src/index.ts b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/src/index.ts new file mode 100644 index 0000000000..bae0bcf49a --- /dev/null +++ b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/src/index.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { BotFrameworkAdapter, MemoryStorage, UserState } from 'botbuilder'; + +// This bot's main dialog. +import { TeamsSearchExtensionBot } from './teamsSearchExtensionBot'; + +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${server.name} listening to ${server.url}`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); + console.log(`\nTo test your bot, see: https://aka.ms/debug-with-emulator`); +}); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error('[onTurnError]:'); + console.error(error); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`); +}; + +// Define a state store for your bot. See https://aka.ms/about-bot-state to learn more about using MemoryStorage. +// A bot requires a state store to persist the dialog and user state between messages. + +// For local development, in-memory storage is used. +// CAUTION: The Memory Storage used here is for local bot debugging only. When the bot +// is restarted, anything stored in memory will be gone. +const memoryStorage = new MemoryStorage(); +const userState = new UserState(memoryStorage); + +// Create the main dialog. +const myBot = new TeamsSearchExtensionBot(userState); + +server.get('/*', restify.plugins.serveStatic({ + directory: path.join(__dirname, '../static'), + appendRequestPath: false +})); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route to main dialog. + await myBot.run(context); + }); +}); diff --git a/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/src/teamsSearchExtensionBot.ts b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/src/teamsSearchExtensionBot.ts new file mode 100644 index 0000000000..939e9839cf --- /dev/null +++ b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/src/teamsSearchExtensionBot.ts @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Attachment, + BotState, + CardAction, + CardFactory, + HeroCard, + MessagingExtensionAttachment, + MessagingExtensionQuery, + MessagingExtensionResponse, + MessagingExtensionResult, + TeamsActivityHandler, + TurnContext +} from 'botbuilder'; + +export class TeamsSearchExtensionBot extends TeamsActivityHandler { + + // For this example, we're using UserState to store the user's preferences for the type of Rich Card they receive. + // Users can specify the type of card they receive by configuring the Messaging Extension. + // To store their configuration, we will use the userState passed in via the constructor. + + /** + * We need to change the key for the user state because the bot might not be in the conversation, which means they get a 403 error. + * @param userState + */ + constructor(public userState: BotState) { + super(); + + // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + this.onMessage(async (context, next) => { + await context.sendActivity(`You said '${context.activity.text}'`); + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + + this.onMembersAdded(async (context, next) => { + const membersAdded = context.activity.membersAdded; + for (const member of membersAdded) { + if (member.id !== context.activity.recipient.id) { + await context.sendActivity('Hello and welcome!'); + } + } + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } + + protected async handleTeamsMessagingExtensionQuery(context: TurnContext, query: MessagingExtensionQuery): Promise{ + const searchQuery = query.parameters[0].value; + const composeExtension = this.createMessagingExtensionResult([ + this.createSearchResultAttachment(searchQuery), + this.createDummySearchResultAttachment(), + this.createSelectItemsResultAttachment(searchQuery) + ]); + + return { + composeExtension: composeExtension + }; + } + + protected async handleTeamsMessagingExtensionSelectItem(context: TurnContext, query: any): Promise { + const searchQuery = query.query; + const bfLogo = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU"; + const card = CardFactory.heroCard('You selected a search result!', `You searched for "${searchQuery}"`, [bfLogo]); + + return { + composeExtension: this.createMessagingExtensionResult([card]) + }; + } + + private createMessagingExtensionResult(attachments: Attachment[]) : MessagingExtensionResult { + return { + type: "result", + attachmentLayout: "list", + attachments: attachments + }; + } + + private createSearchResultAttachment(searchQuery: string) : MessagingExtensionAttachment { + const cardText = `You said \"${searchQuery}\"`; + const bfLogo = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU"; + + const button = { + type: "openUrl", + title: "Click for more Information", + value: "https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/bots/bots-overview" + }; + + const heroCard = CardFactory.heroCard("You searched for:", cardText, [bfLogo], [button]); + const preview = CardFactory.heroCard("You searched for:", cardText, [bfLogo]); + + return { + contentType: CardFactory.contentTypes.heroCard, + content: heroCard, + preview: preview + }; + } + + private createDummySearchResultAttachment() : MessagingExtensionAttachment { + const cardText = "https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/bots/bots-overview"; + const bfLogo = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU"; + + const button = { + type: "openUrl", + title: "Click for more Information", + value: "https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/bots/bots-overview" + }; + + const heroCard = CardFactory.heroCard("Learn more about Teams:", cardText, [bfLogo], [button]); + const preview = CardFactory.heroCard("Learn more about Teams:", cardText, [bfLogo]); + + return { + contentType: CardFactory.contentTypes.heroCard, + content: heroCard, + preview: preview + }; + } + + private createSelectItemsResultAttachment(searchQuery: string): MessagingExtensionAttachment { + const bfLogo = 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU'; + const cardText = `You said: "${searchQuery}"`; + const heroCard = CardFactory.heroCard(cardText, cardText, [bfLogo]); + + const selectItemTap = { + type: "invoke", + value: { query: searchQuery } + }; + + const preview = CardFactory.heroCard(cardText, cardText, [bfLogo], null, null); + const card: Partial = preview.content; + card.tap = selectItemTap; + + return { + contentType: CardFactory.contentTypes.heroCard, + content: heroCard, + preview: preview, + }; + } +} diff --git a/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/static/composeExtensionSettings.html b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/static/composeExtensionSettings.html new file mode 100644 index 0000000000..2efc693b98 --- /dev/null +++ b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/static/composeExtensionSettings.html @@ -0,0 +1,31 @@ + + + + Bot Info + + + + + + + +

I prefer:

+ + + + + \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/teams-app-manifest/icon-color.png b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/teams-app-manifest/icon-color.png new file mode 100644 index 0000000000..bd9928dfc8 Binary files /dev/null and b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/teams-app-manifest/icon-color.png differ diff --git a/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/teams-app-manifest/icon-outline.png b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/teams-app-manifest/icon-outline.png new file mode 100644 index 0000000000..45e7a7c56e Binary files /dev/null and b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/teams-app-manifest/icon-outline.png differ diff --git a/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/teams-app-manifest/manifest.json b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/teams-app-manifest/manifest.json new file mode 100644 index 0000000000..a15cff2ff0 --- /dev/null +++ b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/teams-app-manifest/manifest.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://github.com/OfficeDev/microsoft-teams-app-schema/blob/preview/DevPreview/MicrosoftTeams.schema.json", + "manifestVersion": "1.5", + "version": "1.0", + "id": "00000000-0000-0000-0000-000000000000", + "packageName": "com.microsoft.teams.samples.searchExtension", + "developer": { + "name": "Microsoft Corp", + "websiteUrl": "https://example.azurewebsites.net", + "privacyUrl": "https://example.azurewebsites.net/privacy", + "termsOfUseUrl": "https://example.azurewebsites.net/termsofuse" + }, + "name": { + "short": "search-extension-settings", + "full": "Microsoft Teams V4 Search Messaging Extension Bot and settings" + }, + "description": { + "short": "Microsoft Teams V4 Search Messaging Extension Bot and settings", + "full": "Sample Search Messaging Extension Bot using V4 Bot Builder SDK and V4 Microsoft Teams Extension SDK" + }, + "icons": { + "outline": "icon-outline.png", + "color": "icon-color.png" + }, + "accentColor": "#abcdef", + "bots": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "scopes": ["personal", "team"] + } + ], + "composeExtensions": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "canUpdateConfiguration": true, + "commands": [ + { + "id": "searchQuery", + "context": ["compose", "commandBox"], + "description": "Test command to run query", + "title": "Search", + "type": "query", + "parameters": [ + { + "name": "searchQuery", + "title": "Search Query", + "description": "Your search query", + "inputType": "text" + } + ] + } + ], + "messageHandlers": [ + { + "type": "link", + "value": { + "domains": [ + "*.ngrok.io" + ] + } + } + ] + } + ], + "validDomains": [ + "*.ngrok.io" + ] +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/tsconfig.json b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/tsconfig.json new file mode 100644 index 0000000000..a168d60662 --- /dev/null +++ b/libraries/botbuilder/tests/teams/searchBasedMessagingExtension/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "composite": true, + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + } +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/taskModule/.env b/libraries/botbuilder/tests/teams/taskModule/.env new file mode 100644 index 0000000000..a695b3bf05 --- /dev/null +++ b/libraries/botbuilder/tests/teams/taskModule/.env @@ -0,0 +1,2 @@ +MicrosoftAppId= +MicrosoftAppPassword= diff --git a/libraries/botbuilder/tests/teams/taskModule/package.json b/libraries/botbuilder/tests/teams/taskModule/package.json new file mode 100644 index 0000000000..445c5a0360 --- /dev/null +++ b/libraries/botbuilder/tests/teams/taskModule/package.json @@ -0,0 +1,30 @@ +{ + "name": "task-module", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "start": "tsc --build && node ./lib/index.js", + "build": "tsc --build", + "watch": "nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "botbuilder": "file:../../../", + "dotenv": "^8.1.0", + "node-fetch": "^2.6.0", + "restify": "^8.4.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^12.7.1", + "@types/node-fetch": "^2.5.0", + "@types/request": "^2.48.1", + "@types/restify": "^7.2.7", + "nodemon": "^1.19.1", + "ts-node": "^7.0.1", + "typescript": "^3.2.4" + } +} diff --git a/libraries/botbuilder/tests/teams/taskModule/src/index.ts b/libraries/botbuilder/tests/teams/taskModule/src/index.ts new file mode 100644 index 0000000000..4bc9c89168 --- /dev/null +++ b/libraries/botbuilder/tests/teams/taskModule/src/index.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { BotFrameworkAdapter } from 'botbuilder'; + +// This bot's main dialog. +import { TaskModuleBot } from './taskModuleBot'; + +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${server.name} listening to ${server.url}`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); + console.log(`\nTo test your bot, see: https://aka.ms/debug-with-emulator`); +}); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// adapter.use(new TranscriptLoggerMiddleware(new FileTranscriptStore('./transcripts'))); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error('[onTurnError]:'); + console.error(error); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong in the bot!\n ${error.message}`); +}; + +// Create the main dialog. +const myBot = new TaskModuleBot(); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route to main dialog. + await myBot.run(context); + }); +}); diff --git a/libraries/botbuilder/tests/teams/taskModule/src/taskModuleBot.ts b/libraries/botbuilder/tests/teams/taskModule/src/taskModuleBot.ts new file mode 100644 index 0000000000..603851273c --- /dev/null +++ b/libraries/botbuilder/tests/teams/taskModule/src/taskModuleBot.ts @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + TeamsActivityHandler, +} from 'botbuilder'; +import { + Attachment, + CardFactory, + MessageFactory, + TaskModuleContinueResponse, + TaskModuleMessageResponse, + TaskModuleRequest, + TaskModuleResponse, + TaskModuleTaskInfo, + TurnContext, +} from 'botbuilder-core'; + +export class TaskModuleBot extends TeamsActivityHandler { + constructor() { + super(); + + // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + this.onMessage(async (context, next) => { + const card = this.getTaskModuleHeroCard(); + const message = MessageFactory.attachment(card); + await context.sendActivity(message); + await next(); + }); + } + + protected async handleTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { + var reply = MessageFactory.text("handleTeamsTaskModuleFetchAsync TaskModuleRequest" + JSON.stringify(taskModuleRequest)); + await context.sendActivity(reply); + + return { + task: { + type: "continue", + value: { + card: this.getTaskModuleAdaptiveCard(), + height: 200, + width: 400, + title: "Adaptive Card: Inputs", + } as TaskModuleTaskInfo, + } as TaskModuleContinueResponse + } as TaskModuleResponse; + } + + protected async handleTeamsTaskModuleSubmit(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { + var reply = MessageFactory.text("handleTeamsTaskModuleFetchAsync Value: " + JSON.stringify(taskModuleRequest)); + await context.sendActivity(reply); + + return { + task: { + type: "message", + value: "Hello", + } as TaskModuleMessageResponse + } as TaskModuleResponse; + } + + private getTaskModuleHeroCard() : Attachment { + return CardFactory.heroCard("Task Module Invocation from Hero Card", + "This is a hero card with a Task Module Action button. Click the button to show an Adaptive Card within a Task Module.", + null, // No images + [{type: "invoke", title:"Adaptive Card", value: {type:"task/fetch", data:"adaptivecard"} }] + ); + } + + private getTaskModuleAdaptiveCard(): Attachment { + return CardFactory.adaptiveCard({ + version: '1.0.0', + type: 'AdaptiveCard', + body: [ + { + type: 'TextBlock', + text: `Enter Text Here`, + }, + { + type: 'Input.Text', + id: 'usertext', + placeholder: 'add some text and submit', + IsMultiline: true, + } + ], + actions: [ + { + type: 'Action.Submit', + title: 'Submit', + } + ] + }); + } + +} diff --git a/libraries/botbuilder/tests/teams/taskModule/teams-app-manifest/icon-color.png b/libraries/botbuilder/tests/teams/taskModule/teams-app-manifest/icon-color.png new file mode 100644 index 0000000000..bd9928dfc8 Binary files /dev/null and b/libraries/botbuilder/tests/teams/taskModule/teams-app-manifest/icon-color.png differ diff --git a/libraries/botbuilder/tests/teams/taskModule/teams-app-manifest/icon-outline.png b/libraries/botbuilder/tests/teams/taskModule/teams-app-manifest/icon-outline.png new file mode 100644 index 0000000000..45e7a7c56e Binary files /dev/null and b/libraries/botbuilder/tests/teams/taskModule/teams-app-manifest/icon-outline.png differ diff --git a/libraries/botbuilder/tests/teams/taskModule/teams-app-manifest/manifest.json b/libraries/botbuilder/tests/teams/taskModule/teams-app-manifest/manifest.json new file mode 100644 index 0000000000..cd49680f2f --- /dev/null +++ b/libraries/botbuilder/tests/teams/taskModule/teams-app-manifest/manifest.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", + "manifestVersion": "1.5", + "version": "1.0.0", + "id": "00000000-0000-0000-0000-000000000000", + "packageName": "com.teams.microsoft.bot.test", + "developer": { + "name": "Bot Framework Team", + "websiteUrl": "https://dev.botframework.com", + "privacyUrl": "https://dev.botframework.com", + "termsOfUseUrl": "https://dev.botframework.com" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "Task Module", + "full": "Simple Task Module" + }, + "description": { + "short": "Test Task Module Scenario", + "full": "Simple Task Module Scenario Test" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "00000000-0000-0000-0000-000000000000", + "scopes": [ + "personal", + "team", + "groupchat" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], +"validDomains": [ + "YourBotWebApp.azurewebsites.net" +] +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teams/taskModule/tsconfig.json b/libraries/botbuilder/tests/teams/taskModule/tsconfig.json new file mode 100644 index 0000000000..a168d60662 --- /dev/null +++ b/libraries/botbuilder/tests/teams/taskModule/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "composite": true, + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + } +} \ No newline at end of file diff --git a/libraries/botbuilder/tests/teamsActivityHandler.test.js b/libraries/botbuilder/tests/teamsActivityHandler.test.js new file mode 100644 index 0000000000..025bf214b3 --- /dev/null +++ b/libraries/botbuilder/tests/teamsActivityHandler.test.js @@ -0,0 +1,808 @@ +const assert = require('assert'); +const { TeamsActivityHandler } = require('../'); +const { ActivityTypes, TestAdapter } = require('botbuilder-core'); + +function createInvokeActivity(name, value = {}, channelData = {}) { + const activity = { + type: ActivityTypes.Invoke, + channelData, + name, + value, + }; + return activity; +} + +describe('TeamsActivityHandler', () => { + describe('onTurnActivity()', () => { + it('should not override the InvokeResponse on the context.turnState if it is set', done => { + class InvokeHandler extends TeamsActivityHandler { + async onInvokeActivity(context) { + assert(context, 'context not found'); + await context.sendActivity({ type: 'invokeResponse', value: { status: 200, body: `I'm a teapot.` } }); + return { status: 418 }; + } + } + + const bot = new InvokeHandler(); + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + adapter.send({ type: ActivityTypes.Invoke }) + .assertReply(activity => { + assert.strictEqual(activity.type, 'invokeResponse'); + assert(activity.value, 'activity.value not found'); + assert.strictEqual(activity.value.status, 200); + assert.strictEqual(activity.value.body, `I'm a teapot.`); + done(); + }) + .catch(err => done(err)); + + }); + + it('should call onTurnActivity if non-Invoke is received', done => { + const bot = new TeamsActivityHandler(); + bot.onMessage(async (context, next) => { + await context.sendActivity('Hello'); + await next(); + }); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + adapter.send({ type: ActivityTypes.Message, text: 'Hello' }) + .assertReply(activity => { + assert.strictEqual(activity.type, ActivityTypes.Message); + assert.strictEqual(activity.text, 'Hello'); + done(); + }) + .catch(err => done(err)); + }); + }); + + describe('should send a BadRequest status code if', () => { + it('a bad BotMessagePreview.action is received by the bot', async () => { + const bot = new TeamsActivityHandler(); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const fileConsentActivity = createInvokeActivity('composeExtension/submitAction', { botMessagePreviewAction: 'this.is.a.bad.action' }); + adapter.send(fileConsentActivity) + .assertReply(activity => { + assert(activity.type === 'invokeResponse', `incorrect activity type "${ activity.type }", expected "invokeResponse"`); + assert(activity.value.status === 400, `incorrect status code "${ activity.value.status }", expected "400"`); + assert(!activity.value.body, + `expected empty body for invokeResponse from fileConsent flow.\nReceived: ${ JSON.stringify(activity.value.body) }`); + }); + }); + + it('a bad FileConsentCardResponse.action is received by the bot', async () => { + const bot = new TeamsActivityHandler(); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const fileConsentActivity = createInvokeActivity('fileConsent/invoke', { action: 'this.is.a.bad.action' }); + adapter.send(fileConsentActivity) + .assertReply(activity => { + assert(activity.type === 'invokeResponse', `incorrect activity type "${ activity.type }", expected "invokeResponse"`); + assert(activity.value.status === 400, `incorrect status code "${ activity.value.status }", expected "400"`); + assert(!activity.value.body, + `expected empty body for invokeResponse from fileConsent flow.\nReceived: ${ JSON.stringify(activity.value.body) }`); + }); + }); + }); + + describe('should send a NotImplemented status code if', () => { + it('handleTeamsMessagingExtensionSubmitAction is not overridden', async () => { + const bot = new TeamsActivityHandler(); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const fileConsentActivity = createInvokeActivity('composeExtension/submitAction'); + adapter.send(fileConsentActivity) + .assertReply(activity => { + assert(activity.type === 'invokeResponse', `incorrect activity type "${ activity.type }", expected "invokeResponse"`); + assert(activity.value.status === 501, `incorrect status code "${ activity.value.status }", expected "501"`); + assert(!activity.value.body, + `expected empty body for invokeResponse from fileConsent flow.\nReceived: ${ JSON.stringify(activity.value.body) }`); + }); + }); + + it('onTeamsBotMessagePreviewEdit is not overridden', async () => { + const bot = new TeamsActivityHandler(); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const fileConsentActivity = createInvokeActivity('composeExtension/submitAction', { botMessagePreviewAction: 'edit' }); + adapter.send(fileConsentActivity) + .assertReply(activity => { + assert(activity.type === 'invokeResponse', `incorrect activity type "${ activity.type }", expected "invokeResponse"`); + assert(activity.value.status === 501, `incorrect status code "${ activity.value.status }", expected "501"`); + assert(!activity.value.body, + `expected empty body for invokeResponse from fileConsent flow.\nReceived: ${ JSON.stringify(activity.value.body) }`); + }); + }); + + it('onTeamsBotMessagePreviewSend is not overridden', async () => { + const bot = new TeamsActivityHandler(); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const fileConsentActivity = createInvokeActivity('composeExtension/submitAction', { botMessagePreviewAction: 'send' }); + adapter.send(fileConsentActivity) + .assertReply(activity => { + assert(activity.type === 'invokeResponse', `incorrect activity type "${ activity.type }", expected "invokeResponse"`); + assert(activity.value.status === 501, `incorrect status code "${ activity.value.status }", expected "501"`); + assert(!activity.value.body, + `expected empty body for invokeResponse from fileConsent flow.\nReceived: ${ JSON.stringify(activity.value.body) }`); + }); + }); + + it('handleTeamsFileConsentAccept is not overridden', async () => { + const bot = new TeamsActivityHandler(); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const fileConsentActivity = createInvokeActivity('fileConsent/invoke', { action: 'accept' }); + adapter.send(fileConsentActivity) + .assertReply(activity => { + assert(activity.type === 'invokeResponse', `incorrect activity type "${ activity.type }", expected "invokeResponse"`); + assert(activity.value.status === 501, `incorrect status code "${ activity.value.status }", expected "501"`); + assert(!activity.value.body, + `expected empty body for invokeResponse from fileConsent flow.\nReceived: ${ JSON.stringify(activity.value.body) }`); + }); + }); + + it('handleTeamsFileConsentDecline is not overridden', async () => { + const bot = new TeamsActivityHandler(); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const fileConsentActivity = createInvokeActivity('fileConsent/invoke', { action: 'decline' }); + adapter.send(fileConsentActivity) + .assertReply(activity => { + assert.strictEqual(activity.type, 'invokeResponse'); + assert.strictEqual(activity.value.status, 501); + assert.strictEqual(activity.value.body, undefined); + }); + }); + + it('handleTeamsO365ConnectorCardAction is not overridden', async () => { + const bot = new TeamsActivityHandler(); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const teamsO365ConnectorCardActionActivity = createInvokeActivity('actionableMessage/executeAction'); + adapter.send(teamsO365ConnectorCardActionActivity) + .assertReply(activity => { + assert.strictEqual(activity.type, 'invokeResponse'); + assert.strictEqual(activity.value.status, 501); + assert.strictEqual(activity.value.body, undefined); + }); + }); + + it('handleTeamsSigninVerifyState is not overridden', async () => { + const bot = new TeamsActivityHandler(); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const signinVerifyStateActivity = createInvokeActivity('signin/verifyState'); + adapter.send(signinVerifyStateActivity) + .assertReply(activity => { + assert.strictEqual(activity.type, 'invokeResponse'); + assert.strictEqual(activity.value.status, 501); + assert.strictEqual(activity.value.body, undefined); + }); + }); + + it('handleTeamsMessagingExtensionCardButtonClicked is not overridden', async () => { + const bot = new TeamsActivityHandler(); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const cardButtonClickedActivity = createInvokeActivity('composeExtension/onCardButtonClicked'); + adapter.send(cardButtonClickedActivity) + .assertReply(activity => { + assert.strictEqual(activity.type, 'invokeResponse'); + assert.strictEqual(activity.value.status, 501); + assert.strictEqual(activity.value.body, undefined); + }); + }); + + it('handleTeamsTaskModuleFetch is not overridden', async () => { + const bot = new TeamsActivityHandler(); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const taskFetchActivity = createInvokeActivity('task/fetch'); + adapter.send(taskFetchActivity) + .assertReply(activity => { + assert.strictEqual(activity.type, 'invokeResponse'); + assert.strictEqual(activity.value.status, 501); + assert.strictEqual(activity.value.body, undefined); + }); + }); + + it('handleTeamsTaskModuleSubmit is not overridden', async () => { + const bot = new TeamsActivityHandler(); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const taskSubmitActivity = createInvokeActivity('task/submit'); + adapter.send(taskSubmitActivity) + .assertReply(activity => { + assert.strictEqual(activity.type, 'invokeResponse'); + assert.strictEqual(activity.value.status, 501); + assert.strictEqual(activity.value.body, undefined); + }); + }); + }); + + describe('should send an OK status code', () => { + class OKFileConsent extends TeamsActivityHandler { + async handleTeamsFileConsentAccept(context, fileConsentCardResponse) { + assert(context, 'context not found'); + assert(fileConsentCardResponse, 'fileConsentCardResponse not found'); + } + + async handleTeamsFileConsentDecline(context, fileConsentCardResponse) { + assert(context, 'context not found'); + assert(fileConsentCardResponse, 'fileConsentCardResponse not found'); + } + } + + it('when a "fileConsent/invoke" activity is handled by handleTeamsFileConsentAccept', async () => { + const bot = new OKFileConsent(); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const fileConsentActivity = createInvokeActivity('fileConsent/invoke', { action: 'accept' }); + adapter.send(fileConsentActivity) + .assertReply(activity => { + assert(activity.type === 'invokeResponse', `incorrect activity type "${activity.type}", expected "invokeResponse"`); + assert(activity.value.status === 200, `incorrect status code "${activity.value.status}", expected "200"`); + assert(!activity.value.body, + `expected empty body for invokeResponse from fileConsent flow.\nReceived: ${JSON.stringify(activity.value.body)}`); + }); + }); + + it('when a "fileConsent/invoke" activity is handled by handleTeamsFileConsentDecline', async () => { + const bot = new OKFileConsent(); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const fileConsentActivity = createInvokeActivity('fileConsent/invoke', { action: 'decline' }); + adapter.send(fileConsentActivity) + .assertReply(activity => { + assert(activity.type === 'invokeResponse', `incorrect activity type "${activity.type}", expected "invokeResponse"`); + assert(activity.value.status === 200, `incorrect status code "${activity.value.status}", expected "200"`); + assert(!activity.value.body, + `expected empty body for invokeResponse from fileConsent flow.\nReceived: ${JSON.stringify(activity.value.body)}`); + }); + }); + + it('when a "fileConsent/invoke" activity handled by handleTeamsFileConsent', async () => { + class FileConsent extends TeamsActivityHandler { + async handleTeamsFileConsent(context, fileConsentCardResponse) { + assert(context, 'context not found'); + assert(fileConsentCardResponse, 'fileConsentCardResponse not found'); + } + } + const bot = new FileConsent(); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const fileConsentActivity = createInvokeActivity('fileConsent/invoke', { action: 'accept' }); + adapter.send(fileConsentActivity) + .assertReply(activity => { + assert(activity.type === 'invokeResponse', `incorrect activity type "${activity.type}", expected "invokeResponse"`); + assert(activity.value.status === 200, `incorrect status code "${activity.value.status}", expected "200"`); + assert(!activity.value.body, + `expected empty body for invokeResponse from fileConsent flow.\nReceived: ${JSON.stringify(activity.value.body)}`); + }); + }); + + + describe('and the return value from', () => { + class TaskHandler extends TeamsActivityHandler { + constructor() { + super(); + // TaskModuleResponses with inner types of 'continue' and 'message'. + this.fetchReturn = { task: { type: 'continue', value: { title: 'test' } } }; + this.submitReturn = { task: { type: 'message', value: 'test' } }; + } + + async handleTeamsTaskModuleFetch(context, taskModuleRequest) { + assert(context, 'context not found'); + assert(taskModuleRequest, 'taskModuleRequest not found'); + return this.fetchReturn; + } + + async handleTeamsTaskModuleSubmit(context, taskModuleRequest) { + assert(context, 'context not found'); + assert(taskModuleRequest, 'taskModuleRequest not found'); + return this.submitReturn; + } + } + + it('an overriden handleTeamsTaskModuleFetch()', done => { + const bot = new TaskHandler(); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const taskFetchActivity = createInvokeActivity('task/fetch', { data: 'fetch' }); + adapter.send(taskFetchActivity) + .assertReply(activity => { + assert.strictEqual(activity.type, 'invokeResponse'); + assert(activity.value, 'activity.value not found'); + assert.strictEqual(activity.value.status, 200); + assert.strictEqual(activity.value.body, bot.fetchReturn); + done(); + }) + .catch(err => done(err)); + }); + + it('an overriden handleTeamsTaskModuleSubmit()', done => { + const bot = new TaskHandler(); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const taskSubmitActivity = createInvokeActivity('task/submit', { data: 'submit' }); + adapter.send(taskSubmitActivity) + .assertReply(activity => { + assert.strictEqual(activity.type, 'invokeResponse'); + assert(activity.value, 'activity.value not found'); + assert.strictEqual(activity.value.status, 200); + assert.strictEqual(activity.value.body, bot.submitReturn); + done(); + }) + .catch(err => done(err)); + }); + }); + }); + + describe('should send a BadRequest status code when', () => { + it('handleTeamsFileConsent() receives an unexpected action value', () => { + const bot = new TeamsActivityHandler(); + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + const fileConsentActivity = createInvokeActivity('fileConsent/invoke', { action: 'test' }); + + adapter.send(fileConsentActivity) + .assertReply(activity => { + assert.strictEqual(activity.type, 'invokeResponse'); + assert.strictEqual(activity.value.status, 400); + assert.strictEqual(activity.value.body, undefined); + }); + }); + + it('handleTeamsMessagingExtensionSubmitActionDispatch() receives an unexpected botMessagePreviewAction value', () => { + const bot = new TeamsActivityHandler(); + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + const fileConsentActivity = createInvokeActivity('composeExtension/submitAction', { botMessagePreviewAction: 'test' }); + + adapter.send(fileConsentActivity) + .assertReply(activity => { + assert.strictEqual(activity.type, 'invokeResponse'); + assert.strictEqual(activity.value.status, 400); + assert.strictEqual(activity.value.body, undefined); + }); + }); + }); + + describe('should dispatch through a registered', () => { + let fileConsentAcceptCalled = false; + let fileConsentDeclineCalled = false; + let fileConsentCalled = false; + + class FileConsentBot extends TeamsActivityHandler { + async handleTeamsFileConsent(context, fileConsentCardResponse) { + assert(context, 'context not found'); + assert(fileConsentCardResponse, 'fileConsentCardResponse not found'); + fileConsentCalled = true; + await super.handleTeamsFileConsent(context, fileConsentCardResponse); + } + + async handleTeamsFileConsentAccept(context, fileConsentCardResponse) { + assert(context, 'context not found'); + assert(fileConsentCardResponse, 'fileConsentCardResponse not found'); + assert(fileConsentCalled, 'handleTeamsFileConsent handler was not called before handleTeamsFileConsentAccept handler'); + fileConsentAcceptCalled = true; + } + + async handleTeamsFileConsentDecline(context, fileConsentCardResponse) { + assert(context, 'context not found'); + assert(fileConsentCardResponse, 'fileConsentCardResponse not found'); + assert(fileConsentCalled, 'handleTeamsFileConsent handler was not called before handleTeamsFileConsentDecline handler'); + fileConsentDeclineCalled = true; + } + } + + beforeEach(() => { + fileConsentAcceptCalled = false; + fileConsentDeclineCalled = false; + fileConsentCalled = false; + }); + + afterEach(() => { + fileConsentAcceptCalled = false; + fileConsentDeclineCalled = false; + fileConsentCalled = false; + }); + + it('handleTeamsFileConsent handler before an handleTeamsFileConsentAccept handler', async () => { + const bot = new FileConsentBot(); + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const fileConsentActivity = createInvokeActivity('fileConsent/invoke', { action: 'accept' }); + adapter.send(fileConsentActivity) + .assertReply(activity => { + assert(activity.type === 'invokeResponse', `incorrect activity type "${activity.type}", expected "invokeResponse"`); + assert(activity.value.status === 200, `incorrect status code "${activity.value.status}", expected "200"`); + assert(!activity.value.body, + `expected empty body for invokeResponse from fileConsent flow.\nReceived: ${JSON.stringify(activity.value.body)}`); + }).then(() => { + assert(fileConsentCalled, 'handleTeamsFileConsent handler not called'); + assert(fileConsentAcceptCalled, 'handleTeamsFileConsentAccept handler not called'); + }); + }); + + it('handleTeamsFileConsent handler before an handleTeamsFileConsentDecline handler', async () => { + const bot = new FileConsentBot(); + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const fileConsentActivity = createInvokeActivity('fileConsent/invoke', { action: 'decline' }); + adapter.send(fileConsentActivity) + .assertReply(activity => { + assert(activity.type === 'invokeResponse', `incorrect activity type "${activity.type}", expected "invokeResponse"`); + assert(activity.value.status === 200, `incorrect status code "${activity.value.status}", expected "200"`); + assert(!activity.value.body, + `expected empty body for invokeResponse from fileConsent flow.\nReceived: ${JSON.stringify(activity.value.body)}`); + }).then(() => { + assert(fileConsentCalled, 'handleTeamsFileConsent handler not called'); + assert(fileConsentDeclineCalled, 'handleTeamsFileConsentDecline handler not called'); + + }); + }); + }); + + describe('should call onDialog handlers after successfully handling an', () => { + + function createConvUpdateActivity(channelData) { + const activity = { + type: ActivityTypes.ConversationUpdate, + channelData + }; + return activity; + } + + let onConversationUpdateCalled = false; + let onDialogCalled = false; + + beforeEach(() => { + onConversationUpdateCalled = false; + onDialogCalled = false; + }); + + afterEach(() => { + onConversationUpdateCalled = true; + onDialogCalled = true; + }); + + it('onTeamsMembersAdded routed activity', () => { + const bot = new TeamsActivityHandler(); + let onTeamsMemberAddedEvent = false; + + const membersAddedMock = [{ id: 'test'} , { id: 'id' }]; + const team = { id: 'team' }; + const activity = createConvUpdateActivity({ team, eventType: 'teamMemberAdded' }); + activity.membersAdded = membersAddedMock; + + bot.onConversationUpdate(async (context, next) => { + assert(context, 'context not found'); + assert(next, 'next not found'); + onConversationUpdateCalled = true; + await next(); + }); + + bot.onTeamsMembersAddedEvent(async (membersAdded, teamInfo, context, next) => { + assert(membersAdded, 'membersAdded not found'); + assert(teamInfo, 'teamsInfo not found'); + assert(context, 'context not found'); + assert(next, 'next not found'); + assert.strictEqual(teamInfo, team); + assert.strictEqual(membersAdded, membersAddedMock); + onTeamsMemberAddedEvent = true; + await next(); + }); + + bot.onDialog(async (context, next) => { + assert(context, 'context not found'); + assert(next, 'next not found'); + onDialogCalled = true; + await next(); + }); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + adapter.send(activity) + .then(() => { + assert(onTeamsMemberAddedEvent, 'onTeamsMemberAddedEvent handler not called'); + assert(onConversationUpdateCalled, 'handleTeamsFileConsentAccept handler not called'); + assert(onDialogCalled, 'onDialog handler not called'); + }); + }); + + it('onTeamsMembersRemoved routed activity', () => { + const bot = new TeamsActivityHandler(); + + let onTeamsMemberAddedEvent = false; + + const membersRemovedMock = [{ id: 'test'} , { id: 'id' }]; + const team = { id: 'team' }; + const activity = createConvUpdateActivity({ team, eventType: 'teamMemberRemoved' }); + activity.membersRemoved = membersRemovedMock; + + bot.onConversationUpdate(async (context, next) => { + assert(context, 'context not found'); + assert(next, 'next not found'); + onConversationUpdateCalled = true; + await next(); + }); + + bot.onTeamsMembersRemovedEvent(async (membersRemoved, teamInfo, context, next) => { + assert(membersRemoved, 'membersRemoved not found'); + assert(teamInfo, 'teamsInfo not found'); + assert(context, 'context not found'); + assert(next, 'next not found'); + assert.strictEqual(teamInfo, team); + assert.strictEqual(membersRemoved, membersRemovedMock); + onTeamsMemberAddedEvent = true; + await next(); + }); + + bot.onDialog(async (context, next) => { + assert(context, 'context not found'); + assert(next, 'next not found'); + onDialogCalled = true; + await next(); + }); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + adapter.send(activity) + .then(() => { + assert(onTeamsMemberAddedEvent, 'onTeamsMemberAddedEvent handler not called'); + assert(onConversationUpdateCalled, 'handleTeamsFileConsentAccept handler not called'); + assert(onDialogCalled, 'onDialog handler not called'); + }); + }); + + it('onTeamsChannelCreated routed activity', () => { + const bot = new TeamsActivityHandler(); + + let onTeamsChannelCreatedEventCalled = false; + + const team = { id: 'team' }; + const channel = { id: 'channel', name: 'channelName' }; + const activity = createConvUpdateActivity({ channel, team, eventType: 'channelCreated' }); + + bot.onConversationUpdate(async (context, next) => { + assert(context, 'context not found'); + assert(next, 'next not found'); + onConversationUpdateCalled = true; + await next(); + }); + + bot.onTeamsChannelCreatedEvent(async (channelInfo, teamInfo, context, next) => { + assert(channelInfo, 'channelInfo not found'); + assert(teamInfo, 'teamsInfo not found'); + assert(context, 'context not found'); + assert(next, 'next not found'); + assert.strictEqual(teamInfo, team); + assert.strictEqual(channelInfo, channel); + onTeamsChannelCreatedEventCalled = true; + await next(); + }); + + bot.onDialog(async (context, next) => { + assert(context, 'context not found'); + assert(next, 'next not found'); + onDialogCalled = true; + await next(); + }); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + adapter.send(activity) + .then(() => { + assert(onTeamsChannelCreatedEventCalled, 'onTeamsChannelCreated handler not called'); + assert(onConversationUpdateCalled, 'onConversationUpdate handler not called'); + assert(onDialogCalled, 'onDialog handler not called'); + }); + }); + + it('onTeamsChannelDeleted routed activity', () => { + const bot = new TeamsActivityHandler(); + + let onTeamsChannelDeletedEventCalled = false; + + const team = { id: 'team' }; + const channel = { id: 'channel', name: 'channelName' }; + const activity = createConvUpdateActivity({ channel, team, eventType: 'channelDeleted' }); + + bot.onConversationUpdate(async (context, next) => { + assert(context, 'context not found'); + assert(next, 'next not found'); + onConversationUpdateCalled = true; + await next(); + }); + + bot.onTeamsChannelDeletedEvent(async (channelInfo, teamInfo, context, next) => { + assert(channelInfo, 'channelInfo not found'); + assert(teamInfo, 'teamsInfo not found'); + assert(context, 'context not found'); + assert(next, 'next not found'); + assert.strictEqual(teamInfo, team); + assert.strictEqual(channelInfo, channel); + onTeamsChannelDeletedEventCalled = true; + await next(); + }); + + bot.onDialog(async (context, next) => { + assert(context, 'context not found'); + assert(next, 'next not found'); + onDialogCalled = true; + await next(); + }); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + adapter.send(activity) + .then(() => { + assert(onTeamsChannelDeletedEventCalled, 'onTeamsChannelDeletedEvent handler not called'); + assert(onConversationUpdateCalled, 'onConversationUpdate handler not called'); + assert(onDialogCalled, 'onDialog handler not called'); + }); + }); + + it('onTeamsChannelRenamed routed activity', () => { + const bot = new TeamsActivityHandler(); + + let onTeamsChannelRenamedEventCalled = false; + + const team = { id: 'team' }; + const channel = { id: 'channel', name: 'channelName' }; + const activity = createConvUpdateActivity({ channel, team, eventType: 'channelRenamed' }); + + bot.onConversationUpdate(async (context, next) => { + assert(context, 'context not found'); + assert(next, 'next not found'); + onConversationUpdateCalled = true; + await next(); + }); + + bot.onTeamsChannelRenamedEvent(async (channelInfo, teamInfo, context, next) => { + assert(channelInfo, 'channelInfo not found'); + assert(teamInfo, 'teamsInfo not found'); + assert(context, 'context not found'); + assert(next, 'next not found'); + assert.strictEqual(teamInfo, team); + assert.strictEqual(channelInfo, channel); + onTeamsChannelRenamedEventCalled = true; + await next(); + }); + + bot.onDialog(async (context, next) => { + assert(context, 'context not found'); + assert(next, 'next not found'); + onDialogCalled = true; + await next(); + }); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + adapter.send(activity) + .then(() => { + assert(onTeamsChannelRenamedEventCalled, 'onTeamsChannelRenamedEvent handler not called'); + assert(onConversationUpdateCalled, 'onConversationUpdate handler not called'); + assert(onDialogCalled, 'onDialog handler not called'); + }); + }); + + it('onTeamsTeamRenamed routed activity', () => { + const bot = new TeamsActivityHandler(); + + let onTeamsTeamRenamedEventCalled = false; + + const team = { id: 'team' }; + const activity = createConvUpdateActivity({ team, eventType: 'teamRenamed' }); + + bot.onConversationUpdate(async (context, next) => { + assert(context, 'context not found'); + assert(next, 'next not found'); + onConversationUpdateCalled = true; + await next(); + }); + + bot.onTeamsTeamRenamedEvent(async (teamInfo, context, next) => { + assert(teamInfo, 'teamsInfo not found'); + assert(context, 'context not found'); + assert(next, 'next not found'); + assert.strictEqual(teamInfo, team); + onTeamsTeamRenamedEventCalled = true; + await next(); + }); + + bot.onDialog(async (context, next) => { + assert(context, 'context not found'); + assert(next, 'next not found'); + onDialogCalled = true; + await next(); + }); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + adapter.send(activity) + .then(() => { + assert(onTeamsTeamRenamedEventCalled, 'onTeamsTeamRenamedEvent handler not called'); + assert(onConversationUpdateCalled, 'onConversationUpdate handler not called'); + assert(onDialogCalled, 'onDialog handler not called'); + }); + }); + }); +}); diff --git a/libraries/botbuilder/tests/teamsHelpers.test.js b/libraries/botbuilder/tests/teamsHelpers.test.js new file mode 100644 index 0000000000..2842b49091 --- /dev/null +++ b/libraries/botbuilder/tests/teamsHelpers.test.js @@ -0,0 +1,171 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +const assert = require('assert'); +const { TurnContext } = require('botbuilder-core'); +const sinon = require('sinon'); + +const { + BotFrameworkAdapter, + teamsGetChannelId, + teamsGetTeamId, + teamsNotifyUser +} = require('../'); + + +class TestContext extends TurnContext { + constructor(request) { + const adapter = new BotFrameworkAdapter(); + sinon.stub(adapter, 'createConnectorClient') + .withArgs('http://foo.com/api/messages') + .returns({ conversations: { + createConversation: (parameters) => { + assert(parameters, `createConversation() not passed parameters.`); + const response = { + id: 'MyCreationId', + serviceUrl: 'http://foo.com/api/messages', + activityId: 'MYACTIVITYID', + }; + return response; + } + }}); + super(adapter, request); + this.sent = []; + this.onSendActivities((context, activities, next) => { + this.sent = this.sent.concat(activities); + context.responded = true; + }); + } +} + +describe('TeamsActivityHelpers method', function() { + describe('teamsGetChannelId()', () => { + it('should return null if activity.channelData is falsey', () => { + const channelId = teamsGetChannelId(createActivityNoChannelData()); + assert(channelId === null); + }); + + it('should return null if activity.channelData.channel is falsey', () => { + const activity = createActivityTeamId(); + const channelId = teamsGetChannelId(activity); + assert(channelId === null); + }); + + it('should return null if activity.channelData.channel.id is falsey', () => { + const activity = createActivityTeamId(); + activity.channelData.channel = {}; + const channelId = teamsGetChannelId(activity); + assert(channelId === null); + }); + + it('should return channel id', () => { + const activity = createActivityTeamId(); + activity.channelData.channel = { id: 'channelId' }; + const channelId = teamsGetChannelId(activity); + assert.strictEqual(channelId, 'channelId'); + }); + + it('should throw an error if no activity is passed in', () => { + try { + teamsGetChannelId(undefined); + } catch (err) { + assert.strictEqual(err.message, 'Missing activity parameter'); + } + }); + }); + + describe('teamsGetTeamId()', () => { + it('should return team id', async function() { + const activity = createActivityTeamId(); + const id = teamsGetTeamId(activity); + assert(id === 'myId'); + }); + + it('should return null with no team id', async function() { + const activity = createActivityNoTeamId(); + const id = teamsGetTeamId(activity); + assert(id === null); + }); + + it('should return null with no channelData', async function() { + const activity = createActivityNoChannelData(); + const id = teamsGetTeamId(activity); + assert(id === null); + }); + + it('should throw an error if no activity is passed in', () => { + try { + teamsGetTeamId(undefined); + } catch (err) { + assert.strictEqual(err.message, 'Missing activity parameter'); + } + }); + }); + + describe('teamsNotifyUser()', () => { + it('should add notify with no notification in channelData', async function() { + const activity = createActivityTeamId(); + teamsNotifyUser(activity); + assert(activity.channelData.notification.alert === true); + }); + + it('should add notify with no channelData', async function() { + const activity = createActivityNoChannelData(); + teamsNotifyUser(activity); + assert(activity.channelData.notification.alert === true); + }); + + it('should throw an error if no activity is passed in', () => { + try { + teamsNotifyUser(undefined); + } catch (err) { + assert.strictEqual(err.message, 'Missing activity parameter'); + } + }); + }); +}); + +function createActivityNoTeamId() { + return { + type: 'message', + timestamp: Date.now, + id: 1, + text: "testMessage", + channelId: 'teams', + from: { id: `User1` }, + channelData: { team: 'myTeams'}, + conversation: { id: 'conversationId' }, + recipient: { id: 'Bot1', name: '2' }, + serviceUrl: 'http://foo.com/api/messages' + }; +} +function createActivityNoChannelData() { + return { + type: 'message', + timestamp: Date.now, + id: 1, + text: "testMessage", + channelId: 'teams', + from: { id: `User1` }, + conversation: { id: 'conversationId' }, + recipient: { id: 'Bot1', name: '2' }, + serviceUrl: 'http://foo.com/api/messages' + }; +} + +function createActivityTeamId() { + return { + type: 'message', + timestamp: Date.now, + id: 1, + text: "testMessage", + channelId: 'teams', + from: { id: `User1` }, + channelData: { team: { id: 'myId'}}, + conversation: { id: 'conversationId' }, + recipient: { id: 'Bot1', name: '2' }, + serviceUrl: 'http://foo.com/api/messages' + }; +} diff --git a/libraries/botbuilder/tests/teamsInfo.test.js b/libraries/botbuilder/tests/teamsInfo.test.js new file mode 100644 index 0000000000..af7ce17784 --- /dev/null +++ b/libraries/botbuilder/tests/teamsInfo.test.js @@ -0,0 +1,571 @@ +const assert = require('assert'); +const nock = require('nock'); +const { TurnContext } = require('botbuilder-core'); +const { BotFrameworkAdapter, TeamsInfo } = require('../'); + +beforeEach(function (done) { + nock.cleanAll(); + done(); +}); + +afterEach(function (done) { + nock.cleanAll(); + done(); +}); + +class TeamsInfoAdapter extends BotFrameworkAdapter { + constructor() { + super({ appId: 'appId', appPassword: 'appPassword' }); + } +} + +class TestContext extends TurnContext { + constructor(activity) { + super(new TeamsInfoAdapter(), activity); + } +} + +describe('TeamsInfo', () => { + describe('getTeamChannels()', () => { + it('should error in 1-on-1 chat', async () => { + const context = new TestContext(oneOnOneActivity); + try { + await TeamsInfo.getTeamChannels(context); + } catch (err) { + assert(err.message === 'This method is only valid within the scope of a MS Teams Team.', `unexpected error.message received: ${err.message}`); + } + }); + + it('should error in Group chat', async () => { + const context = new TestContext(groupChatActivity); + try { + await TeamsInfo.getTeamChannels(context); + } catch (err) { + assert(err.message === 'This method is only valid within the scope of a MS Teams Team.', `unexpected error.message received: ${err.message}`); + } + }); + + it('should work in a channel in a Team', async () => { + // This is the property on the ConversationList that contains the information about the channels from Teams. + const conversations = [ + { + "id": "19:generalChannelIdgeneralChannelId@thread.skype" + }, + { + "id": "19:somechannelId2e5ab3df9ae9b594bdb@thread.skype", + "name": "Testing1" + }, + { + "id": "19:somechannelId388ade16aa4dd375e69@thread.skype", + "name": "Testing2" + } + ]; + + const fetchChannelListExpectation = nock('https://smba.trafficmanager.net/amer') + .get('/v3/teams/19%3AgeneralChannelIdgeneralChannelId%40thread.skype/conversations') + .reply(200, { conversations }); + + const context = new TestContext(teamActivity); + const channels = await TeamsInfo.getTeamChannels(context); + assert(fetchChannelListExpectation.isDone()); + assert(Array.isArray(channels)); + assert(channels.length === 3, `unexpected number of channels detected: ${ channels.length }`); + + // There should be a channel in the conversations returned from the conversations + let generalChannelExists; + for(const channel of channels) { + if (channel.id === '19:generalChannelIdgeneralChannelId@thread.skype') { + generalChannelExists = true; + } + }; + assert(generalChannelExists, 'did not find general channel/team id in response'); + }); + + it('should work with a teamId passed in', async () => { + // This is the property on the ConversationList that contains the information about the channels from Teams. + const conversations = [ + { + "id": "19:ChannelIdgeneralChannelId@thread.skype" + }, + { + "id": "19:somechannelId2e5ab3df9ae9b594bdb@thread.skype", + "name": "Testing1" + }, + { + "id": "19:somechannelId388ade16aa4dd375e69@thread.skype", + "name": "Testing2" + } + ]; + + const fetchChannelListExpectation = nock('https://smba.trafficmanager.net/amer') + .get('/v3/teams/19%3AChannelIdgeneralChannelId%40thread.skype/conversations') + .reply(200, { conversations }); + + const context = new TestContext(teamActivity); + const channels = await TeamsInfo.getTeamChannels(context, '19:ChannelIdgeneralChannelId@thread.skype'); + assert(fetchChannelListExpectation.isDone()); + assert(Array.isArray(channels)); + assert(channels.length === 3, `unexpected number of channels detected: ${ channels.length }`); + + // There should be a channel in the conversations returned from the conversations + let generalChannelExists; + for(const channel of channels) { + if (channel.id === '19:ChannelIdgeneralChannelId@thread.skype') { + generalChannelExists = true; + } + }; + assert(generalChannelExists, 'did not find general channel/team id in response'); + }); + }); + + describe('getTeamDetails()', () => { + it('should error in 1-on-1 chat', async () => { + const context = new TestContext(oneOnOneActivity); + try { + await TeamsInfo.getTeamDetails(context); + } catch (err) { + assert(err.message === 'This method is only valid within the scope of a MS Teams Team.', `unexpected error.message received: ${err.message}`); + } + }); + + it('should error in Group chat', async () => { + const context = new TestContext(groupChatActivity); + try { + await TeamsInfo.getTeamDetails(context); + } catch (err) { + assert(err.message === 'This method is only valid within the scope of a MS Teams Team.', `unexpected error.message received: ${err.message}`); + } + }); + + it('should work in a channel in a Team', async () => { + const teamDetails = { + "id": "19:generalChannelIdgeneralChannelId@thread.skype", + "name": "TeamName", + "aadGroupId": "Team-aadGroupId" + }; + const fetchTeamDetailsExpectation = nock('https://smba.trafficmanager.net/amer') + .get('/v3/teams/19%3AgeneralChannelIdgeneralChannelId%40thread.skype') + .reply(200, teamDetails); + + const context = new TestContext(teamActivity); + const fetchedTeamDetails = await TeamsInfo.getTeamDetails(context); + assert(fetchTeamDetailsExpectation.isDone()); + assert(fetchedTeamDetails, `teamDetails should not be falsey: ${ teamDetails }`); + assert(fetchedTeamDetails.id === '19:generalChannelIdgeneralChannelId@thread.skype'); + assert(fetchedTeamDetails.name === 'TeamName'); + assert(fetchedTeamDetails.aadGroupId === 'Team-aadGroupId'); + }); + + it('should work with a teamId passed in', async () => { + const teamDetails = { + "id": "19:ChannelIdgeneralChannelId@thread.skype", + "name": "TeamName", + "aadGroupId": "Team-aadGroupId" + }; + const fetchTeamDetailsExpectation = nock('https://smba.trafficmanager.net/amer') + .get('/v3/teams/19%3AChannelIdgeneralChannelId%40thread.skype') + .reply(200, teamDetails); + + const context = new TestContext(teamActivity); + const fetchedTeamDetails = await TeamsInfo.getTeamDetails(context, '19:ChannelIdgeneralChannelId@thread.skype'); + assert(fetchTeamDetailsExpectation.isDone()); + assert(fetchedTeamDetails, `teamDetails should not be falsey: ${ teamDetails }`); + assert(fetchedTeamDetails.id === '19:ChannelIdgeneralChannelId@thread.skype'); + assert(fetchedTeamDetails.name === 'TeamName'); + assert(fetchedTeamDetails.aadGroupId === 'Team-aadGroupId'); + }); + }); + + describe('getMembers()', () => { + function assertMemberInfo(results, mockedData) { + assert(results && Array.isArray(results), `unexpected type for results arg: "${ typeof results }"`); + assert(mockedData && Array.isArray(mockedData), `unexpected type for mockedData arg: "${ typeof mockedData }"`); + assert.strictEqual(results.length, mockedData.length); + + for(let i = 0; i < results.length; i++) { + assert.strictEqual(results[i].id, mockedData[i].id); + assert.strictEqual(results[i].name, mockedData[i].name); + assert.strictEqual(results[i].aadObjectId, mockedData[i].objectId); + assert.strictEqual(results[i].givenName, mockedData[i].givenName); + assert.strictEqual(results[i].surname, mockedData[i].surname); + assert.strictEqual(results[i].email, mockedData[i].email); + assert.strictEqual(results[i].userPrincipalName, mockedData[i].userPrincipalName); + assert.strictEqual(results[i].tenantId, mockedData[i].tenantId); + }; + } + + it('should work in 1-on-1 chat', async () => { + const members = [ + { + "id": "29:User-One-Id", + "name": "User One", + "objectId": "User-One-Object-Id", + "givenName": "User", + "surname": "One", + "email": "User.One@microsoft.com", + "userPrincipalName": "user1@microsoft.com", + "tenantId": "tenantId-Guid" + } + ]; + + const fetchChannelListExpectation = nock('https://smba.trafficmanager.net/amer') + .get('/v3/conversations/a%3AoneOnOneConversationId/members') + .reply(200, members); + + const context = new TestContext(oneOnOneActivity); + const fetchedMembers = await TeamsInfo.getMembers(context); + assert(fetchChannelListExpectation.isDone()); + assertMemberInfo(fetchedMembers, members); + }); + + it('should work in Group chat', async () => { + const members = [ + { + "id": "29:User-One-Id", + "name": "User One", + "objectId": "User-One-Object-Id", + "givenName": "User", + "surname": "One", + "email": "User.One@microsoft.com", + "userPrincipalName": "user1@microsoft.com", + "tenantId": "tenantId-Guid" + }, + { + "id": "29:User-Two-Id", + "name": "User Two", + "objectId": "User-Two-Object-Id", + "givenName": "User", + "surname": "Two", + "email": "User.Two@microsoft.com", + "userPrincipalName": "user2@microsoft.com", + "tenantId": "tenantId-Guid" + }, + { + "id": "29:User-Three-Id", + "name": "User Three", + "objectId": "User-Three-Object-Id", + "givenName": "User", + "surname": "Three", + "email": "User.Three@microsoft.com", + "userPrincipalName": "user3@microsoft.com", + "tenantId": "tenantId-Guid" + } + ]; + + const fetchChannelListExpectation = nock('https://smba.trafficmanager.net/amer') + .get('/v3/conversations/19%3AgroupChatId%40thread.v2/members') + .reply(200, members); + + const context = new TestContext(groupChatActivity); + const fetchedMembers = await TeamsInfo.getMembers(context); + assert(fetchChannelListExpectation.isDone()); + assertMemberInfo(fetchedMembers, members); + }); + + it('should work in a channel in a Team', async () => { + const members = [ + { + "id": "29:User-Two-Id", + "name": "User Two", + "objectId": "User-Two-Object-Id", + "givenName": "User", + "surname": "Two", + "email": "User.Two@microsoft.com", + "userPrincipalName": "user2@microsoft.com", + "tenantId": "tenantId-Guid" + }, + { + "id": "29:User-One-Id", + "name": "User One", + "objectId": "User-One-Object-Id", + "givenName": "User", + "surname": "One", + "email": "User.One@microsoft.com", + "userPrincipalName": "user1@microsoft.com", + "tenantId": "tenantId-Guid" + } + ]; + + const fetchChannelListExpectation = nock('https://smba.trafficmanager.net/amer') + .get('/v3/conversations/19%3AgeneralChannelIdgeneralChannelId%40thread.skype/members') + .reply(200, members); + + const context = new TestContext(teamActivity); + const fetchedMembers = await TeamsInfo.getMembers(context); + assert(fetchChannelListExpectation.isDone()); + assertMemberInfo(fetchedMembers, members); + }); + + it('should not work if conversationId is falsey', async () => { + const context = new TestContext(oneOnOneActivity); + context.activity.conversation.id = undefined; + try { + await TeamsInfo.getMembers(context); + throw new Error('should have thrown an error'); + } catch (err) { + assert.strictEqual(err.message, 'The getMembers operation needs a valid conversationId.'); + oneOnOneActivity.conversation.id = 'a:oneOnOneConversationId'; + } + }); + }); + + describe('getTeamMembers()', () => { + function assertMemberInfo(results, mockedData) { + assert(results && Array.isArray(results), `unexpected type for results arg: "${ typeof results }"`); + assert(mockedData && Array.isArray(mockedData), `unexpected type for mockedData arg: "${ typeof mockedData }"`); + assert.strictEqual(results.length, mockedData.length); + + for(let i = 0; i < results.length; i++) { + assert.strictEqual(results[i].id, mockedData[i].id); + assert.strictEqual(results[i].name, mockedData[i].name); + assert.strictEqual(results[i].aadObjectId, mockedData[i].objectId); + assert.strictEqual(results[i].givenName, mockedData[i].givenName); + assert.strictEqual(results[i].surname, mockedData[i].surname); + assert.strictEqual(results[i].email, mockedData[i].email); + assert.strictEqual(results[i].userPrincipalName, mockedData[i].userPrincipalName); + assert.strictEqual(results[i].tenantId, mockedData[i].tenantId); + }; + } + + it('should error in 1-on-1 chat', async () => { + const context = new TestContext(oneOnOneActivity); + try { + await TeamsInfo.getTeamMembers(context); + } catch (err) { + assert(err.message === 'This method is only valid within the scope of a MS Teams Team.', `unexpected error.message received: ${err.message}`); + } + }); + + it('should error in Group chat', async () => { + const context = new TestContext(groupChatActivity); + try { + await TeamsInfo.getTeamMembers(context); + } catch (err) { + assert(err.message === 'This method is only valid within the scope of a MS Teams Team.', `unexpected error.message received: ${err.message}`); + } + }); + + it('should work in a channel in a Team', async () => { + const members = [ + { + "id": "29:User-Two-Id", + "name": "User Two", + "objectId": "User-Two-Object-Id", + "givenName": "User", + "surname": "Two", + "email": "User.Two@microsoft.com", + "userPrincipalName": "user2@microsoft.com", + "tenantId": "tenantId-Guid" + }, + { + "id": "29:User-One-Id", + "name": "User One", + "objectId": "User-One-Object-Id", + "givenName": "User", + "surname": "One", + "email": "User.One@microsoft.com", + "userPrincipalName": "user1@microsoft.com", + "tenantId": "tenantId-Guid" + } + ]; + + const fetchChannelListExpectation = nock('https://smba.trafficmanager.net/amer') + .get('/v3/conversations/19%3AgeneralChannelIdgeneralChannelId%40thread.skype/members') + .reply(200, members); + + const context = new TestContext(teamActivity); + const fetchedMembers = await TeamsInfo.getTeamMembers(context); + assert(fetchChannelListExpectation.isDone()); + assertMemberInfo(fetchedMembers, members); + }); + + it('should work with a teamId passed in', async () => { + const members = [ + { + "id": "29:User-Two-Id", + "name": "User Two", + "objectId": "User-Two-Object-Id", + "givenName": "User", + "surname": "Two", + "email": "User.Two@microsoft.com", + "userPrincipalName": "user2@microsoft.com", + "tenantId": "tenantId-Guid" + }, + { + "id": "29:User-One-Id", + "name": "User One", + "objectId": "User-One-Object-Id", + "givenName": "User", + "surname": "One", + "email": "User.One@microsoft.com", + "userPrincipalName": "user1@microsoft.com", + "tenantId": "tenantId-Guid" + } + ]; + + const fetchChannelListExpectation = nock('https://smba.trafficmanager.net/amer') + .get('/v3/conversations/19%3AChannelIdgeneralChannelId%40thread.skype/members') + .reply(200, members); + + const context = new TestContext(teamActivity); + const fetchedMembers = await TeamsInfo.getTeamMembers(context, '19:ChannelIdgeneralChannelId@thread.skype'); + assert(fetchChannelListExpectation.isDone()); + assertMemberInfo(fetchedMembers, members); + }); + }); + + describe('private methods', () => { + it(`getConnectorClient() should error if the context doesn't have an adapter`, done => { + try { + TeamsInfo.getConnectorClient({}); + done(new Error('should have thrown an error')); + } catch (err) { + assert.strictEqual(err.message, 'This method requires a connector client.'); + done(); + } + }); + + it(`getConnectorClient() should error if the adapter doesn't have a createConnectorClient method`, done => { + try { + TeamsInfo.getConnectorClient({ adapter: {} }); + done(new Error('should have thrown an error')); + } catch (err) { + assert.strictEqual(err.message, 'This method requires a connector client.'); + done(); + } + }); + + it(`getMembersInternal() should error if an invalid conversationId is passed in.`, async () => { + try { + const results = await TeamsInfo.getMembersInternal({}, undefined); + console.error(results) + throw new Error('should have thrown an error'); + } catch (err) { + assert.strictEqual(err.message, 'The getMembers operation needs a valid conversationId.'); + } + }); + }); +}); + +const oneOnOneActivity = { + 'text': 'Hello World!', + 'type': 'message', + 'id': 'oneOnOneActivityId', + 'channelId': 'msteams', + 'serviceUrl': 'https://smba.trafficmanager.net/amer/', + 'from': { + 'id': '29:User-One-Id', + 'name': 'User One', + 'aadObjectId': 'User-aadObjectId' + }, + 'conversation': { + 'conversationType': 'personal', + 'tenantId': 'tenantId-Guid', + 'id': 'a:oneOnOneConversationId' + }, + 'recipient': { + 'id': '28:teamsbot-Guid', + 'name': 'Teams Bot' + }, + 'channelData': { + 'tenant': { + 'id': 'tenantId-Guid' + } + } +}; + +const groupChatActivity = { + "text": "Teams Bot test\n", + "attachments": [ + { + "contentType": "text/html", + "content": "
Teams Bot test
\n
" + } + ], + "type": "message", + "id": "groupChatActivityId", + "channelId": "msteams", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "from": { + "id": "29:User-One-Id", + "name": "User One", + "aadObjectId": "User-aadObjectId" + }, + "conversation": { + "isGroup": true, + "conversationType": "groupChat", + "tenantId": "tenantId-Guid", + "id": "19:groupChatId@thread.v2" + }, + "recipient": { + "id": "28:teamsbot-Guid", + "name": "Teams Bot" + }, + "entities": [ + { + "mentioned": { + "id": "28:teamsbot-Guid", + "name": "Teams Bot" + }, + "text": "Teams Bot", + "type": "mention" + } + ], + "channelData": { + "tenant": { + "id": "tenantId-Guid" + } + } +} + +const teamActivity = { + "text": "Teams Bot hi\n", + "attachments": [ + { + "contentType": "text/html", + "content": "
Teams Bot hi
\n
" + } + ], + "type": "message", + "id": "teamActivityId", + "channelId": "msteams", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "from": { + "id": "29:User-One-Id", + "name": "User One", + "aadObjectId": "User-aadObjectId" + }, + "conversation": { + "isGroup": true, + "conversationType": "channel", + "tenantId": "tenantId-Guid", + "id": "19:generalChannelIdgeneralChannelId@thread.skype;messageid=teamActivityId" + }, + "recipient": { + "id": "28:teamsbot-Guid", + "name": "Teams Bot" + }, + "entities": [ + { + "mentioned": { + "id": "28:teamsbot-Guid", + "name": "Teams Bot" + }, + "text": "Teams Bot", + "type": "mention" + } + ], + "channelData": { + "teamsChannelId": "19:generalChannelIdgeneralChannelId@thread.skype", + "teamsTeamId": "19:generalChannelIdgeneralChannelId@thread.skype", + "channel": { + "id": "19:generalChannelIdgeneralChannelId@thread.skype" + }, + "team": { + "id": "19:generalChannelIdgeneralChannelId@thread.skype" + }, + "tenant": { + "id": "tenantId-Guid" + } + } +} diff --git a/libraries/botbuilder/tsconfig.json b/libraries/botbuilder/tsconfig.json index 3788218c0b..9347fa6605 100644 --- a/libraries/botbuilder/tsconfig.json +++ b/libraries/botbuilder/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "ESNext", + "target": "es6", + "lib": ["es2015", "dom"], "module": "commonjs", "declaration": true, "sourceMap": true, diff --git a/libraries/botframework-config/package.json b/libraries/botframework-config/package.json index d5389d9492..3e3ec65b67 100644 --- a/libraries/botframework-config/package.json +++ b/libraries/botframework-config/package.json @@ -30,9 +30,7 @@ }, "dependencies": { "fs-extra": "^7.0.0", - "process": "^0.11.10", "read-text-file": "^1.1.0", - "url": "^0.11.0", "uuid": "^3.3.2" }, "scripts": { diff --git a/libraries/botframework-config/tsconfig.json b/libraries/botframework-config/tsconfig.json index 3788218c0b..f7ed49127c 100644 --- a/libraries/botframework-config/tsconfig.json +++ b/libraries/botframework-config/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "ESNext", + "target": "es6", + "lib": ["es2015"], "module": "commonjs", "declaration": true, "sourceMap": true, diff --git a/libraries/botframework-connector/package-lock.json b/libraries/botframework-connector/package-lock.json index 80013be88b..a5c715cdc7 100644 --- a/libraries/botframework-connector/package-lock.json +++ b/libraries/botframework-connector/package-lock.json @@ -348,126 +348,6 @@ } } }, - "nock": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz", - "integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==", - "requires": { - "chai": "^4.1.2", - "debug": "^4.1.0", - "deep-equal": "^1.0.0", - "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.5", - "mkdirp": "^0.5.0", - "propagate": "^1.0.0", - "qs": "^6.5.1", - "semver": "^5.5.0" - }, - "dependencies": { - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" - }, - "chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", - "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.0", - "type-detect": "^4.0.5" - } - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "requires": { - "type-detect": "^4.0.0" - } - }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" - }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "lodash": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", - "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==" - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=" - }, - "propagate": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", - "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=" - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" - }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" - } - } - }, "node-fetch": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz", diff --git a/libraries/botframework-connector/package.json b/libraries/botframework-connector/package.json index 3536613a4f..a2b4cc1aab 100644 --- a/libraries/botframework-connector/package.json +++ b/libraries/botframework-connector/package.json @@ -26,7 +26,6 @@ "botframework-schema": "4.1.6", "form-data": "^2.3.3", "jsonwebtoken": "8.0.1", - "nock": "^10.0.1", "node-fetch": "^2.2.1", "rsa-pem-from-mod-exp": "^0.8.4" }, @@ -34,6 +33,7 @@ "@types/mocha": "^2.2.47", "codelyzer": "^4.1.0", "mocha": "^5.2.0", + "nock": "^10.0.3", "nyc": "^11.4.1", "should": "^13.2.3", "source-map-support": "^0.5.3", diff --git a/libraries/botframework-connector/src/index.ts b/libraries/botframework-connector/src/index.ts index 0e1dd1eb24..f073174838 100644 --- a/libraries/botframework-connector/src/index.ts +++ b/libraries/botframework-connector/src/index.ts @@ -10,3 +10,4 @@ export { ConnectorClient } from './connectorApi/connectorClient'; export { TokenApiClient, TokenApiModels } from './tokenApi/tokenApiClient'; export { EmulatorApiClient } from './emulatorApiClient'; export * from './tokenApi/models' +export * from './teams'; diff --git a/libraries/botframework-connector/src/teams/index.ts b/libraries/botframework-connector/src/teams/index.ts new file mode 100644 index 0000000000..8a48cbb310 --- /dev/null +++ b/libraries/botframework-connector/src/teams/index.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +export * from './teamsConnectorClient'; +export * from './teamsConnectorClientContext'; +export * from './models'; \ No newline at end of file diff --git a/libraries/botframework-connector/src/teams/models/index.ts b/libraries/botframework-connector/src/teams/models/index.ts new file mode 100644 index 0000000000..05d356ee25 --- /dev/null +++ b/libraries/botframework-connector/src/teams/models/index.ts @@ -0,0 +1,60 @@ +/* + * Code generated by Microsoft (R) AutoRest Code Generator. + * Changes may cause incorrect behavior and will be lost if the code is + * regenerated. + */ +// This code has been manually edited to reflect the integration of the Teams schemas into botframework-schema +// and the botframework-connector libraries. + +import { HttpResponse, ServiceClientOptions } from '@azure/ms-rest-js'; +import { ConversationList, TeamDetails } from 'botframework-schema'; + +/** + * @interface + * An interface representing TeamsConnectorClientOptions. + * @extends ServiceClientOptions + */ +export interface TeamsConnectorClientOptions extends ServiceClientOptions { + /** + * @member {string} [baseUri] + */ + baseUri?: string; +} + +/** + * Contains response data for the fetchChannelList operation. + */ +export type TeamsFetchChannelListResponse = ConversationList & { + /** + * The underlying HTTP response. + */ + _response: HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + /** + * The response body as parsed JSON or XML + */ + parsedBody: ConversationList; + }; +}; + +/** + * Contains response data for the fetchTeamDetails operation. + */ +export type TeamsFetchTeamDetailsResponse = TeamDetails & { + /** + * The underlying HTTP response. + */ + _response: HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + /** + * The response body as parsed JSON or XML + */ + parsedBody: TeamDetails; + }; +}; \ No newline at end of file diff --git a/libraries/botframework-connector/src/teams/models/mappers.ts b/libraries/botframework-connector/src/teams/models/mappers.ts new file mode 100644 index 0000000000..00077650d7 --- /dev/null +++ b/libraries/botframework-connector/src/teams/models/mappers.ts @@ -0,0 +1,1854 @@ +/* + * Code generated by Microsoft (R) AutoRest Code Generator. + * Changes may cause incorrect behavior and will be lost if the code is + * regenerated. + */ + +import * as msRest from '@azure/ms-rest-js'; + + +export const ChannelInfo: msRest.CompositeMapper = { + serializedName: 'ChannelInfo', + type: { + name: 'Composite', + className: 'ChannelInfo', + modelProperties: { + id: { + serializedName: 'id', + type: { + name: 'String' + } + }, + name: { + serializedName: 'name', + type: { + name: 'String' + } + } + } + } +}; + +export const ConversationList: msRest.CompositeMapper = { + serializedName: 'ConversationList', + type: { + name: 'Composite', + className: 'ConversationList', + modelProperties: { + conversations: { + serializedName: 'conversations', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'ChannelInfo' + } + } + } + } + } + } +}; + +export const TeamDetails: msRest.CompositeMapper = { + serializedName: 'TeamDetails', + type: { + name: 'Composite', + className: 'TeamDetails', + modelProperties: { + id: { + serializedName: 'id', + type: { + name: 'String' + } + }, + name: { + serializedName: 'name', + type: { + name: 'String' + } + }, + aadGroupId: { + serializedName: 'aadGroupId', + type: { + name: 'String' + } + } + } + } +}; + +export const TeamInfo: msRest.CompositeMapper = { + serializedName: 'TeamInfo', + type: { + name: 'Composite', + className: 'TeamInfo', + modelProperties: { + id: { + serializedName: 'id', + type: { + name: 'String' + } + }, + name: { + serializedName: 'name', + type: { + name: 'String' + } + } + } + } +}; + +export const NotificationInfo: msRest.CompositeMapper = { + serializedName: 'NotificationInfo', + type: { + name: 'Composite', + className: 'NotificationInfo', + modelProperties: { + alert: { + serializedName: 'alert', + type: { + name: 'Boolean' + } + } + } + } +}; + +export const TenantInfo: msRest.CompositeMapper = { + serializedName: 'TenantInfo', + type: { + name: 'Composite', + className: 'TenantInfo', + modelProperties: { + id: { + serializedName: 'id', + type: { + name: 'String' + } + } + } + } +}; + +export const TeamsChannelData: msRest.CompositeMapper = { + serializedName: 'TeamsChannelData', + type: { + name: 'Composite', + className: 'TeamsChannelData', + modelProperties: { + channel: { + serializedName: 'channel', + type: { + name: 'Composite', + className: 'ChannelInfo' + } + }, + eventType: { + serializedName: 'eventType', + type: { + name: 'String' + } + }, + team: { + serializedName: 'team', + type: { + name: 'Composite', + className: 'TeamInfo' + } + }, + notification: { + serializedName: 'notification', + type: { + name: 'Composite', + className: 'NotificationInfo' + } + }, + tenant: { + serializedName: 'tenant', + type: { + name: 'Composite', + className: 'TenantInfo' + } + } + } + } +}; + +export const ChannelAccount: msRest.CompositeMapper = { + serializedName: 'ChannelAccount', + type: { + name: 'Composite', + className: 'ChannelAccount', + modelProperties: { + id: { + serializedName: 'id', + type: { + name: 'String' + } + }, + name: { + serializedName: 'name', + type: { + name: 'String' + } + } + } + } +}; + +export const TeamsChannelAccount: msRest.CompositeMapper = { + serializedName: 'TeamsChannelAccount', + type: { + name: 'Composite', + className: 'TeamsChannelAccount', + modelProperties: { + ...ChannelAccount.type.modelProperties, + givenName: { + serializedName: 'givenName', + type: { + name: 'String' + } + }, + surname: { + serializedName: 'surname', + type: { + name: 'String' + } + }, + email: { + serializedName: 'email', + type: { + name: 'String' + } + }, + userPrincipalName: { + serializedName: 'userPrincipalName', + type: { + name: 'String' + } + } + } + } +}; + +export const CardAction: msRest.CompositeMapper = { + serializedName: 'CardAction', + type: { + name: 'Composite', + className: 'CardAction', + modelProperties: { + type: { + serializedName: 'type', + type: { + name: 'String' + } + }, + title: { + serializedName: 'title', + type: { + name: 'String' + } + }, + image: { + serializedName: 'image', + type: { + name: 'String' + } + }, + value: { + serializedName: 'value', + type: { + name: 'Object' + } + } + } + } +}; + +export const CardImage: msRest.CompositeMapper = { + serializedName: 'CardImage', + type: { + name: 'Composite', + className: 'CardImage', + modelProperties: { + url: { + serializedName: 'url', + type: { + name: 'String' + } + }, + alt: { + serializedName: 'alt', + type: { + name: 'String' + } + }, + tap: { + serializedName: 'tap', + type: { + name: 'Composite', + className: 'CardAction' + } + } + } + } +}; + +export const O365ConnectorCardFact: msRest.CompositeMapper = { + serializedName: 'O365ConnectorCardFact', + type: { + name: 'Composite', + className: 'O365ConnectorCardFact', + modelProperties: { + name: { + serializedName: 'name', + type: { + name: 'String' + } + }, + value: { + serializedName: 'value', + type: { + name: 'String' + } + } + } + } +}; + +export const O365ConnectorCardImage: msRest.CompositeMapper = { + serializedName: 'O365ConnectorCardImage', + type: { + name: 'Composite', + className: 'O365ConnectorCardImage', + modelProperties: { + image: { + serializedName: 'image', + type: { + name: 'String' + } + }, + title: { + serializedName: 'title', + type: { + name: 'String' + } + } + } + } +}; + +export const O365ConnectorCardActionBase: msRest.CompositeMapper = { + serializedName: 'O365ConnectorCardActionBase', + type: { + name: 'Composite', + className: 'O365ConnectorCardActionBase', + modelProperties: { + type: { + serializedName: '@type', + type: { + name: 'String' + } + }, + name: { + serializedName: 'name', + type: { + name: 'String' + } + }, + id: { + serializedName: '@id', + type: { + name: 'String' + } + } + } + } +}; + +export const O365ConnectorCardSection: msRest.CompositeMapper = { + serializedName: 'O365ConnectorCardSection', + type: { + name: 'Composite', + className: 'O365ConnectorCardSection', + modelProperties: { + title: { + serializedName: 'title', + type: { + name: 'String' + } + }, + text: { + serializedName: 'text', + type: { + name: 'String' + } + }, + activityTitle: { + serializedName: 'activityTitle', + type: { + name: 'String' + } + }, + activitySubtitle: { + serializedName: 'activitySubtitle', + type: { + name: 'String' + } + }, + activityText: { + serializedName: 'activityText', + type: { + name: 'String' + } + }, + activityImage: { + serializedName: 'activityImage', + type: { + name: 'String' + } + }, + activityImageType: { + serializedName: 'activityImageType', + type: { + name: 'String' + } + }, + markdown: { + serializedName: 'markdown', + type: { + name: 'Boolean' + } + }, + facts: { + serializedName: 'facts', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'O365ConnectorCardFact' + } + } + } + }, + images: { + serializedName: 'images', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'O365ConnectorCardImage' + } + } + } + }, + potentialAction: { + serializedName: 'potentialAction', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'O365ConnectorCardActionBase' + } + } + } + } + } + } +}; + +export const O365ConnectorCard: msRest.CompositeMapper = { + serializedName: 'O365ConnectorCard', + type: { + name: 'Composite', + className: 'O365ConnectorCard', + modelProperties: { + title: { + serializedName: 'title', + type: { + name: 'String' + } + }, + text: { + serializedName: 'text', + type: { + name: 'String' + } + }, + summary: { + serializedName: 'summary', + type: { + name: 'String' + } + }, + themeColor: { + serializedName: 'themeColor', + type: { + name: 'String' + } + }, + sections: { + serializedName: 'sections', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'O365ConnectorCardSection' + } + } + } + }, + potentialAction: { + serializedName: 'potentialAction', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'O365ConnectorCardActionBase' + } + } + } + } + } + } +}; + +export const O365ConnectorCardViewAction: msRest.CompositeMapper = { + serializedName: 'O365ConnectorCardViewAction', + type: { + name: 'Composite', + className: 'O365ConnectorCardViewAction', + modelProperties: { + ...O365ConnectorCardActionBase.type.modelProperties, + target: { + serializedName: 'target', + type: { + name: 'Sequence', + element: { + type: { + name: 'String' + } + } + } + } + } + } +}; + +export const O365ConnectorCardOpenUriTarget: msRest.CompositeMapper = { + serializedName: 'O365ConnectorCardOpenUriTarget', + type: { + name: 'Composite', + className: 'O365ConnectorCardOpenUriTarget', + modelProperties: { + os: { + serializedName: 'os', + type: { + name: 'String' + } + }, + uri: { + serializedName: 'uri', + type: { + name: 'String' + } + } + } + } +}; + +export const O365ConnectorCardOpenUri: msRest.CompositeMapper = { + serializedName: 'O365ConnectorCardOpenUri', + type: { + name: 'Composite', + className: 'O365ConnectorCardOpenUri', + modelProperties: { + ...O365ConnectorCardActionBase.type.modelProperties, + targets: { + serializedName: 'targets', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'O365ConnectorCardOpenUriTarget' + } + } + } + } + } + } +}; + +export const O365ConnectorCardHttpPOST: msRest.CompositeMapper = { + serializedName: 'O365ConnectorCardHttpPOST', + type: { + name: 'Composite', + className: 'O365ConnectorCardHttpPOST', + modelProperties: { + ...O365ConnectorCardActionBase.type.modelProperties, + body: { + serializedName: 'body', + type: { + name: 'String' + } + } + } + } +}; + +export const O365ConnectorCardInputBase: msRest.CompositeMapper = { + serializedName: 'O365ConnectorCardInputBase', + type: { + name: 'Composite', + className: 'O365ConnectorCardInputBase', + modelProperties: { + type: { + serializedName: '@type', + type: { + name: 'String' + } + }, + id: { + serializedName: 'id', + type: { + name: 'String' + } + }, + isRequired: { + serializedName: 'isRequired', + type: { + name: 'Boolean' + } + }, + title: { + serializedName: 'title', + type: { + name: 'String' + } + }, + value: { + serializedName: 'value', + type: { + name: 'String' + } + } + } + } +}; + +export const O365ConnectorCardActionCard: msRest.CompositeMapper = { + serializedName: 'O365ConnectorCardActionCard', + type: { + name: 'Composite', + className: 'O365ConnectorCardActionCard', + modelProperties: { + ...O365ConnectorCardActionBase.type.modelProperties, + inputs: { + serializedName: 'inputs', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'O365ConnectorCardInputBase' + } + } + } + }, + actions: { + serializedName: 'actions', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'O365ConnectorCardActionBase' + } + } + } + } + } + } +}; + +export const O365ConnectorCardTextInput: msRest.CompositeMapper = { + serializedName: 'O365ConnectorCardTextInput', + type: { + name: 'Composite', + className: 'O365ConnectorCardTextInput', + modelProperties: { + ...O365ConnectorCardInputBase.type.modelProperties, + isMultiline: { + serializedName: 'isMultiline', + type: { + name: 'Boolean' + } + }, + maxLength: { + serializedName: 'maxLength', + type: { + name: 'Number' + } + } + } + } +}; + +export const O365ConnectorCardDateInput: msRest.CompositeMapper = { + serializedName: 'O365ConnectorCardDateInput', + type: { + name: 'Composite', + className: 'O365ConnectorCardDateInput', + modelProperties: { + ...O365ConnectorCardInputBase.type.modelProperties, + includeTime: { + serializedName: 'includeTime', + type: { + name: 'Boolean' + } + } + } + } +}; + +export const O365ConnectorCardMultichoiceInputChoice: msRest.CompositeMapper = { + serializedName: 'O365ConnectorCardMultichoiceInputChoice', + type: { + name: 'Composite', + className: 'O365ConnectorCardMultichoiceInputChoice', + modelProperties: { + display: { + serializedName: 'display', + type: { + name: 'String' + } + }, + value: { + serializedName: 'value', + type: { + name: 'String' + } + } + } + } +}; + +export const O365ConnectorCardMultichoiceInput: msRest.CompositeMapper = { + serializedName: 'O365ConnectorCardMultichoiceInput', + type: { + name: 'Composite', + className: 'O365ConnectorCardMultichoiceInput', + modelProperties: { + ...O365ConnectorCardInputBase.type.modelProperties, + choices: { + serializedName: 'choices', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'O365ConnectorCardMultichoiceInputChoice' + } + } + } + }, + style: { + serializedName: 'style', + type: { + name: 'String' + } + }, + isMultiSelect: { + serializedName: 'isMultiSelect', + type: { + name: 'Boolean' + } + } + } + } +}; + +export const O365ConnectorCardActionQuery: msRest.CompositeMapper = { + serializedName: 'O365ConnectorCardActionQuery', + type: { + name: 'Composite', + className: 'O365ConnectorCardActionQuery', + modelProperties: { + body: { + serializedName: 'body', + type: { + name: 'String' + } + }, + actionId: { + serializedName: 'actionId', + type: { + name: 'String' + } + } + } + } +}; + +export const SigninStateVerificationQuery: msRest.CompositeMapper = { + serializedName: 'SigninStateVerificationQuery', + type: { + name: 'Composite', + className: 'SigninStateVerificationQuery', + modelProperties: { + state: { + serializedName: 'state', + type: { + name: 'String' + } + } + } + } +}; + +export const MessagingExtensionQueryOptions: msRest.CompositeMapper = { + serializedName: 'MessagingExtensionQueryOptions', + type: { + name: 'Composite', + className: 'MessagingExtensionQueryOptions', + modelProperties: { + skip: { + serializedName: 'skip', + type: { + name: 'Number' + } + }, + count: { + serializedName: 'count', + type: { + name: 'Number' + } + } + } + } +}; + +export const MessagingExtensionParameter: msRest.CompositeMapper = { + serializedName: 'MessagingExtensionParameter', + type: { + name: 'Composite', + className: 'MessagingExtensionParameter', + modelProperties: { + name: { + serializedName: 'name', + type: { + name: 'String' + } + }, + value: { + serializedName: 'value', + type: { + name: 'Object' + } + } + } + } +}; + +export const MessagingExtensionQuery: msRest.CompositeMapper = { + serializedName: 'MessagingExtensionQuery', + type: { + name: 'Composite', + className: 'MessagingExtensionQuery', + modelProperties: { + commandId: { + serializedName: 'commandId', + type: { + name: 'String' + } + }, + parameters: { + serializedName: 'parameters', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'MessagingExtensionParameter' + } + } + } + }, + queryOptions: { + serializedName: 'queryOptions', + type: { + name: 'Composite', + className: 'MessagingExtensionQueryOptions' + } + }, + state: { + serializedName: 'state', + type: { + name: 'String' + } + } + } + } +}; + +export const Activity: msRest.CompositeMapper = { + serializedName: 'Activity', + type: { + name: 'Composite', + className: 'Activity', + modelProperties: { + dummyProperty: { + serializedName: 'dummyProperty', + type: { + name: 'String' + } + } + } + } +}; + +export const MessageActionsPayloadUser: msRest.CompositeMapper = { + serializedName: 'MessageActionsPayloadUser', + type: { + name: 'Composite', + className: 'MessageActionsPayloadUser', + modelProperties: { + userIdentityType: { + serializedName: 'userIdentityType', + type: { + name: 'String' + } + }, + id: { + serializedName: 'id', + type: { + name: 'String' + } + }, + displayName: { + serializedName: 'displayName', + type: { + name: 'String' + } + } + } + } +}; + +export const MessageActionsPayloadApp: msRest.CompositeMapper = { + serializedName: 'MessageActionsPayloadApp', + type: { + name: 'Composite', + className: 'MessageActionsPayloadApp', + modelProperties: { + applicationIdentityType: { + serializedName: 'applicationIdentityType', + type: { + name: 'String' + } + }, + id: { + serializedName: 'id', + type: { + name: 'String' + } + }, + displayName: { + serializedName: 'displayName', + type: { + name: 'String' + } + } + } + } +}; + +export const MessageActionsPayloadConversation: msRest.CompositeMapper = { + serializedName: 'MessageActionsPayloadConversation', + type: { + name: 'Composite', + className: 'MessageActionsPayloadConversation', + modelProperties: { + conversationIdentityType: { + serializedName: 'conversationIdentityType', + type: { + name: 'String' + } + }, + id: { + serializedName: 'id', + type: { + name: 'String' + } + }, + displayName: { + serializedName: 'displayName', + type: { + name: 'String' + } + } + } + } +}; + +export const MessageActionsPayloadFrom: msRest.CompositeMapper = { + serializedName: 'MessageActionsPayloadFrom', + type: { + name: 'Composite', + className: 'MessageActionsPayloadFrom', + modelProperties: { + user: { + serializedName: 'user', + type: { + name: 'Composite', + className: 'MessageActionsPayloadUser' + } + }, + application: { + serializedName: 'application', + type: { + name: 'Composite', + className: 'MessageActionsPayloadApp' + } + }, + conversation: { + serializedName: 'conversation', + type: { + name: 'Composite', + className: 'MessageActionsPayloadConversation' + } + } + } + } +}; + +export const MessageActionsPayloadBody: msRest.CompositeMapper = { + serializedName: 'MessageActionsPayload_body', + type: { + name: 'Composite', + className: 'MessageActionsPayloadBody', + modelProperties: { + contentType: { + serializedName: 'contentType', + type: { + name: 'String' + } + }, + content: { + serializedName: 'content', + type: { + name: 'String' + } + }, + textContent: { + serializedName: 'textContent', + type: { + name: 'String' + } + } + } + } +}; + +export const MessageActionsPayloadAttachment: msRest.CompositeMapper = { + serializedName: 'MessageActionsPayloadAttachment', + type: { + name: 'Composite', + className: 'MessageActionsPayloadAttachment', + modelProperties: { + id: { + serializedName: 'id', + type: { + name: 'String' + } + }, + contentType: { + serializedName: 'contentType', + type: { + name: 'String' + } + }, + contentUrl: { + serializedName: 'contentUrl', + type: { + name: 'String' + } + }, + content: { + serializedName: 'content', + type: { + name: 'Object' + } + }, + name: { + serializedName: 'name', + type: { + name: 'String' + } + }, + thumbnailUrl: { + serializedName: 'thumbnailUrl', + type: { + name: 'String' + } + } + } + } +}; + +export const MessageActionsPayloadMention: msRest.CompositeMapper = { + serializedName: 'MessageActionsPayloadMention', + type: { + name: 'Composite', + className: 'MessageActionsPayloadMention', + modelProperties: { + id: { + serializedName: 'id', + type: { + name: 'Number' + } + }, + mentionText: { + serializedName: 'mentionText', + type: { + name: 'String' + } + }, + mentioned: { + serializedName: 'mentioned', + type: { + name: 'Composite', + className: 'MessageActionsPayloadFrom' + } + } + } + } +}; + +export const MessageActionsPayloadReaction: msRest.CompositeMapper = { + serializedName: 'MessageActionsPayloadReaction', + type: { + name: 'Composite', + className: 'MessageActionsPayloadReaction', + modelProperties: { + reactionType: { + serializedName: 'reactionType', + type: { + name: 'String' + } + }, + createdDateTime: { + serializedName: 'createdDateTime', + type: { + name: 'String' + } + }, + user: { + serializedName: 'user', + type: { + name: 'Composite', + className: 'MessageActionsPayloadFrom' + } + } + } + } +}; + +export const MessageActionsPayload: msRest.CompositeMapper = { + serializedName: 'MessageActionsPayload', + type: { + name: 'Composite', + className: 'MessageActionsPayload', + modelProperties: { + id: { + serializedName: 'id', + type: { + name: 'String' + } + }, + replyToId: { + serializedName: 'replyToId', + type: { + name: 'String' + } + }, + messageType: { + serializedName: 'messageType', + type: { + name: 'String' + } + }, + createdDateTime: { + serializedName: 'createdDateTime', + type: { + name: 'String' + } + }, + lastModifiedDateTime: { + serializedName: 'lastModifiedDateTime', + type: { + name: 'String' + } + }, + deleted: { + serializedName: 'deleted', + type: { + name: 'Boolean' + } + }, + subject: { + serializedName: 'subject', + type: { + name: 'String' + } + }, + summary: { + serializedName: 'summary', + type: { + name: 'String' + } + }, + importance: { + serializedName: 'importance', + type: { + name: 'String' + } + }, + locale: { + serializedName: 'locale', + type: { + name: 'String' + } + }, + from: { + serializedName: 'from', + type: { + name: 'Composite', + className: 'MessageActionsPayloadFrom' + } + }, + body: { + serializedName: 'body', + type: { + name: 'Composite', + className: 'MessageActionsPayloadBody' + } + }, + attachmentLayout: { + serializedName: 'attachmentLayout', + type: { + name: 'String' + } + }, + attachments: { + serializedName: 'attachments', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'MessageActionsPayloadAttachment' + } + } + } + }, + mentions: { + serializedName: 'mentions', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'MessageActionsPayloadMention' + } + } + } + }, + reactions: { + serializedName: 'reactions', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'MessageActionsPayloadReaction' + } + } + } + } + } + } +}; + +export const TaskModuleRequest: msRest.CompositeMapper = { + serializedName: 'TaskModuleRequest', + type: { + name: 'Composite', + className: 'TaskModuleRequest', + modelProperties: { + data: { + serializedName: 'data', + type: { + name: 'Object' + } + }, + context: { + serializedName: 'context', + type: { + name: 'Composite', + className: 'TaskModuleRequestContext' + } + } + } + } +}; + +export const MessagingExtensionAction: msRest.CompositeMapper = { + serializedName: 'MessagingExtensionAction', + type: { + name: 'Composite', + className: 'MessagingExtensionAction', + modelProperties: { + ...TaskModuleRequest.type.modelProperties, + commandId: { + serializedName: 'commandId', + type: { + name: 'String' + } + }, + commandContext: { + serializedName: 'commandContext', + type: { + name: 'String' + } + }, + botMessagePreviewAction: { + serializedName: 'botMessagePreviewAction', + type: { + name: 'String' + } + }, + botActivityPreview: { + serializedName: 'botActivityPreview', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'Activity' + } + } + } + }, + messagePayload: { + serializedName: 'messagePayload', + type: { + name: 'Composite', + className: 'MessageActionsPayload' + } + } + } + } +}; + +export const TaskModuleResponseBase: msRest.CompositeMapper = { + serializedName: 'TaskModuleResponseBase', + type: { + name: 'Composite', + className: 'TaskModuleResponseBase', + modelProperties: { + type: { + serializedName: 'type', + type: { + name: 'String' + } + } + } + } +}; + +export const Attachment: msRest.CompositeMapper = { + serializedName: 'Attachment', + type: { + name: 'Composite', + className: 'Attachment', + modelProperties: { + contentType: { + serializedName: 'contentType', + type: { + name: 'String' + } + }, + contentUrl: { + serializedName: 'contentUrl', + type: { + name: 'String' + } + }, + content: { + serializedName: 'content', + type: { + name: 'Object' + } + }, + name: { + serializedName: 'name', + type: { + name: 'String' + } + }, + thumbnailUrl: { + serializedName: 'thumbnailUrl', + type: { + name: 'String' + } + } + } + } +}; + +export const MessagingExtensionAttachment: msRest.CompositeMapper = { + serializedName: 'MessagingExtensionAttachment', + type: { + name: 'Composite', + className: 'MessagingExtensionAttachment', + modelProperties: { + ...Attachment.type.modelProperties, + preview: { + serializedName: 'preview', + type: { + name: 'Composite', + className: 'Attachment' + } + } + } + } +}; + +export const MessagingExtensionSuggestedAction: msRest.CompositeMapper = { + serializedName: 'MessagingExtensionSuggestedAction', + type: { + name: 'Composite', + className: 'MessagingExtensionSuggestedAction', + modelProperties: { + actions: { + serializedName: 'actions', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'CardAction' + } + } + } + } + } + } +}; + +export const MessagingExtensionResult: msRest.CompositeMapper = { + serializedName: 'MessagingExtensionResult', + type: { + name: 'Composite', + className: 'MessagingExtensionResult', + modelProperties: { + attachmentLayout: { + serializedName: 'attachmentLayout', + type: { + name: 'String' + } + }, + type: { + serializedName: 'type', + type: { + name: 'String' + } + }, + attachments: { + serializedName: 'attachments', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'MessagingExtensionAttachment' + } + } + } + }, + suggestedActions: { + serializedName: 'suggestedActions', + type: { + name: 'Composite', + className: 'MessagingExtensionSuggestedAction' + } + }, + text: { + serializedName: 'text', + type: { + name: 'String' + } + }, + activityPreview: { + serializedName: 'activityPreview', + type: { + name: 'Composite', + className: 'Activity' + } + } + } + } +}; + +export const MessagingExtensionActionResponse: msRest.CompositeMapper = { + serializedName: 'MessagingExtensionActionResponse', + type: { + name: 'Composite', + className: 'MessagingExtensionActionResponse', + modelProperties: { + task: { + serializedName: 'task', + type: { + name: 'Composite', + className: 'TaskModuleResponseBase' + } + }, + composeExtension: { + serializedName: 'composeExtension', + type: { + name: 'Composite', + className: 'MessagingExtensionResult' + } + } + } + } +}; + +export const MessagingExtensionResponse: msRest.CompositeMapper = { + serializedName: 'MessagingExtensionResponse', + type: { + name: 'Composite', + className: 'MessagingExtensionResponse', + modelProperties: { + composeExtension: { + serializedName: 'composeExtension', + type: { + name: 'Composite', + className: 'MessagingExtensionResult' + } + } + } + } +}; + +export const FileConsentCard: msRest.CompositeMapper = { + serializedName: 'FileConsentCard', + type: { + name: 'Composite', + className: 'FileConsentCard', + modelProperties: { + description: { + serializedName: 'description', + type: { + name: 'String' + } + }, + sizeInBytes: { + serializedName: 'sizeInBytes', + type: { + name: 'Number' + } + }, + acceptContext: { + serializedName: 'acceptContext', + type: { + name: 'Object' + } + }, + declineContext: { + serializedName: 'declineContext', + type: { + name: 'Object' + } + } + } + } +}; + +export const FileDownloadInfo: msRest.CompositeMapper = { + serializedName: 'FileDownloadInfo', + type: { + name: 'Composite', + className: 'FileDownloadInfo', + modelProperties: { + downloadUrl: { + serializedName: 'downloadUrl', + type: { + name: 'String' + } + }, + uniqueId: { + serializedName: 'uniqueId', + type: { + name: 'String' + } + }, + fileType: { + serializedName: 'fileType', + type: { + name: 'String' + } + }, + etag: { + serializedName: 'etag', + type: { + name: 'Object' + } + } + } + } +}; + +export const FileInfoCard: msRest.CompositeMapper = { + serializedName: 'FileInfoCard', + type: { + name: 'Composite', + className: 'FileInfoCard', + modelProperties: { + uniqueId: { + serializedName: 'uniqueId', + type: { + name: 'String' + } + }, + fileType: { + serializedName: 'fileType', + type: { + name: 'String' + } + }, + etag: { + serializedName: 'etag', + type: { + name: 'Object' + } + } + } + } +}; + +export const FileUploadInfo: msRest.CompositeMapper = { + serializedName: 'FileUploadInfo', + type: { + name: 'Composite', + className: 'FileUploadInfo', + modelProperties: { + name: { + serializedName: 'name', + type: { + name: 'String' + } + }, + uploadUrl: { + serializedName: 'uploadUrl', + type: { + name: 'String' + } + }, + contentUrl: { + serializedName: 'contentUrl', + type: { + name: 'String' + } + }, + uniqueId: { + serializedName: 'uniqueId', + type: { + name: 'String' + } + }, + fileType: { + serializedName: 'fileType', + type: { + name: 'String' + } + } + } + } +}; + +export const FileConsentCardResponse: msRest.CompositeMapper = { + serializedName: 'FileConsentCardResponse', + type: { + name: 'Composite', + className: 'FileConsentCardResponse', + modelProperties: { + action: { + serializedName: 'action', + type: { + name: 'String' + } + }, + context: { + serializedName: 'context', + type: { + name: 'Object' + } + }, + uploadInfo: { + serializedName: 'uploadInfo', + type: { + name: 'Composite', + className: 'FileUploadInfo' + } + } + } + } +}; + +export const TaskModuleTaskInfo: msRest.CompositeMapper = { + serializedName: 'TaskModuleTaskInfo', + type: { + name: 'Composite', + className: 'TaskModuleTaskInfo', + modelProperties: { + title: { + serializedName: 'title', + type: { + name: 'String' + } + }, + height: { + serializedName: 'height', + type: { + name: 'Object' + } + }, + width: { + serializedName: 'width', + type: { + name: 'Object' + } + }, + url: { + serializedName: 'url', + type: { + name: 'String' + } + }, + card: { + serializedName: 'card', + type: { + name: 'Composite', + className: 'Attachment' + } + }, + fallbackUrl: { + serializedName: 'fallbackUrl', + type: { + name: 'String' + } + }, + completionBotId: { + serializedName: 'completionBotId', + type: { + name: 'String' + } + } + } + } +}; + +export const TaskModuleContinueResponse: msRest.CompositeMapper = { + serializedName: 'TaskModuleContinueResponse', + type: { + name: 'Composite', + className: 'TaskModuleContinueResponse', + modelProperties: { + ...TaskModuleResponseBase.type.modelProperties, + value: { + serializedName: 'value', + type: { + name: 'Composite', + className: 'TaskModuleTaskInfo' + } + } + } + } +}; + +export const TaskModuleMessageResponse: msRest.CompositeMapper = { + serializedName: 'TaskModuleMessageResponse', + type: { + name: 'Composite', + className: 'TaskModuleMessageResponse', + modelProperties: { + ...TaskModuleResponseBase.type.modelProperties, + value: { + serializedName: 'value', + type: { + name: 'String' + } + } + } + } +}; + +export const TaskModuleResponse: msRest.CompositeMapper = { + serializedName: 'TaskModuleResponse', + type: { + name: 'Composite', + className: 'TaskModuleResponse', + modelProperties: { + task: { + serializedName: 'task', + type: { + name: 'Composite', + className: 'TaskModuleResponseBase' + } + } + } + } +}; + +export const TaskModuleRequestContext: msRest.CompositeMapper = { + serializedName: 'TaskModuleRequest_context', + type: { + name: 'Composite', + className: 'TaskModuleRequestContext', + modelProperties: { + theme: { + serializedName: 'theme', + type: { + name: 'String' + } + } + } + } +}; + +export const AppBasedLinkQuery: msRest.CompositeMapper = { + serializedName: 'AppBasedLinkQuery', + type: { + name: 'Composite', + className: 'AppBasedLinkQuery', + modelProperties: { + url: { + serializedName: 'url', + type: { + name: 'String' + } + } + } + } +}; diff --git a/libraries/botframework-connector/src/teams/models/parameters.ts b/libraries/botframework-connector/src/teams/models/parameters.ts new file mode 100644 index 0000000000..b42b7306fa --- /dev/null +++ b/libraries/botframework-connector/src/teams/models/parameters.ts @@ -0,0 +1,18 @@ +/* + * Code generated by Microsoft (R) AutoRest Code Generator. + * Changes may cause incorrect behavior and will be lost if the code is + * regenerated. + */ + +import * as msRest from '@azure/ms-rest-js'; + +export const teamId: msRest.OperationURLParameter = { + parameterPath: 'teamId', + mapper: { + required: true, + serializedName: 'teamId', + type: { + name: 'String' + } + } +}; diff --git a/libraries/botframework-connector/src/teams/models/teamsMappers.ts b/libraries/botframework-connector/src/teams/models/teamsMappers.ts new file mode 100644 index 0000000000..3f08cd17f5 --- /dev/null +++ b/libraries/botframework-connector/src/teams/models/teamsMappers.ts @@ -0,0 +1,13 @@ +/* + * Code generated by Microsoft (R) AutoRest Code Generator. + * Changes may cause incorrect behavior and will be lost if the code is + * regenerated. + */ +// This code has been manually edited to reflect the integration of the Teams schemas into botframework-schema +// and the botframework-connector libraries. + +export { + ConversationList, + ChannelInfo, + TeamDetails +} from './mappers'; diff --git a/libraries/botframework-connector/src/teams/operations/index.ts b/libraries/botframework-connector/src/teams/operations/index.ts new file mode 100644 index 0000000000..d89f7a6ebe --- /dev/null +++ b/libraries/botframework-connector/src/teams/operations/index.ts @@ -0,0 +1,7 @@ +/* + * Code generated by Microsoft (R) AutoRest Code Generator. + * Changes may cause incorrect behavior and will be lost if the code is + * regenerated. + */ + +export * from './teams'; diff --git a/libraries/botframework-connector/src/teams/operations/teams.ts b/libraries/botframework-connector/src/teams/operations/teams.ts new file mode 100644 index 0000000000..52c167211f --- /dev/null +++ b/libraries/botframework-connector/src/teams/operations/teams.ts @@ -0,0 +1,117 @@ +/* + * Code generated by Microsoft (R) AutoRest Code Generator. + * Changes may cause incorrect behavior and will be lost if the code is + * regenerated. + */ +// This code has been manually edited to reflect the integration of the Teams schemas into botframework-schema +// and the botframework-connector libraries. + +import * as msRest from '@azure/ms-rest-js'; +import * as Models from '../models'; +import * as Mappers from '../models/teamsMappers'; +import * as Parameters from '../models/parameters'; +import { TeamsConnectorClientContext } from '../'; +import { ConversationList, TeamDetails } from 'botframework-schema'; + +/** Class representing a Teams. */ +export class Teams { + private readonly client: TeamsConnectorClientContext; + + /** + * Create a Teams. + * @param {TeamsConnectorClientContext} client Reference to the service client. + */ + constructor(client: TeamsConnectorClientContext) { + this.client = client; + } + + /** + * Fetch the channel list. + * @summary Fetches channel list for a given team + * @param teamId Team Id + * @param [options] The optional parameters + * @returns Promise + */ + fetchChannelList(teamId: string, options?: msRest.RequestOptionsBase): Promise; + /** + * @param teamId Team Id + * @param callback The callback + */ + fetchChannelList(teamId: string, callback: msRest.ServiceCallback): void; + /** + * @param teamId Team Id + * @param options The optional parameters + * @param callback The callback + */ + fetchChannelList(teamId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback): void; + fetchChannelList(teamId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback, callback?: msRest.ServiceCallback): Promise { + return this.client.sendOperationRequest( + { + teamId, + options + }, + fetchChannelListOperationSpec, + callback) as Promise; + } + + /** + * Fetch details for a team + * @summary Fetches details related to a team + * @param teamId Team Id + * @param [options] The optional parameters + * @returns Promise + */ + fetchTeamDetails(teamId: string, options?: msRest.RequestOptionsBase): Promise; + /** + * @param teamId Team Id + * @param callback The callback + */ + fetchTeamDetails(teamId: string, callback: msRest.ServiceCallback): void; + /** + * @param teamId Team Id + * @param options The optional parameters + * @param callback The callback + */ + fetchTeamDetails(teamId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback): void; + fetchTeamDetails(teamId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback, callback?: msRest.ServiceCallback): Promise { + return this.client.sendOperationRequest( + { + teamId, + options + }, + fetchTeamDetailsOperationSpec, + callback) as Promise; + } +} + +// Operation Specifications +const serializer = new msRest.Serializer(Mappers); +const fetchChannelListOperationSpec: msRest.OperationSpec = { + httpMethod: 'GET', + path: 'v3/teams/{teamId}/conversations', + urlParameters: [ + Parameters.teamId + ], + responses: { + 200: { + bodyMapper: Mappers.ConversationList + }, + default: {} + }, + serializer +}; + +const fetchTeamDetailsOperationSpec: msRest.OperationSpec = { + httpMethod: 'GET', + path: 'v3/teams/{teamId}', + urlParameters: [ + Parameters.teamId + ], + responses: { + 200: { + bodyMapper: Mappers.TeamDetails + }, + default: {} + }, + serializer +}; diff --git a/libraries/botframework-connector/src/teams/teamsConnectorClient.ts b/libraries/botframework-connector/src/teams/teamsConnectorClient.ts new file mode 100644 index 0000000000..51cbfab707 --- /dev/null +++ b/libraries/botframework-connector/src/teams/teamsConnectorClient.ts @@ -0,0 +1,38 @@ +/* + * Code generated by Microsoft (R) AutoRest Code Generator. + * Changes may cause incorrect behavior and will be lost if the code is + * regenerated. + */ +// This code has been manually edited to reflect the integration of the Teams schemas into botframework-schema +// and the botframework-connector libraries. + +import { ServiceClientCredentials } from '@azure/ms-rest-js'; +import * as Models from './models'; +import * as Mappers from './models/mappers'; +import * as operations from './operations'; +import { TeamsConnectorClientContext } from './teamsConnectorClientContext'; + +class TeamsConnectorClient extends TeamsConnectorClientContext { + // Operation groups + teams: operations.Teams; + + /** + * Initializes a new instance of the TeamsConnectorClient class. + * @param credentials Subscription credentials which uniquely identify client subscription. + * @param [options] The parameter options + */ + constructor(credentials: ServiceClientCredentials, options?: Models.TeamsConnectorClientOptions) { + super(credentials, options); + this.teams = new operations.Teams(this); + } +} + +// Operation Specifications + +export { + TeamsConnectorClient, + TeamsConnectorClientContext, + Models as TeamsConnectorModels, + Mappers as TeamsConnectorMappers +}; +export * from './operations'; diff --git a/libraries/botframework-connector/src/teams/teamsConnectorClientContext.ts b/libraries/botframework-connector/src/teams/teamsConnectorClientContext.ts new file mode 100644 index 0000000000..cc6720ea1d --- /dev/null +++ b/libraries/botframework-connector/src/teams/teamsConnectorClientContext.ts @@ -0,0 +1,35 @@ +/* + * Code generated by Microsoft (R) AutoRest Code Generator. + * Changes may cause incorrect behavior and will be lost if the code is + * regenerated. + */ +// This code has been manually edited to reflect the integration of the Teams schemas into botframework-schema +// and the botframework-connector libraries. + +import { + ServiceClientCredentials, + ServiceClient +} from '@azure/ms-rest-js'; +import { TeamsConnectorClientOptions } from './models'; + +export class TeamsConnectorClientContext extends ServiceClient { + credentials: ServiceClientCredentials; + + /** + * Initializes a new instance of the TeamsConnectorClientContext class. + * @param credentials Subscription credentials which uniquely identify client subscription. + * @param [options] The parameter options + */ + constructor(credentials: ServiceClientCredentials, options?: TeamsConnectorClientOptions) { + + if (!options) { + options = {}; + } + + super(credentials, options); + + this.baseUri = options.baseUri || this.baseUri || 'https://api.botframework.com'; + this.requestContentType = 'application/json; charset=utf-8'; + this.credentials = credentials; + } +} diff --git a/libraries/botframework-connector/tsconfig.json b/libraries/botframework-connector/tsconfig.json index a401ef0291..8a583f3a89 100644 --- a/libraries/botframework-connector/tsconfig.json +++ b/libraries/botframework-connector/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "ESNext", + "target": "es6", + "lib": ["es2015", "dom"], "module": "commonjs", "strict": false, "declaration": true, diff --git a/libraries/botframework-expressions/package-lock.json b/libraries/botframework-expressions/package-lock.json index 43243f850b..0c8ff7d044 100644 --- a/libraries/botframework-expressions/package-lock.json +++ b/libraries/botframework-expressions/package-lock.json @@ -9,11 +9,6 @@ "resolved": "https://registry.npmjs.org/@types/mocha/-/@types/mocha-5.2.7.tgz", "integrity": "sha1-MV1XDMtWxTRS/4Y4c432BybVtuo=", "dev": true - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha1-tEf2ZwoEVbv+7dETku/zMOoJdUg=" } } } diff --git a/libraries/botframework-expressions/src/builtInFunction.ts b/libraries/botframework-expressions/src/builtInFunction.ts index 698c323baa..fbf9ea8fc6 100644 --- a/libraries/botframework-expressions/src/builtInFunction.ts +++ b/libraries/botframework-expressions/src/builtInFunction.ts @@ -2112,12 +2112,11 @@ export class BuiltInFunctions { (args: ReadonlyArray) => { let value: any; let error: string; - const dateFormat: string = 'YYYY-MM-DD'; ({ value, error } = BuiltInFunctions.ParseTimestamp(args[0])); if (error === undefined) { - const timestamp1: Date = new Date(value.format(dateFormat)); + const timestamp1: Date = value.startOf('day').toDate(); ({ value, error } = BuiltInFunctions.ParseTimestamp(args[1])); - const timestamp2: string = value.format(dateFormat); + const timestamp2: string = value.startOf('day').local().format('YYYY-MM-DD'); const timex: TimexProperty = new TimexProperty(timestamp2); return { value: timex.toNaturalLanguage(timestamp1), error }; diff --git a/libraries/botframework-expressions/tests/expression.test.js b/libraries/botframework-expressions/tests/expression.test.js index 52f588cb51..f1fbe9ad01 100644 --- a/libraries/botframework-expressions/tests/expression.test.js +++ b/libraries/botframework-expressions/tests/expression.test.js @@ -262,7 +262,7 @@ const dataSource = [ ["date(timestamp)", "3/15/2018"],//Default. TODO ["year(timestamp)", 2018], ["length(utcNow())", 24], - ["utcNow('MM-DD-YY')", moment(new Date().toISOString()).format('MM-DD-YY')], + ["utcNow('MM-DD-YY')", moment(new Date().toISOString()).utc().format('MM-DD-YY')], ["formatDateTime(notISOTimestamp)", "2018-03-15T13:00:00.000Z"], ["formatDateTime(notISOTimestamp, 'MM-dd-yy')", "03-15-18"], ["formatDateTime('2018-03-15')", "2018-03-15T00:00:00.000Z"], @@ -284,14 +284,14 @@ const dataSource = [ ["getTimeOfDay('2018-03-15T18:00:00.000Z')", "evening"], ["getTimeOfDay('2018-03-15T22:00:00.000Z')", "evening"], ["getTimeOfDay('2018-03-15T23:00:00.000Z')", "night"], - ["getPastTime(1, 'Year', 'MM-dd-yy')", moment(new Date().toISOString()).subtract(1, 'years').format('MM-DD-YY')], - ["getPastTime(1, 'Month', 'MM-dd-yy')", moment(new Date().toISOString()).subtract(1, 'months').format('MM-DD-YY')], - ["getPastTime(1, 'Week', 'MM-dd-yy')", moment(new Date().toISOString()).subtract(7, 'days').format('MM-DD-YY')], - ["getPastTime(1, 'Day', 'MM-dd-yy')", moment(new Date().toISOString()).subtract(1, 'days').format('MM-DD-YY')], - ["getFutureTime(1, 'Year', 'MM-dd-yy')", moment(new Date().toISOString()).add(1, 'years').format('MM-DD-YY')], - ["getFutureTime(1, 'Month', 'MM-dd-yy')", moment(new Date().toISOString()).add(1, 'months').format('MM-DD-YY')], - ["getFutureTime(1, 'Week', 'MM-dd-yy')", moment(new Date().toISOString()).add(7, 'days').format('MM-DD-YY')], - ["getFutureTime(1, 'Day', 'MM-dd-yy')", moment(new Date().toISOString()).add(1, 'days').format('MM-DD-YY')], + ["getPastTime(1, 'Year', 'MM-dd-yy')", moment(new Date().toISOString()).utc().subtract(1, 'years').format('MM-DD-YY')], + ["getPastTime(1, 'Month', 'MM-dd-yy')", moment(new Date().toISOString()).utc().subtract(1, 'months').format('MM-DD-YY')], + ["getPastTime(1, 'Week', 'MM-dd-yy')", moment(new Date().toISOString()).utc().subtract(7, 'days').format('MM-DD-YY')], + ["getPastTime(1, 'Day', 'MM-dd-yy')", moment(new Date().toISOString()).utc().subtract(1, 'days').format('MM-DD-YY')], + ["getFutureTime(1, 'Year', 'MM-dd-yy')", moment(new Date().toISOString()).utc().add(1, 'years').format('MM-DD-YY')], + ["getFutureTime(1, 'Month', 'MM-dd-yy')", moment(new Date().toISOString()).utc().add(1, 'months').format('MM-DD-YY')], + ["getFutureTime(1, 'Week', 'MM-dd-yy')", moment(new Date().toISOString()).utc().add(7, 'days').format('MM-DD-YY')], + ["getFutureTime(1, 'Day', 'MM-dd-yy')", moment(new Date().toISOString()).utc().add(1, 'days').format('MM-DD-YY')], ["addToTime('2018-01-01T08:00:00.000Z', 1, 'Day')", "2018-01-02T08:00:00.000+00:00"], ["addToTime('2018-01-01T08:00:00.000Z', sub(3,1), 'Week')", "2018-01-15T08:00:00.000+00:00"], ["addToTime('2018-01-01T08:00:00.000Z', 1, 'Month', 'MM-DD-YY')", "02-01-18"], diff --git a/libraries/botframework-schema/src/index.ts b/libraries/botframework-schema/src/index.ts index 6f20a91f79..ec87f65dba 100644 --- a/libraries/botframework-schema/src/index.ts +++ b/libraries/botframework-schema/src/index.ts @@ -4,6 +4,9 @@ */ export * from './activityInterfaces'; +// The Teams schemas was manually added to this library. This file has been updated to export those schemas. +export * from './teams'; + /** * Attachment View name and size */ @@ -160,7 +163,7 @@ export interface CardAction { /** * The type of action implemented by this button. Possible values include: 'openUrl', 'imBack', * 'postBack', 'playAudio', 'playVideo', 'showImage', 'downloadFile', 'signin', 'call', - * 'payment', 'messageBack' + * 'payment', 'messageBack', 'openApp' */ type: ActionTypes | string; /** @@ -1240,6 +1243,10 @@ export interface TokenResponse { * Expiration for the token, in ISO 8601 format (e.g. "2007-04-05T14:30Z") */ expiration: string; + /** + * A collection of properties about this response, such as token polling parameters + */ + properties?: { [propertyName: string]: any }; } /** @@ -1683,7 +1690,7 @@ export enum InputHints { /** * Defines values for ActionTypes. * Possible values include: 'openUrl', 'imBack', 'postBack', 'playAudio', 'playVideo', 'showImage', - * 'downloadFile', 'signin', 'call', 'payment', 'messageBack' + * 'downloadFile', 'signin', 'call', 'payment', 'messageBack', 'openApp' * @readonly * @enum {string} */ @@ -1699,6 +1706,7 @@ export enum ActionTypes { Call = 'call', Payment = 'payment', MessageBack = 'messageBack', + OpenApp = 'openApp', } /** diff --git a/libraries/botframework-schema/src/teams/extension/index.ts b/libraries/botframework-schema/src/teams/extension/index.ts new file mode 100644 index 0000000000..3b4559ce10 --- /dev/null +++ b/libraries/botframework-schema/src/teams/extension/index.ts @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { Attachment } from '../../'; +import * as teams from '../'; + +/** + * Defines values for Type. + * Possible values include: 'ViewAction', 'OpenUri', 'HttpPOST', 'ActionCard' + * @readonly + * @enum {string} + */ +export type O365ConnectorCardActionType = 'ViewAction' | 'OpenUri' | 'HttpPOST' | 'ActionCard'; + +/** + * @interface + * An interface representing O365ConnectorCardActionBase. + * O365 connector card action base + * + */ +export interface O365ConnectorCardActionBase { + /** + * @member {Type} [type] Type of the action. Possible values include: + * 'ViewAction', 'OpenUri', 'HttpPOST', 'ActionCard' + */ + '@type'?: O365ConnectorCardActionType; + /** + * @member {string} [name] Name of the action that will be used as button + * title + */ + name?: string; + /** + * @member {string} [id] Action Id + */ + '@id'?: string; +} + +/** + * Defines values for Type1. + * Possible values include: 'textInput', 'dateInput', 'multichoiceInput' + * @readonly + * @enum {string} + */ +export type O365ConnectorCardInputType = 'textInput' | 'dateInput' | 'multichoiceInput'; + +/** + * @interface + * An interface representing O365ConnectorCardInputBase. + * O365 connector card input for ActionCard action + * + */ +export interface O365ConnectorCardInputBase { + /** + * @member {Type1} [type] Input type name. Possible values include: + * 'textInput', 'dateInput', 'multichoiceInput' + */ + '@type'?: O365ConnectorCardInputType; + /** + * @member {string} [id] Input Id. It must be unique per entire O365 + * connector card. + */ + id?: string; + /** + * @member {boolean} [isRequired] Define if this input is a required field. + * Default value is false. + */ + isRequired?: boolean; + /** + * @member {string} [title] Input title that will be shown as the placeholder + */ + title?: string; + /** + * @member {string} [value] Default value for this input field + */ + value?: string; +} + +export interface TeamsAttachment extends Attachment { + content: ContentType; +} + +export type FileDownloadInfoAttachment = TeamsAttachment; + +/** + * @interface + * An interface representing MessageActionsPayloadBody. + * Plaintext/HTML representation of the content of the message. + * + */ +export interface MessageActionsPayloadBody { + /** + * @member {ContentType} [contentType] Type of the content. Possible values + * include: 'html', 'text' + */ + contentType?: teams.ContentType; + /** + * @member {string} [content] The content of the body. + */ + content?: string; + /** + * @member {string} [textContent] The text content of the body after + * stripping HTML tags. + */ + textContent?: string; +} diff --git a/libraries/botframework-schema/src/teams/index.ts b/libraries/botframework-schema/src/teams/index.ts new file mode 100644 index 0000000000..0c40c6ecd0 --- /dev/null +++ b/libraries/botframework-schema/src/teams/index.ts @@ -0,0 +1,1362 @@ +/* + * Code generated by Microsoft (R) AutoRest Code Generator. + * Changes may cause incorrect behavior and will be lost if the code is + * regenerated. + */ + + // The Teams schemas was manually added to botframework-schema. This file has been updated import from the botframework-schema and the extension folder. +import * as teams from './extension'; +import { Activity, Attachment, CardAction, ChannelAccount } from '../'; +export * from './extension'; + +/** + * @interface + * An interface representing ChannelInfo. + * A channel info object which decribes the channel. + * + */ +export interface ChannelInfo { + /** + * @member {string} [id] Unique identifier representing a channel + */ + id?: string; + /** + * @member {string} [name] Name of the channel + */ + name?: string; +} + +/** + * @interface + * An interface representing ConversationList. + * List of channels under a team + * + */ +export interface ConversationList { + /** + * @member {ChannelInfo[]} [conversations] + */ + conversations?: ChannelInfo[]; +} + +/** + * @interface + * An interface representing TeamDetails. + * Details related to a team + * + */ +export interface TeamDetails { + /** + * @member {string} [id] Unique identifier representing a team + */ + id?: string; + /** + * @member {string} [name] Name of team. + */ + name?: string; + /** + * @member {string} [aadGroupId] Azure Active Directory (AAD) Group Id for + * the team. + */ + aadGroupId?: string; +} + +/** + * @interface + * An interface representing TeamInfo. + * Describes a team + * + */ +export interface TeamInfo { + /** + * @member {string} [id] Unique identifier representing a team + */ + id?: string; + /** + * @member {string} [name] Name of team. + */ + name?: string; +} + +/** + * @interface + * An interface representing NotificationInfo. + * Specifies if a notification is to be sent for the mentions. + * + */ +export interface NotificationInfo { + /** + * @member {boolean} [alert] true if notification is to be sent to the user, + * false otherwise. + */ + alert?: boolean; +} + +/** + * @interface + * An interface representing TenantInfo. + * Describes a tenant + * + */ +export interface TenantInfo { + /** + * @member {string} [id] Unique identifier representing a tenant + */ + id?: string; +} + +/** + * @interface + * An interface representing TeamsChannelData. + * Channel data specific to messages received in Microsoft Teams + * + */ +export interface TeamsChannelData { + /** + * @member {ChannelInfo} [channel] Information about the channel in which the + * message was sent + */ + channel?: ChannelInfo; + /** + * @member {string} [eventType] Type of event. + */ + eventType?: string; + /** + * @member {TeamInfo} [team] Information about the team in which the message + * was sent + */ + team?: TeamInfo; + /** + * @member {NotificationInfo} [notification] Notification settings for the + * message + */ + notification?: NotificationInfo; + /** + * @member {TenantInfo} [tenant] Information about the tenant in which the + * message was sent + */ + tenant?: TenantInfo; +} + +/** + * @interface + * An interface representing TeamsChannelAccount. + * Teams channel account detailing user Azure Active Directory details. + * + * @extends ChannelAccount + */ +export interface TeamsChannelAccount extends ChannelAccount { + /** + * @member {string} [givenName] Given name part of the user name. + */ + givenName?: string; + /** + * @member {string} [surname] Surname part of the user name. + */ + surname?: string; + /** + * @member {string} [email] Email Id of the user. + */ + email?: string; + /** + * @member {string} [userPrincipalName] Unique user principal name + */ + userPrincipalName?: string; +} + +/** + * @interface + * An interface representing O365ConnectorCardFact. + * O365 connector card fact + * + */ +export interface O365ConnectorCardFact { + /** + * @member {string} [name] Display name of the fact + */ + name?: string; + /** + * @member {string} [value] Display value for the fact + */ + value?: string; +} + +/** + * @interface + * An interface representing O365ConnectorCardImage. + * O365 connector card image + * + */ +export interface O365ConnectorCardImage { + /** + * @member {string} [image] URL for the image + */ + image?: string; + /** + * @member {string} [title] Alternative text for the image + */ + title?: string; +} + +/** + * @interface + * An interface representing O365ConnectorCardSection. + * O365 connector card section + * + */ +export interface O365ConnectorCardSection { + /** + * @member {string} [title] Title of the section + */ + title?: string; + /** + * @member {string} [text] Text for the section + */ + text?: string; + /** + * @member {string} [activityTitle] Activity title + */ + activityTitle?: string; + /** + * @member {string} [activitySubtitle] Activity subtitle + */ + activitySubtitle?: string; + /** + * @member {string} [activityText] Activity text + */ + activityText?: string; + /** + * @member {string} [activityImage] Activity image + */ + activityImage?: string; + /** + * @member {ActivityImageType} [activityImageType] Describes how Activity + * image is rendered. Possible values include: 'avatar', 'article' + */ + activityImageType?: ActivityImageType; + /** + * @member {boolean} [markdown] Use markdown for all text contents. Default + * vaule is true. + */ + markdown?: boolean; + /** + * @member {O365ConnectorCardFact[]} [facts] Set of facts for the current + * section + */ + facts?: O365ConnectorCardFact[]; + /** + * @member {O365ConnectorCardImage[]} [images] Set of images for the current + * section + */ + images?: O365ConnectorCardImage[]; + /** + * @member {O365ConnectorCardActionBase[]} [potentialAction] Set of actions + * for the current section + */ + potentialAction?: teams.O365ConnectorCardActionBase[]; +} + +/** + * @interface + * An interface representing O365ConnectorCard. + * O365 connector card + * + */ +export interface O365ConnectorCard { + /** + * @member {string} [title] Title of the item + */ + title?: string; + /** + * @member {string} [text] Text for the card + */ + text?: string; + /** + * @member {string} [summary] Summary for the card + */ + summary?: string; + /** + * @member {string} [themeColor] Theme color for the card + */ + themeColor?: string; + /** + * @member {O365ConnectorCardSection[]} [sections] Set of sections for the + * current card + */ + sections?: O365ConnectorCardSection[]; + /** + * @member {O365ConnectorCardActionBase[]} [potentialAction] Set of actions + * for the current card + */ + potentialAction?: teams.O365ConnectorCardActionBase[]; +} + +/** + * @interface + * An interface representing O365ConnectorCardViewAction. + * O365 connector card ViewAction action + * + * @extends teams.O365ConnectorCardActionBase + */ +export interface O365ConnectorCardViewAction extends teams.O365ConnectorCardActionBase { + /** + * @member {string[]} [target] Target urls, only the first url effective for + * card button + */ + target?: string[]; +} + +/** + * @interface + * An interface representing O365ConnectorCardOpenUriTarget. + * O365 connector card OpenUri target + * + */ +export interface O365ConnectorCardOpenUriTarget { + /** + * @member {Os} [os] Target operating system. Possible values include: + * 'default', 'iOS', 'android', 'windows' + */ + os?: Os; + /** + * @member {string} [uri] Target url + */ + uri?: string; +} + +/** + * @interface + * An interface representing O365ConnectorCardOpenUri. + * O365 connector card OpenUri action + * + * @extends teams.O365ConnectorCardActionBase + */ +export interface O365ConnectorCardOpenUri extends teams.O365ConnectorCardActionBase { + /** + * @member {O365ConnectorCardOpenUriTarget[]} [targets] Target os / urls + */ + targets?: O365ConnectorCardOpenUriTarget[]; +} + +/** + * @interface + * An interface representing O365ConnectorCardHttpPOST. + * O365 connector card HttpPOST action + * + * @extends teams.O365ConnectorCardActionBase + */ +export interface O365ConnectorCardHttpPOST extends teams.O365ConnectorCardActionBase { + /** + * @member {string} [body] Content to be posted back to bots via invoke + */ + body?: string; +} + +/** + * @interface + * An interface representing O365ConnectorCardActionCard. + * O365 connector card ActionCard action + * + * @extends teams.O365ConnectorCardActionBase + */ +export interface O365ConnectorCardActionCard extends teams.O365ConnectorCardActionBase { + /** + * @member {O365ConnectorCardInputBase[]} [inputs] Set of inputs contained in + * this ActionCard whose each item can be in any subtype of + * teams.O365ConnectorCardInputBase + */ + inputs?: teams.O365ConnectorCardInputBase[]; + /** + * @member {O365ConnectorCardActionBase[]} [actions] Set of actions contained + * in this ActionCard whose each item can be in any subtype of + * teams.O365ConnectorCardActionBase except O365ConnectorCardActionCard, as nested + * ActionCard is forbidden. + */ + actions?: teams.O365ConnectorCardActionBase[]; +} + +/** + * @interface + * An interface representing O365ConnectorCardTextInput. + * O365 connector card text input + * + * @extends teams.O365ConnectorCardInputBase + */ +export interface O365ConnectorCardTextInput extends teams.O365ConnectorCardInputBase { + /** + * @member {boolean} [isMultiline] Define if text input is allowed for + * multiple lines. Default value is false. + */ + isMultiline?: boolean; + /** + * @member {number} [maxLength] Maximum length of text input. Default value + * is unlimited. + */ + maxLength?: number; +} + +/** + * @interface + * An interface representing O365ConnectorCardDateInput. + * O365 connector card date input + * + * @extends teams.O365ConnectorCardInputBase + */ +export interface O365ConnectorCardDateInput extends teams.O365ConnectorCardInputBase { + /** + * @member {boolean} [includeTime] Include time input field. Default value + * is false (date only). + */ + includeTime?: boolean; +} + +/** + * @interface + * An interface representing O365ConnectorCardMultichoiceInputChoice. + * O365O365 connector card multiple choice input item + * + */ +export interface O365ConnectorCardMultichoiceInputChoice { + /** + * @member {string} [display] The text rednered on ActionCard. + */ + display?: string; + /** + * @member {string} [value] The value received as results. + */ + value?: string; +} + +/** + * @interface + * An interface representing O365ConnectorCardMultichoiceInput. + * O365 connector card multiple choice input + * + * @extends teams.O365ConnectorCardInputBase + */ +export interface O365ConnectorCardMultichoiceInput extends teams.O365ConnectorCardInputBase { + /** + * @member {O365ConnectorCardMultichoiceInputChoice[]} [choices] Set of + * choices whose each item can be in any subtype of + * O365ConnectorCardMultichoiceInputChoice. + */ + choices?: O365ConnectorCardMultichoiceInputChoice[]; + /** + * @member {Style} [style] Choice item rendering style. Default valud is + * 'compact'. Possible values include: 'compact', 'expanded' + */ + style?: Style; + /** + * @member {boolean} [isMultiSelect] Define if this input field allows + * multiple selections. Default value is false. + */ + isMultiSelect?: boolean; +} + +/** + * @interface + * An interface representing O365ConnectorCardActionQuery. + * O365 connector card HttpPOST invoke query + * + */ +export interface O365ConnectorCardActionQuery { + /** + * @member {string} [body] The results of body string defined in + * IO365ConnectorCardHttpPOST with substituted input values + */ + body?: string; + /** + * @member {string} [actionId] Action Id associated with the HttpPOST action + * button triggered, defined in teams.O365ConnectorCardActionBase. + */ + actionId?: string; +} + +/** + * @interface + * An interface representing SigninStateVerificationQuery. + * Signin state (part of signin action auth flow) verification invoke query + * + */ +export interface SigninStateVerificationQuery { + /** + * @member {string} [state] The state string originally received when the + * signin web flow is finished with a state posted back to client via tab SDK + * microsoftTeams.authentication.notifySuccess(state) + */ + state?: string; +} + +/** + * @interface + * An interface representing MessagingExtensionQueryOptions. + * Messaging extension query options + * + */ +export interface MessagingExtensionQueryOptions { + /** + * @member {number} [skip] Number of entities to skip + */ + skip?: number; + /** + * @member {number} [count] Number of entities to fetch + */ + count?: number; +} + +/** + * @interface + * An interface representing MessagingExtensionParameter. + * Messaging extension query parameters + * + */ +export interface MessagingExtensionParameter { + /** + * @member {string} [name] Name of the parameter + */ + name?: string; + /** + * @member {any} [value] Value of the parameter + */ + value?: any; +} + +/** + * @interface + * An interface representing MessagingExtensionQuery. + * Messaging extension query + * + */ +export interface MessagingExtensionQuery { + /** + * @member {string} [commandId] Id of the command assigned by Bot + */ + commandId?: string; + /** + * @member {MessagingExtensionParameter[]} [parameters] Parameters for the + * query + */ + parameters?: MessagingExtensionParameter[]; + /** + * @member {MessagingExtensionQueryOptions} [queryOptions] + */ + queryOptions?: MessagingExtensionQueryOptions; + /** + * @member {string} [state] State parameter passed back to the bot after + * authentication/configuration flow + */ + state?: string; +} + +/** + * @interface + * An interface representing MessageActionsPayloadUser. + * Represents a user entity. + * + */ +export interface MessageActionsPayloadUser { + /** + * @member {UserIdentityType} [userIdentityType] The identity type of the + * user. Possible values include: 'aadUser', 'onPremiseAadUser', + * 'anonymousGuest', 'federatedUser' + */ + userIdentityType?: UserIdentityType; + /** + * @member {string} [id] The id of the user. + */ + id?: string; + /** + * @member {string} [displayName] The plaintext display name of the user. + */ + displayName?: string; +} + +/** + * @interface + * An interface representing MessageActionsPayloadApp. + * Represents an application entity. + * + */ +export interface MessageActionsPayloadApp { + /** + * @member {ApplicationIdentityType} [applicationIdentityType] The type of + * application. Possible values include: 'aadApplication', 'bot', + * 'tenantBot', 'office365Connector', 'webhook' + */ + applicationIdentityType?: ApplicationIdentityType; + /** + * @member {string} [id] The id of the application. + */ + id?: string; + /** + * @member {string} [displayName] The plaintext display name of the + * application. + */ + displayName?: string; +} + +/** + * @interface + * An interface representing MessageActionsPayloadConversation. + * Represents a team or channel entity. + * + */ +export interface MessageActionsPayloadConversation { + /** + * @member {ConversationIdentityType} [conversationIdentityType] The type of + * conversation, whether a team or channel. Possible values include: 'team', + * 'channel' + */ + conversationIdentityType?: ConversationIdentityType; + /** + * @member {string} [id] The id of the team or channel. + */ + id?: string; + /** + * @member {string} [displayName] The plaintext display name of the team or + * channel entity. + */ + displayName?: string; +} + +/** + * @interface + * An interface representing MessageActionsPayloadFrom. + * Represents a user, application, or conversation type that either sent or was + * referenced in a message. + * + */ +export interface MessageActionsPayloadFrom { + /** + * @member {MessageActionsPayloadUser} [user] Represents details of the user. + */ + user?: MessageActionsPayloadUser; + /** + * @member {MessageActionsPayloadApp} [application] Represents details of the + * app. + */ + application?: MessageActionsPayloadApp; + /** + * @member {MessageActionsPayloadConversation} [conversation] Represents + * details of the converesation. + */ + conversation?: MessageActionsPayloadConversation; +} + +/** + * @interface + * An interface representing MessageActionsPayloadAttachment. + * Represents the attachment in a message. + * + */ +export interface MessageActionsPayloadAttachment { + /** + * @member {string} [id] The id of the attachment. + */ + id?: string; + /** + * @member {string} [contentType] The type of the attachment. + */ + contentType?: string; + /** + * @member {string} [contentUrl] The url of the attachment, in case of a + * external link. + */ + contentUrl?: string; + /** + * @member {any} [content] The content of the attachment, in case of a code + * snippet, email, or file. + */ + content?: any; + /** + * @member {string} [name] The plaintext display name of the attachment. + */ + name?: string; + /** + * @member {string} [thumbnailUrl] The url of a thumbnail image that might be + * embedded in the attachment, in case of a card. + */ + thumbnailUrl?: string; +} + +/** + * @interface + * An interface representing MessageActionsPayloadMention. + * Represents the entity that was mentioned in the message. + * + */ +export interface MessageActionsPayloadMention { + /** + * @member {number} [id] The id of the mentioned entity. + */ + id?: number; + /** + * @member {string} [mentionText] The plaintext display name of the mentioned + * entity. + */ + mentionText?: string; + /** + * @member {MessageActionsPayloadFrom} [mentioned] Provides more details on + * the mentioned entity. + */ + mentioned?: MessageActionsPayloadFrom; +} + +/** + * @interface + * An interface representing MessageActionsPayloadReaction. + * Represents the reaction of a user to a message. + * + */ +export interface MessageActionsPayloadReaction { + /** + * @member {ReactionType} [reactionType] The type of reaction given to the + * message. Possible values include: 'like', 'heart', 'laugh', 'surprised', + * 'sad', 'angry' + */ + reactionType?: ReactionType; + /** + * @member {string} [createdDateTime] Timestamp of when the user reacted to + * the message. + */ + createdDateTime?: string; + /** + * @member {MessageActionsPayloadFrom} [user] The user with which the + * reaction is associated. + */ + user?: MessageActionsPayloadFrom; +} + +/** + * @interface + * An interface representing MessageActionsPayload. + * Represents the individual message within a chat or channel where a message + * actions is taken. + * + */ +export interface MessageActionsPayload { + /** + * @member {string} [id] Unique id of the message. + */ + id?: string; + /** + * @member {string} [replyToId] Id of the parent/root message of the thread. + */ + replyToId?: string; + /** + * @member {MessageType} [messageType] Type of message - automatically set to + * message. Possible values include: 'message' + */ + messageType?: MessageType; + /** + * @member {string} [createdDateTime] Timestamp of when the message was + * created. + */ + createdDateTime?: string; + /** + * @member {string} [lastModifiedDateTime] Timestamp of when the message was + * edited or updated. + */ + lastModifiedDateTime?: string; + /** + * @member {boolean} [deleted] Indicates whether a message has been soft + * deleted. + */ + deleted?: boolean; + /** + * @member {string} [subject] Subject line of the message. + */ + subject?: string; + /** + * @member {string} [summary] Summary text of the message that could be used + * for notifications. + */ + summary?: string; + /** + * @member {Importance} [importance] The importance of the message. Possible + * values include: 'normal', 'high', 'urgent' + */ + importance?: Importance; + /** + * @member {string} [locale] Locale of the message set by the client. + */ + locale?: string; + /** + * @member {MessageActionsPayloadFrom} [from] Sender of the message. + */ + from?: MessageActionsPayloadFrom; + /** + * @member {MessageActionsPayloadBody} [body] Plaintext/HTML representation + * of the content of the message. + */ + body?: teams.MessageActionsPayloadBody; + /** + * @member {string} [attachmentLayout] How the attachment(s) are displayed in + * the message. + */ + attachmentLayout?: string; + /** + * @member {MessageActionsPayloadAttachment[]} [attachments] Attachments in + * the message - card, image, file, etc. + */ + attachments?: MessageActionsPayloadAttachment[]; + /** + * @member {MessageActionsPayloadMention[]} [mentions] List of entities + * mentioned in the message. + */ + mentions?: MessageActionsPayloadMention[]; + /** + * @member {MessageActionsPayloadReaction[]} [reactions] Reactions for the + * message. + */ + reactions?: MessageActionsPayloadReaction[]; +} + +/** + * @interface + * An interface representing TaskModuleRequest. + * Task module invoke request value payload + * + */ +export interface TaskModuleRequest { + /** + * @member {any} [data] User input data. Free payload with key-value pairs. + */ + data?: any; + /** + * @member {TaskModuleRequestContext} [context] Current user context, i.e., + * the current theme + */ + context?: TaskModuleRequestContext; +} + +/** + * @interface + * An interface representing MessagingExtensionAction. + * Messaging extension action + * + * @extends TaskModuleRequest + */ +export interface MessagingExtensionAction extends TaskModuleRequest { + /** + * @member {string} [commandId] Id of the command assigned by Bot + */ + commandId?: string; + /** + * @member {CommandContext} [commandContext] The context from which the + * command originates. Possible values include: 'message', 'compose', + * 'commandbox' + */ + commandContext?: CommandContext; + /** + * @member {BotMessagePreviewAction} [botMessagePreviewAction] Bot message + * preview action taken by user. Possible values include: 'edit', 'send' + */ + botMessagePreviewAction?: BotMessagePreviewAction; + /** + * @member {Activity[]} [botActivityPreview] + */ + botActivityPreview?: Activity[]; + /** + * @member {MessageActionsPayload} [messagePayload] Message content sent as + * part of the command request. + */ + messagePayload?: MessageActionsPayload; +} + +/** + * @interface + * An interface representing TaskModuleResponseBase. + * Base class for Task Module responses + * + */ +export interface TaskModuleResponseBase { + /** + * @member {Type2} [type] Choice of action options when responding to the + * task/submit message. Possible values include: 'message', 'continue' + */ + type?: Type2; +} + +/** + * @interface + * An interface representing MessagingExtensionAttachment. + * Messaging extension attachment. + * + * @extends Attachment + */ +export interface MessagingExtensionAttachment extends Attachment { + /** + * @member {Attachment} [preview] + */ + preview?: Attachment; +} + +/** + * @interface + * An interface representing MessagingExtensionSuggestedAction. + * Messaging extension Actions (Only when type is auth or config) + * + */ +export interface MessagingExtensionSuggestedAction { + /** + * @member {CardAction[]} [actions] Actions + */ + actions?: CardAction[]; +} + +/** + * @interface + * An interface representing MessagingExtensionResult. + * Messaging extension result + * + */ +export interface MessagingExtensionResult { + /** + * @member {AttachmentLayout} [attachmentLayout] Hint for how to deal with + * multiple attachments. Possible values include: 'list', 'grid' + */ + attachmentLayout?: AttachmentLayout; + /** + * @member {Type3} [type] The type of the result. Possible values include: + * 'result', 'auth', 'config', 'message', 'botMessagePreview' + */ + type?: Type3; + /** + * @member {MessagingExtensionAttachment[]} [attachments] (Only when type is + * result) Attachments + */ + attachments?: MessagingExtensionAttachment[]; + /** + * @member {MessagingExtensionSuggestedAction} [suggestedActions] + */ + suggestedActions?: MessagingExtensionSuggestedAction; + /** + * @member {string} [text] (Only when type is message) Text + */ + text?: string; + /** + * @member {Activity} [activityPreview] (Only when type is botMessagePreview) + * Message activity to preview + */ + activityPreview?: Activity; +} + +/** + * @interface + * An interface representing MessagingExtensionActionResponse. + * Response of messaging extension action + * + */ +export interface MessagingExtensionActionResponse { + /** + * @member {TaskModuleResponseBase} [task] The JSON for the Adaptive card to + * appear in the task module. + */ + task?: TaskModuleResponseBase; + /** + * @member {MessagingExtensionResult} [composeExtension] + */ + composeExtension?: MessagingExtensionResult; +} + +/** + * @interface + * An interface representing MessagingExtensionResponse. + * Messaging extension response + * + */ +export interface MessagingExtensionResponse { + /** + * @member {MessagingExtensionResult} [composeExtension] + */ + composeExtension?: MessagingExtensionResult; +} + +/** + * @interface + * An interface representing FileConsentCard. + * File consent card attachment. + * + */ +export interface FileConsentCard { + /** + * @member {string} [description] File description. + */ + description?: string; + /** + * @member {number} [sizeInBytes] Size of the file to be uploaded in Bytes. + */ + sizeInBytes?: number; + /** + * @member {any} [acceptContext] Context sent back to the Bot if user + * consented to upload. This is free flow schema and is sent back in Value + * field of Activity. + */ + acceptContext?: any; + /** + * @member {any} [declineContext] Context sent back to the Bot if user + * declined. This is free flow schema and is sent back in Value field of + * Activity. + */ + declineContext?: any; +} + +/** + * @interface + * An interface representing FileDownloadInfo. + * File download info attachment. + * + */ +export interface FileDownloadInfo { + /** + * @member {string} [downloadUrl] File download url. + */ + downloadUrl?: string; + /** + * @member {string} [uniqueId] Unique Id for the file. + */ + uniqueId?: string; + /** + * @member {string} [fileType] Type of file. + */ + fileType?: string; + /** + * @member {any} [etag] ETag for the file. + */ + etag?: any; +} + +/** + * @interface + * An interface representing FileInfoCard. + * File info card. + * + */ +export interface FileInfoCard { + /** + * @member {string} [uniqueId] Unique Id for the file. + */ + uniqueId?: string; + /** + * @member {string} [fileType] Type of file. + */ + fileType?: string; + /** + * @member {any} [etag] ETag for the file. + */ + etag?: any; +} + +/** + * @interface + * An interface representing FileUploadInfo. + * Information about the file to be uploaded. + * + */ +export interface FileUploadInfo { + /** + * @member {string} [name] Name of the file. + */ + name?: string; + /** + * @member {string} [uploadUrl] URL to an upload session that the bot can use + * to set the file contents. + */ + uploadUrl?: string; + /** + * @member {string} [contentUrl] URL to file. + */ + contentUrl?: string; + /** + * @member {string} [uniqueId] ID that uniquely identifies the file. + */ + uniqueId?: string; + /** + * @member {string} [fileType] Type of the file. + */ + fileType?: string; +} + +/** + * @interface + * An interface representing FileConsentCardResponse. + * Represents the value of the invoke activity sent when the user acts on a + * file consent card + * + */ +export interface FileConsentCardResponse { + /** + * @member {Action} [action] The action the user took. Possible values + * include: 'accept', 'decline' + */ + action?: Action; + /** + * @member {any} [context] The context associated with the action. + */ + context?: any; + /** + * @member {FileUploadInfo} [uploadInfo] If the user accepted the file, + * contains information about the file to be uploaded. + */ + uploadInfo?: FileUploadInfo; +} + +/** + * @interface + * An interface representing TaskModuleTaskInfo. + * Metadata for a Task Module. + * + */ +export interface TaskModuleTaskInfo { + /** + * @member {string} [title] Appears below the app name and to the right of + * the app icon. + */ + title?: string; + /** + * @member {any} [height] This can be a number, representing the task + * module's height in pixels, or a string, one of: small, medium, large. + */ + height?: any; + /** + * @member {any} [width] This can be a number, representing the task module's + * width in pixels, or a string, one of: small, medium, large. + */ + width?: any; + /** + * @member {string} [url] The URL of what is loaded as an iframe inside the + * task module. One of url or card is required. + */ + url?: string; + /** + * @member {Attachment} [card] The JSON for the Adaptive card to appear in + * the task module. + */ + card?: Attachment; + /** + * @member {string} [fallbackUrl] If a client does not support the task + * module feature, this URL is opened in a browser tab. + */ + fallbackUrl?: string; + /** + * @member {string} [completionBotId] If a client does not support the task + * module feature, this URL is opened in a browser tab. + */ + completionBotId?: string; +} + +/** + * @interface + * An interface representing TaskModuleContinueResponse. + * Task Module Response with continue action. + * + * @extends TaskModuleResponseBase + */ +export interface TaskModuleContinueResponse extends TaskModuleResponseBase { + /** + * @member {TaskModuleTaskInfo} [value] The JSON for the Adaptive card to + * appear in the task module. + */ + value?: TaskModuleTaskInfo; +} + +/** + * @interface + * An interface representing TaskModuleMessageResponse. + * Task Module response with message action. + * + * @extends TaskModuleResponseBase + */ +export interface TaskModuleMessageResponse extends TaskModuleResponseBase { + /** + * @member {string} [value] Teams will display the value of value in a popup + * message box. + */ + value?: string; +} + +/** + * @interface + * An interface representing TaskModuleResponse. + * Envelope for Task Module Response. + * + */ +export interface TaskModuleResponse { + /** + * @member {TaskModuleResponseBase} [task] The JSON for the Adaptive card to + * appear in the task module. + */ + task?: TaskModuleResponseBase; +} + +/** + * @interface + * An interface representing TaskModuleRequestContext. + * Current user context, i.e., the current theme + * + */ +export interface TaskModuleRequestContext { + /** + * @member {string} [theme] + */ + theme?: string; +} + +/** + * @interface + * An interface representing AppBasedLinkQuery. + * Invoke request body type for app-based link query. + * + */ +export interface AppBasedLinkQuery { + /** + * @member {string} [url] Url queried by user + */ + url?: string; +} + +/** + * Defines values for Type. + * Possible values include: 'ViewAction', 'OpenUri', 'HttpPOST', 'ActionCard' + * @readonly + * @enum {string} + */ +export type Type = 'ViewAction' | 'OpenUri' | 'HttpPOST' | 'ActionCard'; + +/** + * Defines values for ActivityImageType. + * Possible values include: 'avatar', 'article' + * @readonly + * @enum {string} + */ +export type ActivityImageType = 'avatar' | 'article'; + +/** + * Defines values for Os. + * Possible values include: 'default', 'iOS', 'android', 'windows' + * @readonly + * @enum {string} + */ +export type Os = 'default' | 'iOS' | 'android' | 'windows'; + +/** + * Defines values for Type1. + * Possible values include: 'textInput', 'dateInput', 'multichoiceInput' + * @readonly + * @enum {string} + */ +export type Type1 = 'textInput' | 'dateInput' | 'multichoiceInput'; + +/** + * Defines values for Style. + * Possible values include: 'compact', 'expanded' + * @readonly + * @enum {string} + */ +export type Style = 'compact' | 'expanded'; + +/** + * Defines values for UserIdentityType. + * Possible values include: 'aadUser', 'onPremiseAadUser', 'anonymousGuest', 'federatedUser' + * @readonly + * @enum {string} + */ +export type UserIdentityType = 'aadUser' | 'onPremiseAadUser' | 'anonymousGuest' | 'federatedUser'; + +/** + * Defines values for ApplicationIdentityType. + * Possible values include: 'aadApplication', 'bot', 'tenantBot', 'office365Connector', 'webhook' + * @readonly + * @enum {string} + */ +export type ApplicationIdentityType = 'aadApplication' | 'bot' | 'tenantBot' | 'office365Connector' | 'webhook'; + +/** + * Defines values for ConversationIdentityType. + * Possible values include: 'team', 'channel' + * @readonly + * @enum {string} + */ +export type ConversationIdentityType = 'team' | 'channel'; + +/** + * Defines values for ContentType. + * Possible values include: 'html', 'text' + * @readonly + * @enum {string} + */ +export type ContentType = 'html' | 'text'; + +/** + * Defines values for ReactionType. + * Possible values include: 'like', 'heart', 'laugh', 'surprised', 'sad', 'angry' + * @readonly + * @enum {string} + */ +export type ReactionType = 'like' | 'heart' | 'laugh' | 'surprised' | 'sad' | 'angry'; + +/** + * Defines values for MessageType. + * Possible values include: 'message' + * @readonly + * @enum {string} + */ +export type MessageType = 'message'; + +/** + * Defines values for Importance. + * Possible values include: 'normal', 'high', 'urgent' + * @readonly + * @enum {string} + */ +export type Importance = 'normal' | 'high' | 'urgent'; + +/** + * Defines values for CommandContext. + * Possible values include: 'message', 'compose', 'commandbox' + * @readonly + * @enum {string} + */ +export type CommandContext = 'message' | 'compose' | 'commandbox'; + +/** + * Defines values for BotMessagePreviewAction. + * Possible values include: 'edit', 'send' + * @readonly + * @enum {string} + */ +export type BotMessagePreviewAction = 'edit' | 'send'; + +/** + * Defines values for Type2. + * Possible values include: 'message', 'continue' + * @readonly + * @enum {string} + */ +export type Type2 = 'message' | 'continue'; + +/** + * Defines values for AttachmentLayout. + * Possible values include: 'list', 'grid' + * @readonly + * @enum {string} + */ +export type AttachmentLayout = 'list' | 'grid'; + +/** + * Defines values for Type3. + * Possible values include: 'result', 'auth', 'config', 'message', 'botMessagePreview' + * @readonly + * @enum {string} + */ +export type Type3 = 'result' | 'auth' | 'config' | 'message' | 'botMessagePreview'; + +/** + * Defines values for Action. + * Possible values include: 'accept', 'decline' + * @readonly + * @enum {string} + */ +export type Action = 'accept' | 'decline'; + diff --git a/libraries/botframework-schema/tsconfig.json b/libraries/botframework-schema/tsconfig.json index 3788218c0b..f7ed49127c 100644 --- a/libraries/botframework-schema/tsconfig.json +++ b/libraries/botframework-schema/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "ESNext", + "target": "es6", + "lib": ["es2015"], "module": "commonjs", "declaration": true, "sourceMap": true, diff --git a/libraries/botframework-streaming/.gitignore b/libraries/botframework-streaming/.gitignore new file mode 100644 index 0000000000..392f0b0976 --- /dev/null +++ b/libraries/botframework-streaming/.gitignore @@ -0,0 +1,4 @@ +/lib +/node_modules +*.js.map +/coverage/** diff --git a/libraries/botframework-streaming/.nycrc b/libraries/botframework-streaming/.nycrc new file mode 100644 index 0000000000..9f5ddca271 --- /dev/null +++ b/libraries/botframework-streaming/.nycrc @@ -0,0 +1,19 @@ +{ + "extension": [ + ".js" + ], + "include": [ + "lib/**/*.js" + ], + "exclude": [ + "**/node_modules/**", + "**/tests/**", + "**/coverage/**", + "**/*.d.ts" + ], + "reporter": [ + "html" + ], + "all": true, + "cache": true +} \ No newline at end of file diff --git a/libraries/botframework-streaming/README.md b/libraries/botframework-streaming/README.md new file mode 100644 index 0000000000..01c561faaf --- /dev/null +++ b/libraries/botframework-streaming/README.md @@ -0,0 +1,25 @@ +This library contains the core of Bot Framework Streaming Extensions, which extends the 3.0 Bot Framework protocol to communicate over multiplexed, persistent, connections such as named pipes or WebSocket. + +- [Installing](#installing) +- [Documentation](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) +- [GitHub Repo](https://github.com/Microsoft/botbuilder-js) +- [Report Issues](https://github.com/Microsoft/botbuilder-js/issues) + +## Installing +To add the latest published version of this package to your bot: + +```bash +npm install --save botframework-streaming +``` + +#### Use the Daily Build + +To get access to the daily builds of this library, configure npm to use the MyGet feed before installing. + +```bash +npm config set registry https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ +``` + +To reset the registry in order to get the latest published version, run: +```bash +npm config set registry https://registry.npmjs.org/ \ No newline at end of file diff --git a/libraries/botframework-streaming/package.json b/libraries/botframework-streaming/package.json new file mode 100644 index 0000000000..803652f063 --- /dev/null +++ b/libraries/botframework-streaming/package.json @@ -0,0 +1,54 @@ +{ + "name": "botframework-streaming", + "author": "Microsoft Corp.", + "description": "Streaming library for the Microsoft Bot Framework", + "version": "4.1.6", + "license": "MIT", + "keywords": [ + "botbuilder", + "botframework", + "bots", + "chatbots", + "websockets", + "streaming" + ], + "bugs": { + "url": "https://github.com/microsoft/botbuilder-js/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/botbuilder-js.git" + }, + "main": "lib/index.js", + "typings": "lib/index.d.js", + "dependencies": { + "@azure/ms-rest-js": "1.2.6", + "botbuilder": "4.1.6", + "botbuilder-core": "4.1.6", + "uuid": "^3.3.2", + "watershed": "^0.4.0", + "ws": "^7.1.2" + }, + "devDependencies": { + "@types/chai": "^4.1.7", + "@types/node": "^10.12.18", + "@types/ws": "^6.0.3", + "chai": "^4.2.0", + "mocha": "^6.2.0", + "nyc": "^14.1.1", + "sinon": "^7.4.1", + "ts-node": "^4.1.0", + "tslint": "^5.16.0", + "tslint-microsoft-contrib": "^5.2.1" + }, + "scripts": { + "test": "tsc && nyc mocha tests/", + "build": "tsc", + "clean": "erase /q /s .\\lib", + "set-version": "npm version --allow-same-version ${Version}" + }, + "files": [ + "/lib", + "/src" + ] +} diff --git a/libraries/botframework-streaming/src/adapters/index.ts b/libraries/botframework-streaming/src/adapters/index.ts new file mode 100644 index 0000000000..a1913a5c47 --- /dev/null +++ b/libraries/botframework-streaming/src/adapters/index.ts @@ -0,0 +1,11 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './streamingAdapter'; +export * from './streamingHttpClient'; +export * from './tokenResolver'; diff --git a/libraries/botframework-streaming/src/adapters/streamingAdapter.ts b/libraries/botframework-streaming/src/adapters/streamingAdapter.ts new file mode 100644 index 0000000000..3caf7b58ed --- /dev/null +++ b/libraries/botframework-streaming/src/adapters/streamingAdapter.ts @@ -0,0 +1,656 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + BotFrameworkAdapter, + BotFrameworkAdapterSettings, + InvokeResponse, + INVOKE_RESPONSE_KEY, + StatusCodes, + WebRequest, + WebResponse +} from 'botbuilder'; +import { + Activity, ActivityTypes, BotCallbackHandlerKey, + IUserTokenProvider, ResourceResponse, TurnContext } from 'botbuilder-core'; +import { AuthenticationConstants, ChannelValidation, ConnectorClient, GovernmentConstants, GovernmentChannelValidation, JwtTokenValidation, MicrosoftAppCredentials, SimpleCredentialProvider } from 'botbuilder/node_modules/botframework-connector'; +import { IncomingMessage } from 'http'; +import * as os from 'os'; + +import { StreamingHttpClient } from './streamingHttpClient'; +import { TokenResolver } from './tokenResolver'; + +import { IReceiveRequest, ISocket, IStreamingTransportServer } from '../interfaces'; +import { NamedPipeServer } from '../namedPipe'; +import { StreamingResponse } from '../streamingResponse'; +import { NodeWebSocketFactory, NodeWebSocketFactoryBase, WebSocketServer } from '../webSocket'; + +// Retrieve additional information, i.e., host operating system, host OS release, architecture, Node.js version +const ARCHITECTURE: any = os.arch(); +const TYPE: any = os.type(); +const RELEASE: any = os.release(); +const NODE_VERSION: any = process.version; + +// tslint:disable-next-line:no-var-requires no-require-imports +const pjson: any = require('../../package.json'); +const USER_AGENT: string = `Microsoft-BotFramework/3.1 BotBuilder/${ pjson.version } ` + + `(Node.js,Version=${ NODE_VERSION }; ${ TYPE } ${ RELEASE }; ${ ARCHITECTURE })`; + +const defaultPipeName = 'bfv4.pipes'; +const VERSION_PATH: string = '/api/version'; +const MESSAGES_PATH: string = '/api/messages'; +const GET: string = 'GET'; +const POST: string = 'POST'; + + +export interface StreamingAdapterSettings extends BotFrameworkAdapterSettings { + /** + * Optional. The option to determine if this adapter accepts WebSocket connections + */ + enableWebSockets?: boolean; + + /** + * Optional. Used to pass in a NodeWebSocketFactoryBase instance. Allows bot to accept WebSocket connections. + */ + webSocketFactory?: NodeWebSocketFactoryBase; +} + +export class StreamingAdapter extends BotFrameworkAdapter implements IUserTokenProvider { + protected readonly credentials: MicrosoftAppCredentials; + protected readonly credentialsProvider: SimpleCredentialProvider; + protected readonly settings: StreamingAdapterSettings; + + private logic: (context: TurnContext) => Promise; + private streamingServer: IStreamingTransportServer; + private _isEmulatingOAuthCards: boolean; + private webSocketFactory: NodeWebSocketFactoryBase; + + /** + * Creates a new instance of the [BotFrameworkAdapter](xref:botbuilder.BotFrameworkAdapter) class. + * + * @param settings Optional. The settings to use for this adapter instance. + * + * @remarks + * If the `settings` parameter does not include + * [channelService](xref:botbuilder.BotFrameworkAdapterSettings.channelService) or + * [openIdMetadata](xref:botbuilder.BotFrameworkAdapterSettings.openIdMetadata) values, the + * constructor checks the process' environment variables for these values. These values may be + * set when a bot is provisioned on Azure and if so are required for the bot to work properly + * in the global cloud or in a national cloud. + * + * The [BotFrameworkAdapterSettings](xref:botbuilder.BotFrameworkAdapterSettings) class defines + * the available adapter settings. + */ + constructor(settings?: Partial) { + super(settings); + + this._isEmulatingOAuthCards = false; + + // If the developer wants to use WebSockets, but didn't provide a WebSocketFactory, + // create a NodeWebSocketFactory. + if (this.settings.enableWebSockets && !this.settings.webSocketFactory) { + this.webSocketFactory = new NodeWebSocketFactory(); + } + + if (this.settings.webSocketFactory) { + this.webSocketFactory = this.settings.webSocketFactory; + } + + // Relocate the tenantId field used by MS Teams to a new location (from channelData to conversation) + // This will only occur on activities from teams that include tenant info in channelData but NOT in conversation, + // thus should be future friendly. However, once the the transition is complete. we can remove this. + this.use(async(context, next) => { + if (context.activity.channelId === 'msteams' && context.activity && context.activity.conversation && !context.activity.conversation.tenantId && context.activity.channelData && context.activity.channelData.tenant) { + context.activity.conversation.tenantId = context.activity.channelData.tenant.id; + } + await next(); + }); + + } + + /** + * Asynchronously creates a turn context and runs the middleware pipeline for an incoming activity. + * + * @param req An Express or Restify style request object. + * @param res An Express or Restify style response object. + * @param logic The function to call at the end of the middleware pipeline. + * + * @remarks + * This is the main way a bot receives incoming messages and defines a turn in the conversation. This method: + * + * 1. Parses and authenticates an incoming request. + * - The activity is read from the body of the incoming request. An error will be returned + * if the activity can't be parsed. + * - The identity of the sender is authenticated as either the Emulator or a valid Microsoft + * server, using the bot's `appId` and `appPassword`. The request is rejected if the sender's + * identity is not verified. + * 1. Creates a [TurnContext](xref:botbuilder-core.TurnContext) object for the received activity. + * - This object is wrapped with a [revocable proxy](https://www.ecma-international.org/ecma-262/6.0/#sec-proxy.revocable). + * - When this method completes, the proxy is revoked. + * 1. Sends the turn context through the adapter's middleware pipeline. + * 1. Sends the turn context to the `logic` function. + * - The bot may perform additional routing or processing at this time. + * Returning a promise (or providing an `async` handler) will cause the adapter to wait for any asynchronous operations to complete. + * - After the `logic` function completes, the promise chain set up by the middleware is resolved. + * + * > [!TIP] + * > If you see the error `TypeError: Cannot perform 'set' on a proxy that has been revoked` + * > in your bot's console output, the likely cause is that an async function was used + * > without using the `await` keyword. Make sure all async functions use await! + * + * Middleware can _short circuit_ a turn. When this happens, subsequent middleware and the + * `logic` function is not called; however, all middleware prior to this point still run to completion. + * For more information about the middleware pipeline, see the + * [how bots work](https://docs.microsoft.com/azure/bot-service/bot-builder-basics) and + * [middleware](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-middleware) articles. + * Use the adapter's [use](xref:botbuilder-core.BotAdapter.use) method to add middleware to the adapter. + * + * For example: + * ```JavaScript + * server.post('/api/messages', (req, res) => { + * // Route received request to adapter for processing + * adapter.processActivity(req, res, async (context) => { + * // Process any messages received + * if (context.activity.type === ActivityTypes.Message) { + * await context.sendActivity(`Hello World`); + * } + * }); + * }); + * ``` + */ + public async processActivity(req: WebRequest, res: WebResponse, logic: (context: TurnContext) => Promise): Promise { + if (this.settings.enableWebSockets && req.method === GET && (req.headers.Upgrade || req.headers.upgrade)) { + return this.useWebSocket(req, res, logic); + } + + let body: any; + let status: number; + let processError: Error; + try { + // Parse body of request + status = 400; + const request = await parseRequest(req); + + // Authenticate the incoming request + status = 401; + const authHeader: string = req.headers.authorization || req.headers.Authorization || ''; + await this.authenticateRequest(request, authHeader); + + // Process received activity + status = 500; + const context: TurnContext = this.createContext(request); + context.turnState.set(BotCallbackHandlerKey, logic); + await this.runMiddleware(context, logic); + + // Retrieve cached invoke response. + if (request.type === ActivityTypes.Invoke) { + const invokeResponse: any = context.turnState.get(INVOKE_RESPONSE_KEY); + if (invokeResponse && invokeResponse.value) { + const value: InvokeResponse = invokeResponse.value; + status = value.status; + body = value.body; + } else { + status = 501; + } + } else { + status = 200; + } + } catch (err) { + // Catch the error to try and throw the stacktrace out of processActivity() + processError = err; + body = err.toString(); + } + + // Return status + res.status(status); + if (body) { res.send(body); } + res.end(); + + // Check for an error + if (status >= 400) { + if (processError && (processError as Error).stack) { + throw new Error(`BotFrameworkAdapter.processActivity(): ${ status } ERROR\n ${ processError.stack }`); + } else { + throw new Error(`BotFrameworkAdapter.processActivity(): ${ status } ERROR`); + } + } + } + + /** + * Asynchronously creates a turn context and runs the middleware pipeline for an incoming activity. + * + * @param activity The activity to process. + * @param logic The function to call at the end of the middleware pipeline. + * + * @remarks + * This is the main way a bot receives incoming messages and defines a turn in the conversation. This method: + * + * 1. Creates a [TurnContext](xref:botbuilder-core.TurnContext) object for the received activity. + * - This object is wrapped with a [revocable proxy](https://www.ecma-international.org/ecma-262/6.0/#sec-proxy.revocable). + * - When this method completes, the proxy is revoked. + * 1. Sends the turn context through the adapter's middleware pipeline. + * 1. Sends the turn context to the `logic` function. + * - The bot may perform additional routing or processing at this time. + * Returning a promise (or providing an `async` handler) will cause the adapter to wait for any asynchronous operations to complete. + * - After the `logic` function completes, the promise chain set up by the middleware is resolved. + * + * Middleware can _short circuit_ a turn. When this happens, subsequent middleware and the + * `logic` function is not called; however, all middleware prior to this point still run to completion. + * For more information about the middleware pipeline, see the + * [how bots work](https://docs.microsoft.com/azure/bot-service/bot-builder-basics) and + * [middleware](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-middleware) articles. + * Use the adapter's [use](xref:botbuilder-core.BotAdapter.use) method to add middleware to the adapter. + */ + public async processActivityDirect(activity: Activity, logic: (context: TurnContext) => Promise): Promise { + let processError: Error; + try { + // Process activity + const context: TurnContext = this.createContext(activity); + context.turnState.set(BotCallbackHandlerKey, logic); + await this.runMiddleware(context, logic); + } catch (err) { + // Catch the error to try and throw the stacktrace out of processActivity() + processError = err; + } + + if (processError) { + if (processError && (processError as Error).stack) { + throw new Error(`BotFrameworkAdapter.processActivity(): ${ status } ERROR\n ${ processError.stack }`); + } else { + throw new Error(`BotFrameworkAdapter.processActivity(): ${ status } ERROR`); + } + } + } + + /** + * Asynchronously sends a set of outgoing activities to a channel server. + * + * This method supports the framework and is not intended to be called directly for your code. + * Use the turn context's [sendActivity](xref:botbuilder-core.TurnContext.sendActivity) or + * [sendActivities](xref:botbuilder-core.TurnContext.sendActivities) method from your bot code. + * + * @param context The context object for the turn. + * @param activities The activities to send. + * + * @returns An array of [ResourceResponse](xref:) + * + * @remarks + * The activities will be sent one after another in the order in which they're received. A + * response object will be returned for each sent activity. For `message` activities this will + * contain the ID of the delivered message. + */ + public async sendActivities(context: TurnContext, activities: Partial[]): Promise { + const responses: ResourceResponse[] = []; + for (let i = 0; i < activities.length; i++) { + const activity: Partial = activities[i]; + switch (activity.type) { + case 'delay': + await delay(typeof activity.value === 'number' ? activity.value : 1000); + responses.push({} as ResourceResponse); + break; + case 'invokeResponse': + // Cache response to context object. This will be retrieved when turn completes. + context.turnState.set(INVOKE_RESPONSE_KEY, activity); + responses.push({} as ResourceResponse); + break; + default: + if (!activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.sendActivity(): missing serviceUrl.`); } + if (!activity.conversation || !activity.conversation.id) { + throw new Error(`BotFrameworkAdapter.sendActivity(): missing conversation id.`); + } + if (StreamingAdapter.isFromStreamingConnection(activity as Activity)) { + TokenResolver.checkForOAuthCards(this, context, activity as Activity); + } + const client: ConnectorClient = this.createConnectorClient(activity.serviceUrl); + if (activity.type === 'trace' && activity.channelId !== 'emulator') { + // Just eat activity + responses.push({} as ResourceResponse); + } else if (activity.replyToId) { + responses.push(await client.conversations.replyToActivity( + activity.conversation.id, + activity.replyToId, + activity as Activity + )); + } else { + responses.push(await client.conversations.sendToConversation( + activity.conversation.id, + activity as Activity + )); + } + break; + } + } + return responses; + } + + /** + * Creates a connector client. + * + * @param serviceUrl The client's service URL. + * + * @remarks + * Override this in a derived class to create a mock connector client for unit testing. + */ + public createConnectorClient(serviceUrl: string): ConnectorClient { + + if (StreamingAdapter.isStreamingServiceUrl(serviceUrl)) { + + // Check if we have a streaming server. Otherwise, requesting a connector client + // for a non-existent streaming connection results in an error + if (!this.streamingServer) { + throw new Error(`Cannot create streaming connector client for serviceUrl ${serviceUrl} without a streaming connection. Call 'useWebSocket' or 'useNamedPipe' to start a streaming connection.`) + } + + return new ConnectorClient( + this.credentials, + { + baseUri: serviceUrl, + userAgent: USER_AGENT, + httpClient: new StreamingHttpClient(this.streamingServer) + }); + } + + const client: ConnectorClient = new ConnectorClient(this.credentials, { baseUri: serviceUrl, userAgent: USER_AGENT} ); + return client; + } + + /** + * Checks the validity of the request and attempts to map it the correct virtual endpoint, + * then generates and returns a response if appropriate. + * @param request A ReceiveRequest from the connected channel. + * @returns A response created by the BotAdapter to be sent to the client that originated the request. + */ + public async processRequest(request: IReceiveRequest): Promise { + let response = new StreamingResponse(); + + if (!request) { + response.statusCode = StatusCodes.BAD_REQUEST; + response.setBody(`No request provided.`); + return response; + } + + if (!request.verb || !request.path) { + response.statusCode = StatusCodes.BAD_REQUEST; + response.setBody(`Request missing verb and/or path. Verb: ${ request.verb }. Path: ${ request.path }`); + return response; + } + + if (request.verb.toLocaleUpperCase() !== POST && request.verb.toLocaleUpperCase() !== GET) { + response.statusCode = StatusCodes.METHOD_NOT_ALLOWED; + response.setBody(`Invalid verb received. Only GET and POST are accepted. Verb: ${ request.verb }`); + } + + if (request.path.toLocaleLowerCase() === VERSION_PATH) { + return await this.handleVersionRequest(request, response); + } + + // Convert the StreamingRequest into an activity the Adapter can understand. + let body: Activity; + try { + body = await this.readRequestBodyAsString(request); + + } catch (error) { + response.statusCode = StatusCodes.BAD_REQUEST; + response.setBody(`Request body missing or malformed: ${ error }`); + return response; + } + + if (request.path.toLocaleLowerCase() !== MESSAGES_PATH) { + response.statusCode = StatusCodes.NOT_FOUND; + response.setBody(`Path ${ request.path.toLocaleLowerCase() } not not found. Expected ${ MESSAGES_PATH }}.`); + return response; + } + + if (request.verb.toLocaleUpperCase() !== POST) { + response.statusCode = StatusCodes.METHOD_NOT_ALLOWED; + response.setBody(`Invalid verb received for ${ request.verb.toLocaleLowerCase() }. Only GET and POST are accepted. Verb: ${ request.verb }`); + return response; + } + + try { + let context = new TurnContext(this, body); + await this.runMiddleware(context, this.logic); + + if (body.type === ActivityTypes.Invoke) { + let invokeResponse: any = context.turnState.get(INVOKE_RESPONSE_KEY); + + if (invokeResponse && invokeResponse.value) { + const value: InvokeResponse = invokeResponse.value; + response.statusCode = value.status; + response.setBody(value.body); + } else { + response.statusCode = StatusCodes.NOT_IMPLEMENTED; + } + } else { + response.statusCode = StatusCodes.OK; + } + } catch (error) { + response.statusCode = StatusCodes.INTERNAL_SERVER_ERROR; + response.setBody(error); + return response; + } + + return response; + } + + private async handleVersionRequest(request: IReceiveRequest, response: StreamingResponse): Promise { + if (request.verb.toLocaleUpperCase() === GET) { + response.statusCode = StatusCodes.OK; + + if (!this.credentials.appId) { + response.setBody({ UserAgent: USER_AGENT }); + return response; + } + + let token = ''; + try { + token = await this.credentials.getToken(); + + } catch (err) { + /** + * In reality a missing BotToken will cause the channel to close the connection, + * but we still send the response and allow the channel to make that decision + * instead of proactively disconnecting. This allows the channel to know why + * the connection has been closed and make the choice not to make endless reconnection + * attempts that will end up right back here. + */ + console.error(err.message); + } + response.setBody({ UserAgent: USER_AGENT, BotToken: token }); + + } else { + response.statusCode = StatusCodes.METHOD_NOT_ALLOWED; + response.setBody(`Invalid verb received for path: ${ request.path }. Only GET is accepted. Verb: ${ request.verb }`); + } + + return response; + } + + /** + * Allows for the overriding of authentication in unit tests. + * @param request Received request. + * @param authHeader Received authentication header. + */ + protected async authenticateRequest(request: Partial, authHeader: string): Promise { + const claims = await JwtTokenValidation.authenticateRequest( + request as Activity, authHeader, + this.credentialsProvider, + this.settings.channelService + ); + if (!claims.isAuthenticated) { throw new Error('Unauthorized Access. Request is not authorized'); } + } + + /** + * Checks the environment and can set a flag to emulate OAuth cards. + * + * @param context The context object for the turn. + * + * @remarks + * Override this in a derived class to control how OAuth cards are emulated for unit testing. + */ + protected checkEmulatingOAuthCards(context: TurnContext): void { + if (!this._isEmulatingOAuthCards && + context.activity.channelId === 'emulator' && + (!this.credentials.appId)) { + this._isEmulatingOAuthCards = true; + } + } + + /** + * Determine if the Activity was sent via an Http/Https connection or Streaming + * This can be determined by looking at the ServiceUrl property: + * (1) All channels that send messages via http/https are not streaming + * (2) Channels that send messages via streaming have a ServiceUrl that does not begin with http/https. + * @param activity the activity. + */ + private static isFromStreamingConnection(activity: Activity): boolean { + return activity && this.isStreamingServiceUrl(activity.serviceUrl); + } + + /** + * Determine if the serviceUrl was sent via an Http/Https connection or Streaming + * This can be determined by looking at the ServiceUrl property: + * (1) All channels that send messages via http/https are not streaming + * (2) Channels that send messages via streaming have a ServiceUrl that does not begin with http/https. + * @param serviceUrl the serviceUrl provided in the resquest. + */ + private static isStreamingServiceUrl(serviceUrl: string): boolean { + return serviceUrl && !serviceUrl.toLowerCase().startsWith('http'); + } + + private async authenticateConnection(req: WebRequest, channelService?: string): Promise { + if (!this.credentials.appId) { + // auth is disabled + return; + } + + const authHeader: string = req.headers.authorization || req.headers.Authorization || ''; + const channelIdHeader: string = req.headers.channelid || req.headers.ChannelId || req.headers.ChannelID || ''; + // Validate the received Upgrade request from the channel. + const claims = await JwtTokenValidation.validateAuthHeader(authHeader, this.credentialsProvider, channelService, channelIdHeader); + + // Add serviceUrl from claim to static cache to trigger token refreshes. + const serviceUrl = claims.getClaimValue(AuthenticationConstants.ServiceUrlClaim); + MicrosoftAppCredentials.trustServiceUrl(serviceUrl); + + if (!claims.isAuthenticated) { throw new Error('Unauthorized Access. Request is not authorized'); } + } + + /** + * Connects the handler to a Named Pipe server and begins listening for incoming requests. + * @param pipeName The name of the named pipe to use when creating the server. + * @param logic The logic that will handle incoming requests. + */ + private async useNamedPipe(pipeName: string = defaultPipeName, logic: (context: TurnContext) => Promise): Promise{ + if (!logic) { + throw new Error('Bot logic needs to be provided to `useNamedPipe`'); + } + + this.logic = logic; + + this.streamingServer = new NamedPipeServer(pipeName, this); + await this.streamingServer.start(); + } + + /** + * Process the initial request to establish a long lived connection via a streaming server. + * @param req The connection request. + * @param res The response sent on error or connection termination. + * @param logic The logic that will handle incoming requests. + */ + private async useWebSocket(req: WebRequest, res: WebResponse, logic: (context: TurnContext) => Promise): Promise { + if (!logic) { + throw new Error('Streaming logic needs to be provided to `useWebSocket`'); + } + + if (!this.webSocketFactory || !this.webSocketFactory.createWebSocket) { + throw new Error('BotFrameworkAdapter must have a WebSocketFactory in order to support streaming.'); + } + + this.logic = logic; + + // Restify-specific check. + if (typeof((res as any).claimUpgrade) !== 'function') { + throw new Error('ClaimUpgrade is required for creating WebSocket connection.'); + } + + try { + await this.authenticateConnection(req, this.settings.channelService); + } catch (err) { + // Set the correct status code for the socket to send back to the channel. + res.status(StatusCodes.UNAUTHORIZED); + res.send(err.message); + // Re-throw the error so the developer will know what occurred. + throw err; + } + + const upgrade = (res as any).claimUpgrade(); + const socket = this.webSocketFactory.createWebSocket(req as IncomingMessage, upgrade.socket, upgrade.head); + + await this.startWebSocket(socket); + } + + /** + * Connects the handler to a WebSocket server and begins listening for incoming requests. + * @param socket The socket to use when creating the server. + */ + private async startWebSocket(socket: ISocket): Promise{ + this.streamingServer = new WebSocketServer(socket, this); + await this.streamingServer.start(); + } + + private async readRequestBodyAsString(request: IReceiveRequest): Promise { + const contentStream = request.streams[0]; + return await contentStream.readAsJson(); + } +} + +/** + * Handles incoming webhooks from the botframework + * @private + * @param req incoming web request + */ +function parseRequest(req: WebRequest): Promise { + return new Promise((resolve: any, reject: any): void => { + function returnActivity(activity: Activity): void { + if (typeof activity !== 'object') { throw new Error(`BotFrameworkAdapter.parseRequest(): invalid request body.`); } + if (typeof activity.type !== 'string') { throw new Error(`BotFrameworkAdapter.parseRequest(): missing activity type.`); } + if (typeof activity.timestamp === 'string') { activity.timestamp = new Date(activity.timestamp); } + if (typeof activity.localTimestamp === 'string') { activity.localTimestamp = new Date(activity.localTimestamp); } + if (typeof activity.expiration === 'string') { activity.expiration = new Date(activity.expiration); } + resolve(activity); + } + + if (req.body) { + try { + returnActivity(req.body); + } catch (err) { + reject(err); + } + } else { + let requestData = ''; + req.on('data', (chunk: string) => { + requestData += chunk; + }); + req.on('end', () => { + try { + req.body = JSON.parse(requestData); + returnActivity(req.body); + } catch (err) { + reject(err); + } + }); + } + }); +} + +function delay(timeout: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, timeout); + }); +} \ No newline at end of file diff --git a/libraries/botframework-streaming/src/adapters/streamingHttpClient.ts b/libraries/botframework-streaming/src/adapters/streamingHttpClient.ts new file mode 100644 index 0000000000..1f7847fd02 --- /dev/null +++ b/libraries/botframework-streaming/src/adapters/streamingHttpClient.ts @@ -0,0 +1,55 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { WebResource, HttpOperationResponse, HttpClient } from '@azure/ms-rest-js'; + +import { IStreamingTransportServer } from '../interfaces'; +import { StreamingRequest } from '../streamingRequest'; + +export class StreamingHttpClient implements HttpClient { + private readonly server: IStreamingTransportServer; + + /** + * Creates a new streaming Http client. + * + * @param server Transport server implementation to be used. + */ + public constructor(server: IStreamingTransportServer) { + if (!server) { + throw new Error(`StreamingHttpClient: Expected server.`); + } + this.server = server; + } + + /** + * This function hides the default sendRequest of the HttpClient, replacing it + * with a version that takes the WebResource created by the BotFrameworkAdapter + * and converting it to a form that can be sent over a streaming transport. + * + * @param httpRequest The outgoing request created by the BotframeworkAdapter. + * @return The streaming transport compatible response to send back to the client. + */ + public async sendRequest(httpRequest: WebResource): Promise { + if (!httpRequest) { + throw new Error('SendRequest invalid parameter: httpRequest should be provided'); + } + const request = this.mapHttpRequestToProtocolRequest(httpRequest); + request.path = request.path.substring(request.path.indexOf('/v3')); + const res = await this.server.send(request); + return { + request: httpRequest, + status: res.statusCode, + headers: httpRequest.headers, + readableStreamBody: res.streams.length > 0 ? res.streams[0].getStream() : undefined + }; + } + + private mapHttpRequestToProtocolRequest(httpRequest: WebResource): StreamingRequest { + return StreamingRequest.create(httpRequest.method, httpRequest.url, httpRequest.body); + } +} diff --git a/libraries/botframework-streaming/src/adapters/tokenResolver.ts b/libraries/botframework-streaming/src/adapters/tokenResolver.ts new file mode 100644 index 0000000000..4f5546706f --- /dev/null +++ b/libraries/botframework-streaming/src/adapters/tokenResolver.ts @@ -0,0 +1,125 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { BotFrameworkAdapter } from 'botbuilder'; +import { + Activity, + ActivityTypes, + BotCallbackHandlerKey, + CardFactory, + ConversationReference, + OAuthCard, + OAuthLoginTimeoutMsValue, + TokenResponse, + TokenPollingSettings, + TokenPollingSettingsKey, + TurnContext, +} from 'botbuilder-core'; + +/** + * Looks for OAuthCards in Activity attachments and takes action on them + */ +export class TokenResolver { + private static readonly PollingIntervalMs: number = 1000; + + public static checkForOAuthCards(adapter: BotFrameworkAdapter, context: TurnContext, activity: Activity, log?: string[]) { + if (!activity || !activity.attachments) { + return; + } + + for (const attachment of activity.attachments) { + if (attachment.contentType == CardFactory.contentTypes.oauthCard) { + const oauthCard = attachment.content; + if (!oauthCard.connectionName) { + throw new Error(`The OAuthPrompt's ConnectionName property is missing a value.`); + } + + let pollingTimeoutMs = context.turnState.get(TokenPollingSettingsKey); + + if (!pollingTimeoutMs) { + pollingTimeoutMs = OAuthLoginTimeoutMsValue; + } + + let pollingTimeout: Date = new Date(); + pollingTimeout.setMilliseconds(pollingTimeout.getMilliseconds() + pollingTimeoutMs); + + setTimeout(() => this.pollForToken(adapter, context, activity, oauthCard.connectionName, pollingTimeout, log), TokenResolver.PollingIntervalMs); + } + } + } + + private static pollForToken(adapter: BotFrameworkAdapter, context: TurnContext, activity: Activity, connectionName: string, pollingTimeout: Date, log?: string[]) { + if (pollingTimeout > new Date()) { + adapter.getUserToken(context, connectionName).then((tokenResponse: TokenResponse) => { + let pollingIntervalMs = TokenResolver.PollingIntervalMs; + if (tokenResponse) { + if (tokenResponse.token) { + const logic = context.turnState.get(BotCallbackHandlerKey); + const eventActivity = TokenResolver.createTokenResponseActivity(TurnContext.getConversationReference(activity), tokenResponse.token, connectionName); + // received a token, send it to the bot and end polling + adapter.processActivityDirect(eventActivity, logic).then(() => { + }).catch(reason => { + adapter.onTurnError(context, new Error(reason)).then(() => { }); + }); + if (log) + log.push('Returned token'); + return; + } else if (tokenResponse.properties && tokenResponse.properties[TokenPollingSettingsKey]) { + const pollingSettings = tokenResponse.properties[TokenPollingSettingsKey]; + if (pollingSettings.timeout <= 0) { + // end polling + if (log) + log.push('End polling'); + return; + } + if (pollingSettings.interval > 0) { + // reset the polling interval + if (log) + log.push(`Changing polling interval to ${pollingSettings.interval}`); + pollingIntervalMs = pollingSettings.interval; + } + } + } + if (log) + log.push('Polling again'); + setTimeout(() => this.pollForToken(adapter, context, activity, connectionName, pollingTimeout), pollingIntervalMs); + }); + } + } + + private static createTokenResponseActivity(relatesTo: Partial, token: string, connectionName: string): Partial { + let tokenResponse: Partial = { + id: this.generate_guid(), + timestamp: new Date(), + type: ActivityTypes.Event, + serviceUrl: relatesTo.serviceUrl, + from: relatesTo.user, + recipient: relatesTo.bot, + replyToId: relatesTo.activityId, + channelId: relatesTo.channelId, + conversation: relatesTo.conversation, + name: 'tokens/response', + relatesTo: relatesTo, + value: { + token: token, + connectionName: connectionName + } + }; + return tokenResponse; + } + + private static generate_guid(): string { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + + s4() + '-' + s4() + s4() + s4(); + } +} diff --git a/libraries/botframework-streaming/src/assemblers/index.ts b/libraries/botframework-streaming/src/assemblers/index.ts new file mode 100644 index 0000000000..b7b330aa38 --- /dev/null +++ b/libraries/botframework-streaming/src/assemblers/index.ts @@ -0,0 +1,9 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './payloadAssembler'; diff --git a/libraries/botframework-streaming/src/assemblers/payloadAssembler.ts b/libraries/botframework-streaming/src/assemblers/payloadAssembler.ts new file mode 100644 index 0000000000..8e361badfe --- /dev/null +++ b/libraries/botframework-streaming/src/assemblers/payloadAssembler.ts @@ -0,0 +1,127 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { SubscribableStream } from '../subscribableStream'; +import { StreamManager, PayloadTypes } from '../payloads'; +import { ContentStream } from '../contentStream'; +import { IAssemblerParams } from '../interfaces/IAssemblerParams'; +import { IHeader } from '../interfaces/IHeader'; +import { IResponsePayload } from '../interfaces/IResponsePayload'; +import { IReceiveResponse, IReceiveRequest } from '../interfaces'; +import { IRequestPayload } from '../interfaces/IRequestPayload'; + +/** + * Assembles payloads for streaming library. + */ +export class PayloadAssembler { + public id: string; + public end: boolean; + public contentLength: number; + public payloadType: string; + private stream: SubscribableStream; + private readonly _onCompleted: Function; + private readonly _streamManager: StreamManager; + private readonly _byteOrderMark = 0xFEFF; + private readonly _utf: string = 'utf8'; + + public constructor(streamManager: StreamManager, params: IAssemblerParams) { + if(params.header){ + this.id = params.header.id; + this.payloadType = params.header.payloadType; + this.contentLength = params.header.payloadLength; + this.end = params.header.end; + } else { + this.id = params.id; + } + + if(!this.id){ + throw Error('An ID must be supplied when creating an assembler.'); + } + + this._streamManager = streamManager; + this._onCompleted = params.onCompleted; + } + + public getPayloadStream(): SubscribableStream { + if (!this.stream) { + this.stream = this.createPayloadStream(); + } + + return this.stream; + } + + public onReceive(header: IHeader, stream: SubscribableStream, contentLength: number): void { + this.end = header.end; + + if (header.payloadType === PayloadTypes.response || header.payloadType === PayloadTypes.request) { + this.process(stream) + .then() + .catch(); + } else if (header.end) { + stream.end(); + } + } + + public close(): void { + this._streamManager.closeStream(this.id); + } + + private createPayloadStream(): SubscribableStream { + return new SubscribableStream(); + } + + private payloadFromJson(json: string): T { + return JSON.parse((json.charCodeAt(0) === this._byteOrderMark) ? json.slice(1) : json) as T; + } + + private stripBOM(input: string): string { + return (input.charCodeAt(0) === this._byteOrderMark) ? input.slice(1) : input; + } + + private async process(stream: SubscribableStream): Promise { + let streamData: Buffer = stream.read(stream.length) as Buffer; + if (!streamData) { + return; + } + + let streamDataAsString = streamData.toString(this._utf); + + if(this.payloadType === PayloadTypes.request){ + await this.processRequest(streamDataAsString); + } else if(this.payloadType === PayloadTypes.response){ + await this.processResponse(streamDataAsString); + } + } + + private async processResponse(streamDataAsString: string): Promise { + + let responsePayload: IResponsePayload = this.payloadFromJson(this.stripBOM(streamDataAsString)); + let receiveResponse: IReceiveResponse = { streams: [], statusCode: responsePayload.statusCode }; + + await this.processStreams(responsePayload, receiveResponse); + } + + private async processRequest(streamDataAsString: string): Promise { + + let requestPayload: IRequestPayload = this.payloadFromJson(streamDataAsString); + let receiveRequest: IReceiveRequest = { streams: [], path: requestPayload.path, verb: requestPayload.verb }; + + await this.processStreams(requestPayload, receiveRequest); + } + + private async processStreams(responsePayload: any, receiveResponse: any) { + if (responsePayload.streams) { + responsePayload.streams.forEach((responseStream): void => { + let contentAssembler: PayloadAssembler = this._streamManager.getPayloadAssembler(responseStream.id); + contentAssembler.payloadType = responseStream.contentType; + contentAssembler.contentLength = responseStream.length; + receiveResponse.streams.push(new ContentStream(responseStream.id, contentAssembler)); + }); + } + await this._onCompleted(this.id, receiveResponse); + } +} diff --git a/libraries/botframework-streaming/src/contentStream.ts b/libraries/botframework-streaming/src/contentStream.ts new file mode 100644 index 0000000000..fa4fabc3fc --- /dev/null +++ b/libraries/botframework-streaming/src/contentStream.ts @@ -0,0 +1,90 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { SubscribableStream } from './subscribableStream'; +import { PayloadAssembler } from './assemblers'; + +export class ContentStream { + public id: string; + private readonly assembler: PayloadAssembler; + private stream: SubscribableStream; + + public constructor(id: string, assembler: PayloadAssembler) { + if (!assembler) { + throw Error('Null Argument Exception'); + } + this.id = id; + this.assembler = assembler; + } + + public get contentType(): string { + return this.assembler.payloadType; + } + + public get length(): number { + return this.assembler.contentLength; + } + + public getStream(): SubscribableStream { + if (!this.stream) { + this.stream = this.assembler.getPayloadStream(); + } + + return this.stream; + } + + public cancel(): void { + this.assembler.close(); + } + + public async readAsString(): Promise { + const { bufferArray } = await this.readAll(); + return (bufferArray || []).map(result => result.toString('utf8')).join(''); + } + + public async readAsJson(): Promise { + let stringToParse = await this.readAsString(); + try { + return JSON.parse(stringToParse); + } catch (error) { + throw error; + } + } + + private async readAll(): Promise> { + // do a read-all + let allData: Buffer[] = []; + let count = 0; + let stream = this.getStream(); + + // populate the array with any existing buffers + while (count < stream.length) { + let chunk = stream.read(stream.length); + allData.push(chunk); + count += (chunk as Buffer).length; + } + + if (count < this.length) { + let readToEnd = new Promise((resolve): void => { + let callback = (cs: ContentStream) => (chunk: any): void => { + allData.push(chunk); + count += (chunk as Buffer).length; + if (count === cs.length) { + resolve(true); + } + }; + + stream.subscribe(callback(this)); + }); + + await readToEnd; + } + + return {bufferArray: allData, size: count}; + } + +} diff --git a/libraries/botframework-streaming/src/disassemblers/cancelDisassembler.ts b/libraries/botframework-streaming/src/disassemblers/cancelDisassembler.ts new file mode 100644 index 0000000000..6cb0853e32 --- /dev/null +++ b/libraries/botframework-streaming/src/disassemblers/cancelDisassembler.ts @@ -0,0 +1,27 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { IHeader } from '../interfaces/IHeader'; +import { PayloadTypes } from '../payloads/payloadTypes'; +import { PayloadSender } from '../payloadTransport/payloadSender'; + +export class CancelDisassembler { + private readonly sender: PayloadSender; + private readonly id: string; + private readonly payloadType: PayloadTypes; + + public constructor(sender: PayloadSender, id: string, payloadType: PayloadTypes) { + this.sender = sender; + this.id = id; + this.payloadType = payloadType; + } + + public disassemble(): void { + const header: IHeader = {payloadType: this.payloadType, payloadLength: 0, id: this.id, end: true}; + this.sender.sendPayload(header); + } +} diff --git a/libraries/botframework-streaming/src/disassemblers/httpContentStreamDisassembler.ts b/libraries/botframework-streaming/src/disassemblers/httpContentStreamDisassembler.ts new file mode 100644 index 0000000000..4087a2d0fc --- /dev/null +++ b/libraries/botframework-streaming/src/disassemblers/httpContentStreamDisassembler.ts @@ -0,0 +1,32 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { HttpContentStream } from '../httpContentStream'; +import { PayloadSender } from '../payloadTransport/payloadSender'; +import { SubscribableStream } from '../subscribableStream'; +import { PayloadTypes } from '../payloads/payloadTypes'; +import { PayloadDisassembler } from './payloadDisassembler'; +import { IStreamWrapper } from '../interfaces/IStreamWrapper'; + +/** + * Disassembler for Http content stream + */ +export class HttpContentStreamDisassembler extends PayloadDisassembler { + public readonly contentStream: HttpContentStream; + public payloadType: PayloadTypes = PayloadTypes.stream; + + public constructor(sender: PayloadSender, contentStream: HttpContentStream) { + super(sender, contentStream.id); + this.contentStream = contentStream; + } + + public async getStream(): Promise { + let stream: SubscribableStream = this.contentStream.content.getStream(); + + return {stream, streamLength: stream.length}; + } +} diff --git a/libraries/botframework-streaming/src/disassemblers/index.ts b/libraries/botframework-streaming/src/disassemblers/index.ts new file mode 100644 index 0000000000..860a66d5bb --- /dev/null +++ b/libraries/botframework-streaming/src/disassemblers/index.ts @@ -0,0 +1,13 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './cancelDisassembler'; +export * from './httpContentStreamDisassembler'; +export * from './payloadDisassembler'; +export * from './requestDisassembler'; +export * from './responseDisassembler'; diff --git a/libraries/botframework-streaming/src/disassemblers/payloadDisassembler.ts b/libraries/botframework-streaming/src/disassemblers/payloadDisassembler.ts new file mode 100644 index 0000000000..13987a751b --- /dev/null +++ b/libraries/botframework-streaming/src/disassemblers/payloadDisassembler.ts @@ -0,0 +1,53 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { IHeader } from '../interfaces/IHeader'; +import { PayloadTypes } from '../payloads/payloadTypes'; +import { PayloadSender } from '../payloadTransport/payloadSender'; +import { SubscribableStream } from '../subscribableStream'; +import { IStreamWrapper } from '../interfaces/IStreamWrapper'; + +/** + * Base class streaming payload disassembling. + */ +export abstract class PayloadDisassembler { + public abstract payloadType: PayloadTypes; + private readonly sender: PayloadSender; + private stream: SubscribableStream; + private streamLength?: number; + private readonly id: string; + + public constructor(sender: PayloadSender, id: string) { + this.sender = sender; + this.id = id; + } + + protected static serialize(item: T): IStreamWrapper { + let stream: SubscribableStream = new SubscribableStream(); + + stream.write(JSON.stringify(item)); + stream.end(); + + return {stream, streamLength: stream.length}; + } + + public abstract async getStream(): Promise; + + public async disassemble(): Promise { + let { stream, streamLength }: IStreamWrapper = await this.getStream(); + + this.stream = stream; + this.streamLength = streamLength; + + return this.send(); + } + + private async send(): Promise { + let header: IHeader = {payloadType: this.payloadType, payloadLength: this.streamLength, id: this.id, end: true}; + this.sender.sendPayload(header, this.stream); + } +} diff --git a/libraries/botframework-streaming/src/disassemblers/requestDisassembler.ts b/libraries/botframework-streaming/src/disassemblers/requestDisassembler.ts new file mode 100644 index 0000000000..445ed02036 --- /dev/null +++ b/libraries/botframework-streaming/src/disassemblers/requestDisassembler.ts @@ -0,0 +1,36 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { PayloadTypes } from '../payloads/payloadTypes'; +import { PayloadSender } from '../payloadTransport/payloadSender'; +import { StreamingRequest } from '../streamingRequest'; +import { PayloadDisassembler } from './payloadDisassembler'; +import { IStreamWrapper } from '../interfaces/IStreamWrapper'; +import { IRequestPayload } from '../interfaces/IRequestPayload'; + +/** + * Streaming request disassembler. + */ +export class RequestDisassembler extends PayloadDisassembler { + public request: StreamingRequest; + public payloadType: PayloadTypes = PayloadTypes.request; + + public constructor(sender: PayloadSender, id: string, request?: StreamingRequest) { + super(sender, id); + this.request = request; + } + + public async getStream(): Promise { + let payload: IRequestPayload = {verb: this.request.verb, path: this.request.path, streams: []}; + if (this.request.streams) { + this.request.streams.forEach(function(stream){ + payload.streams.push(stream.description); + }); + } + return PayloadDisassembler.serialize(payload); + } +} diff --git a/libraries/botframework-streaming/src/disassemblers/responseDisassembler.ts b/libraries/botframework-streaming/src/disassemblers/responseDisassembler.ts new file mode 100644 index 0000000000..37dfa226b1 --- /dev/null +++ b/libraries/botframework-streaming/src/disassemblers/responseDisassembler.ts @@ -0,0 +1,36 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { PayloadTypes } from '../payloads/payloadTypes'; +import { PayloadSender } from '../payloadTransport/payloadSender'; +import { StreamingResponse } from '../streamingResponse'; +import { PayloadDisassembler } from './payloadDisassembler'; +import { IStreamWrapper } from '../interfaces/IStreamWrapper'; +import { IResponsePayload } from '../interfaces/IResponsePayload'; + +/** + * Streaming response disassembler. + */ +export class ResponseDisassembler extends PayloadDisassembler { + public readonly response: StreamingResponse; + public readonly payloadType: PayloadTypes = PayloadTypes.response; + + public constructor(sender: PayloadSender, id: string, response: StreamingResponse) { + super(sender, id); + this.response = response; + } + + public async getStream(): Promise { + let payload: IResponsePayload = {statusCode: this.response.statusCode, streams: []}; + if (this.response.streams) { + this.response.streams.forEach(function(stream){ + payload.streams.push(stream.description); + }); + } + return PayloadDisassembler.serialize(payload); + } +} diff --git a/libraries/botframework-streaming/src/httpContentStream.ts b/libraries/botframework-streaming/src/httpContentStream.ts new file mode 100644 index 0000000000..11cd2a42ad --- /dev/null +++ b/libraries/botframework-streaming/src/httpContentStream.ts @@ -0,0 +1,36 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { SubscribableStream } from './subscribableStream'; +import { generateGuid } from './utilities/protocol-base'; +import { IHttpContentHeaders } from './interfaces/IHttpContentHeaders'; + +export class HttpContentStream { + public readonly id: string; + public readonly content: HttpContent; + public description: { id: string; type: string; length: number; }; + + public constructor(content: HttpContent) { + this.id = generateGuid(); + this.content = content; + this.description = {id: this.id, type: (this.content.headers) ? this.content.headers.type : 'unknown', length: (this.content.headers) ? this.content.headers.contentLength : 0}; + } +} + +export class HttpContent { + public headers: IHttpContentHeaders; + private readonly stream: SubscribableStream; + + public constructor(headers: IHttpContentHeaders, stream: SubscribableStream) { + this.headers = headers; + this.stream = stream; + } + + public getStream(): SubscribableStream { + return this.stream; + } +} diff --git a/libraries/botframework-streaming/src/index.ts b/libraries/botframework-streaming/src/index.ts new file mode 100644 index 0000000000..11f2ca2e9a --- /dev/null +++ b/libraries/botframework-streaming/src/index.ts @@ -0,0 +1,27 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export { StreamingAdapter, StreamingHttpClient, TokenResolver } from './adapters'; +export { ContentStream } from './contentStream'; +export { HttpContent } from './httpContentStream'; +export { IStreamingTransportServer, IStreamingTransportClient, ISocket, IReceiveRequest, IReceiveResponse } from './interfaces'; +export { NamedPipeClient, NamedPipeServer } from './namedPipe'; +export { RequestHandler } from './requestHandler'; +export { StreamingRequest } from './streamingRequest'; +export { StreamingResponse } from './streamingResponse'; +export { SubscribableStream } from './subscribableStream'; +export { + BrowserWebSocket, + NodeWebSocket, + NodeWebSocketFactory, + NodeWebSocketFactoryBase, + WebSocketClient, + WebSocketServer, + WsNodeWebSocket, + WsNodeWebSocketFactory, +} from './webSocket'; diff --git a/libraries/botframework-streaming/src/interfaces/IAssemblerParams.ts b/libraries/botframework-streaming/src/interfaces/IAssemblerParams.ts new file mode 100644 index 0000000000..a2ee36092b --- /dev/null +++ b/libraries/botframework-streaming/src/interfaces/IAssemblerParams.ts @@ -0,0 +1,17 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { IHeader } from './IHeader'; + +/** + * Parameters for a streaming assembler. + */ +export interface IAssemblerParams { + header?: IHeader; + id?: string; + onCompleted?: Function; +} diff --git a/libraries/botframework-streaming/src/interfaces/IHeader.ts b/libraries/botframework-streaming/src/interfaces/IHeader.ts new file mode 100644 index 0000000000..bcc9a77d47 --- /dev/null +++ b/libraries/botframework-streaming/src/interfaces/IHeader.ts @@ -0,0 +1,17 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + + /** + * Streaming payload header definition. + */ +export interface IHeader { + payloadType: string; + payloadLength: number; + id: string; + end: boolean; +} diff --git a/libraries/botframework-streaming/src/interfaces/IHttpContentHeaders.ts b/libraries/botframework-streaming/src/interfaces/IHttpContentHeaders.ts new file mode 100644 index 0000000000..70a23a88a7 --- /dev/null +++ b/libraries/botframework-streaming/src/interfaces/IHttpContentHeaders.ts @@ -0,0 +1,15 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Streaming Http content header definition. + */ +export interface IHttpContentHeaders { + type?: string; + contentLength?: number; +} \ No newline at end of file diff --git a/libraries/botframework-streaming/src/interfaces/IReceiveRequest.ts b/libraries/botframework-streaming/src/interfaces/IReceiveRequest.ts new file mode 100644 index 0000000000..c6c626f3c8 --- /dev/null +++ b/libraries/botframework-streaming/src/interfaces/IReceiveRequest.ts @@ -0,0 +1,27 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { ContentStream } from '../contentStream'; + +/** + * Streaming receive request definition + */ +export interface IReceiveRequest { + /// Request verb, null on responses + /// + verb?: string; + + /// + /// Request path; null on responses + /// + path?: string; + + /// + /// Gets or sets the collection of stream attachments included in this request. + /// + streams: ContentStream[]; +} diff --git a/libraries/botframework-streaming/src/interfaces/IReceiveResponse.ts b/libraries/botframework-streaming/src/interfaces/IReceiveResponse.ts new file mode 100644 index 0000000000..6fb2a6c31b --- /dev/null +++ b/libraries/botframework-streaming/src/interfaces/IReceiveResponse.ts @@ -0,0 +1,16 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { ContentStream } from '../contentStream'; + +/** + * Streaming response from a receive request. + */ +export interface IReceiveResponse { + statusCode?: number; + streams: ContentStream[]; +} diff --git a/libraries/botframework-streaming/src/interfaces/IRequestPayload.ts b/libraries/botframework-streaming/src/interfaces/IRequestPayload.ts new file mode 100644 index 0000000000..82ea4afc11 --- /dev/null +++ b/libraries/botframework-streaming/src/interfaces/IRequestPayload.ts @@ -0,0 +1,17 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { IStreamDescription } from './IStreamDescription'; + +/** + * Definition for a streaming request payload. + */ +export interface IRequestPayload { + verb: string; + path: string; + streams?: IStreamDescription[]; +} diff --git a/libraries/botframework-streaming/src/interfaces/IResponsePayload.ts b/libraries/botframework-streaming/src/interfaces/IResponsePayload.ts new file mode 100644 index 0000000000..a84969165d --- /dev/null +++ b/libraries/botframework-streaming/src/interfaces/IResponsePayload.ts @@ -0,0 +1,16 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { IStreamDescription } from './IStreamDescription'; + +/** + * Base class for all dialogs. + */ +export interface IResponsePayload { + statusCode: number; + streams?: IStreamDescription[]; +} diff --git a/libraries/botframework-streaming/src/interfaces/ISendPacket.ts b/libraries/botframework-streaming/src/interfaces/ISendPacket.ts new file mode 100644 index 0000000000..e45a7d38cb --- /dev/null +++ b/libraries/botframework-streaming/src/interfaces/ISendPacket.ts @@ -0,0 +1,18 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { IHeader } from './IHeader'; +import { SubscribableStream } from '../subscribableStream'; + +/** + * Streaming send packet definition. + */ +export interface ISendPacket { + header: IHeader; + payload: SubscribableStream; + sentCallback: () => Promise; +} diff --git a/libraries/botframework-streaming/src/interfaces/ISocket.ts b/libraries/botframework-streaming/src/interfaces/ISocket.ts new file mode 100644 index 0000000000..be36b7efe0 --- /dev/null +++ b/libraries/botframework-streaming/src/interfaces/ISocket.ts @@ -0,0 +1,21 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * The interface implemented by any compatible socket transport, typically used + * with the WebSocket server or client. + */ +export interface ISocket { + isConnected: boolean; + write(buffer: Buffer); + connect(serverAddress: string): Promise; + close(); + setOnMessageHandler(handler: (x: any) => void); + setOnErrorHandler(handler: (x: any) => void); + setOnCloseHandler(handler: (x: any) => void); +} diff --git a/libraries/botframework-streaming/src/interfaces/IStreamDescription.ts b/libraries/botframework-streaming/src/interfaces/IStreamDescription.ts new file mode 100644 index 0000000000..f68449c029 --- /dev/null +++ b/libraries/botframework-streaming/src/interfaces/IStreamDescription.ts @@ -0,0 +1,16 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Definition of a stream description. + */ +export interface IStreamDescription { + id: string; + contentType?: string; + length?: number; +} diff --git a/libraries/botframework-streaming/src/interfaces/IStreamWrapper.ts b/libraries/botframework-streaming/src/interfaces/IStreamWrapper.ts new file mode 100644 index 0000000000..c26dc54fd2 --- /dev/null +++ b/libraries/botframework-streaming/src/interfaces/IStreamWrapper.ts @@ -0,0 +1,16 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { SubscribableStream } from '../subscribableStream'; + +/** + * Stream with length. + */ +export interface IStreamWrapper { + stream: SubscribableStream; + streamLength?: number; +} diff --git a/libraries/botframework-streaming/src/interfaces/IStreamingTransportClient.ts b/libraries/botframework-streaming/src/interfaces/IStreamingTransportClient.ts new file mode 100644 index 0000000000..ee9304c3a4 --- /dev/null +++ b/libraries/botframework-streaming/src/interfaces/IStreamingTransportClient.ts @@ -0,0 +1,19 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { IReceiveResponse } from './IReceiveResponse'; +import { StreamingRequest } from '../StreamingRequest'; + +/** + * Abstraction to define the characteristics of a streaming transport client. + * Example possible implementations include WebSocket transport client or NamedPipe transport client. + */ +export interface IStreamingTransportClient { + connect(): Promise; + disconnect(): void; + send(request: StreamingRequest): Promise; +} diff --git a/libraries/botframework-streaming/src/interfaces/IStreamingTransportServer.ts b/libraries/botframework-streaming/src/interfaces/IStreamingTransportServer.ts new file mode 100644 index 0000000000..7270d7c528 --- /dev/null +++ b/libraries/botframework-streaming/src/interfaces/IStreamingTransportServer.ts @@ -0,0 +1,19 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { IReceiveResponse } from './IReceiveResponse'; +import { StreamingRequest } from '../StreamingRequest'; + +/** + * Abstraction to define the characteristics of a streaming transport server. + * Example possible implementations include WebSocket transport server or NamedPipe transport server. + */ +export interface IStreamingTransportServer { + start(): Promise; + disconnect(): void; + send(request: StreamingRequest): Promise; +} diff --git a/libraries/botframework-streaming/src/interfaces/ITransport.ts b/libraries/botframework-streaming/src/interfaces/ITransport.ts new file mode 100644 index 0000000000..a74323b5cd --- /dev/null +++ b/libraries/botframework-streaming/src/interfaces/ITransport.ts @@ -0,0 +1,15 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + + /** + * Abstraction for a generic transport definition. + */ +export interface ITransport { + isConnected(): boolean; + close(); +} diff --git a/libraries/botframework-streaming/src/interfaces/ITransportReceiver.ts b/libraries/botframework-streaming/src/interfaces/ITransportReceiver.ts new file mode 100644 index 0000000000..bdd13e7eb3 --- /dev/null +++ b/libraries/botframework-streaming/src/interfaces/ITransportReceiver.ts @@ -0,0 +1,15 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { ITransport } from './ITransport'; + +/** + * Definition of a streaming transport that can receive requests. + */ +export interface ITransportReceiver extends ITransport { + receive(count: number): Promise; +} diff --git a/libraries/botframework-streaming/src/interfaces/ITransportSender.ts b/libraries/botframework-streaming/src/interfaces/ITransportSender.ts new file mode 100644 index 0000000000..f3e087e370 --- /dev/null +++ b/libraries/botframework-streaming/src/interfaces/ITransportSender.ts @@ -0,0 +1,15 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { ITransport } from './ITransport'; + +/** + * Definition of a streaming transport that can send requests. + */ +export interface ITransportSender extends ITransport { + send(buffer: Buffer): number; +} diff --git a/libraries/botframework-streaming/src/interfaces/index.ts b/libraries/botframework-streaming/src/interfaces/index.ts new file mode 100644 index 0000000000..df6b74c679 --- /dev/null +++ b/libraries/botframework-streaming/src/interfaces/index.ts @@ -0,0 +1,12 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +export * from './IReceiveRequest'; +export * from './IReceiveResponse'; +export * from './ISocket'; +export * from './IStreamingTransportClient'; +export * from './IStreamingTransportServer'; diff --git a/libraries/botframework-streaming/src/namedPipe/index.ts b/libraries/botframework-streaming/src/namedPipe/index.ts new file mode 100644 index 0000000000..86f96dfd54 --- /dev/null +++ b/libraries/botframework-streaming/src/namedPipe/index.ts @@ -0,0 +1,11 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './namedPipeClient'; +export * from './namedPipeServer'; +export * from './namedPipeTransport'; diff --git a/libraries/botframework-streaming/src/namedPipe/namedPipeClient.ts b/libraries/botframework-streaming/src/namedPipe/namedPipeClient.ts new file mode 100644 index 0000000000..90651f9b0f --- /dev/null +++ b/libraries/botframework-streaming/src/namedPipe/namedPipeClient.ts @@ -0,0 +1,105 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { connect } from 'net'; +import { ProtocolAdapter } from '../protocolAdapter'; +import { RequestHandler } from '../requestHandler'; +import { StreamingRequest } from '../streamingRequest'; +import { RequestManager } from '../payloads'; +import { + PayloadReceiver, + PayloadSender +} from '../payloadTransport'; +import { NamedPipeTransport } from './NamedPipeTransport'; +import { IStreamingTransportClient, IReceiveResponse } from '../interfaces'; + +/** + * Streaming transport client implementation that uses named pipes for inter-process communication. + */ +export class NamedPipeClient implements IStreamingTransportClient { + private readonly _baseName: string; + private readonly _requestHandler: RequestHandler; + private readonly _sender: PayloadSender; + private readonly _receiver: PayloadReceiver; + private readonly _requestManager: RequestManager; + private readonly _protocolAdapter: ProtocolAdapter; + private readonly _autoReconnect: boolean; + private _isDisconnecting: boolean; + + /** + * Creates a new instance of the [NamedPipeClient](xref:botframework-streaming.NamedPipeClient) class. + * + * @param baseName The named pipe to connect to. + * @param requestHandler Optional [RequestHandler](xref:botframework-streaming.RequestHandler) to process incoming messages received by this client. + * @param autoReconnect Optional setting to determine if the client sould attempt to reconnect automatically on disconnection events. Defaults to true. + */ + public constructor(baseName: string, requestHandler?: RequestHandler, autoReconnect: boolean = true) { + this._baseName = baseName; + this._requestHandler = requestHandler; + this._autoReconnect = autoReconnect; + this._requestManager = new RequestManager(); + this._sender = new PayloadSender(); + this._sender.disconnected = this.onConnectionDisconnected.bind(this); + this._receiver = new PayloadReceiver(); + this._receiver.disconnected = this.onConnectionDisconnected.bind(this); + this._protocolAdapter = new ProtocolAdapter(this._requestHandler, this._requestManager, this._sender, this._receiver); + } + + /** + * Establish a connection with no custom headers. + */ + public async connect(): Promise { + let outgoingPipeName: string = NamedPipeTransport.PipePath + this._baseName + NamedPipeTransport.ServerIncomingPath; + let outgoing = connect(outgoingPipeName); + let incomingPipeName: string = NamedPipeTransport.PipePath + this._baseName + NamedPipeTransport.ServerOutgoingPath; + let incoming = connect(incomingPipeName); + this._sender.connect(new NamedPipeTransport(outgoing)); + this._receiver.connect(new NamedPipeTransport(incoming)); + } + + /** + * Disconnect the client. + */ + public disconnect(): void { + this._sender.disconnect(); + this._receiver.disconnect(); + } + + /** + * Task used to send data over this client connection. + * + * @param request The [StreamingRequest](xref:botframework-streaming.StreamingRequest) to send. + * @returns A promise for an instance of [IReceiveResponse](xref:botframework-streaming.IReceiveResponse) on completion of the send operation. + */ + public async send(request: StreamingRequest): Promise { + return this._protocolAdapter.sendRequest(request); + } + + private onConnectionDisconnected(sender: object, args: any): void { + if (!this._isDisconnecting) { + this._isDisconnecting = true; + try { + if (this._sender.isConnected) { + this._sender.disconnect(); + } + + if (this._receiver.isConnected) { + this._receiver.disconnect(); + } + + if (this._autoReconnect) { + this.connect() + .then((): void => { }) + .catch((error): void => { throw new Error(`Failed to reconnect. Reason: ${ error.message } Sender: ${ sender } Args: ${ args }. `); }); + } + } + finally { + this._isDisconnecting = false; + } + } + } +} diff --git a/libraries/botframework-streaming/src/namedPipe/namedPipeServer.ts b/libraries/botframework-streaming/src/namedPipe/namedPipeServer.ts new file mode 100644 index 0000000000..95bddb92b5 --- /dev/null +++ b/libraries/botframework-streaming/src/namedPipe/namedPipeServer.ts @@ -0,0 +1,144 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { Server, Socket } from 'net'; +import { ProtocolAdapter } from '../protocolAdapter'; +import { RequestHandler } from '../requestHandler'; +import { StreamingRequest } from '../streamingRequest'; +import { RequestManager } from '../payloads'; +import { + PayloadReceiver, + PayloadSender +} from '../payloadTransport'; +import { NamedPipeTransport } from './NamedPipeTransport'; +import { IStreamingTransportServer, IReceiveResponse } from '../interfaces'; + +/** +* Streaming transport server implementation that uses named pipes for inter-process communication. +*/ +export class NamedPipeServer implements IStreamingTransportServer { + private _outgoingServer: Server; + private _incomingServer: Server; + private readonly _baseName: string; + private readonly _requestHandler: RequestHandler; + private readonly _sender: PayloadSender; + private readonly _receiver: PayloadReceiver; + private readonly _requestManager: RequestManager; + private readonly _protocolAdapter: ProtocolAdapter; + private readonly _autoReconnect: boolean; + private _isDisconnecting: boolean; + + /** + * Creates a new instance of the [NamedPipeServer](xref:botframework-streaming.NamedPipeServer) class. + * + * @param baseName The named pipe to connect to. + * @param requestHandler Optional [RequestHandler](xref:botframework-streaming.RequestHandler) to process incoming messages received by this client. + * @param autoReconnect Optional setting to determine if the client sould attempt to reconnect automatically on disconnection events. Defaults to true. + */ + public constructor(baseName: string, requestHandler?: RequestHandler, autoReconnect: boolean = true) { + this._baseName = baseName; + this._requestHandler = requestHandler; + this._autoReconnect = autoReconnect; + this._requestManager = new RequestManager(); + this._sender = new PayloadSender(); + this._receiver = new PayloadReceiver(); + this._protocolAdapter = new ProtocolAdapter(this._requestHandler, this._requestManager, this._sender, this._receiver); + this._sender.disconnected = this.onConnectionDisconnected.bind(this); + this._receiver.disconnected = this.onConnectionDisconnected.bind(this); + } + + /** + * Used to establish the connection used by this server and begin listening for incoming messages. + * + * @returns A promised string that will not resolve as long as the server is running. + */ + public async start(): Promise { + if (this._receiver.isConnected || this._sender.isConnected || this._incomingServer || this._outgoingServer) { + this.disconnect(); + } + + const incoming = new Promise(resolve => { + this._incomingServer = new Server((socket: Socket): void => { + this._receiver.connect(new NamedPipeTransport(socket)); + resolve(); + }); + }); + + const outgoing = new Promise(resolve => { + this._outgoingServer = new Server((socket: Socket): void => { + this._sender.connect(new NamedPipeTransport(socket)); + resolve(); + }); + }); + + // These promises will only resolve when the underlying connection has terminated. + // Anything awaiting on them will be blocked for the duration of the session, + // which is useful when detecting premature terminations, but requires an unawaited + // promise during the process of establishing the connection. + Promise.all([incoming, outgoing]); + + const { PipePath, ServerIncomingPath, ServerOutgoingPath } = NamedPipeTransport; + const incomingPipeName = PipePath + this._baseName + ServerIncomingPath; + const outgoingPipeName = PipePath + this._baseName + ServerOutgoingPath; + + this._incomingServer.listen(incomingPipeName); + this._outgoingServer.listen(outgoingPipeName); + + return 'connected'; + } + + /** + * Allows for manually disconnecting the server. + */ + public disconnect(): void { + this._sender.disconnect(); + this._receiver.disconnect(); + + if (this._incomingServer) { + this._incomingServer.close(); + this._incomingServer = null; + } + + if (this._outgoingServer) { + this._outgoingServer.close(); + this._outgoingServer = null; + } + } + + /** + * Task used to send data over this client connection. + * + * @param request The [StreamingRequest](xref:botframework-streaming.StreamingRequest) to send. + * @returns A promise for an instance of [IReceiveResponse](xref:botframework-streaming.IReceiveResponse) on completion of the send operation. + */ + public async send(request: StreamingRequest): Promise { + return this._protocolAdapter.sendRequest(request); + } + + private onConnectionDisconnected(): void { + if (!this._isDisconnecting) { + this._isDisconnecting = true; + try { + if (this._sender.isConnected) { + this._sender.disconnect(); + } + + if (this._receiver.isConnected) { + this._receiver.disconnect(); + } + + if (this._autoReconnect) { + this.start() + .catch((err): void => { throw(new Error(`Unable to reconnect: ${ err.message }`)); }); + } + } + finally { + this._isDisconnecting = false; + } + } + } +} diff --git a/libraries/botframework-streaming/src/namedPipe/namedPipeTransport.ts b/libraries/botframework-streaming/src/namedPipe/namedPipeTransport.ts new file mode 100644 index 0000000000..1941334ee8 --- /dev/null +++ b/libraries/botframework-streaming/src/namedPipe/namedPipeTransport.ts @@ -0,0 +1,167 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { Socket } from 'net'; +import { ITransportSender } from '../interfaces/ITransportSender'; +import { ITransportReceiver } from '../interfaces/ITransportReceiver'; + + +/** + * Named pipes based transport sender and receiver abstraction + */ +export class NamedPipeTransport implements ITransportSender, ITransportReceiver { + public static readonly PipePath: string = '\\\\.\\pipe\\'; + public static readonly ServerIncomingPath: string = '.incoming'; + public static readonly ServerOutgoingPath: string = '.outgoing'; + + private _socket: Socket; + private readonly _queue: Buffer[]; + private _active: Buffer; + private _activeOffset: number; + private _activeReceiveResolve: (resolve: Buffer) => void; + private _activeReceiveReject: (reason?: any) => void; + private _activeReceiveCount: number; + + /** + * Creates a new instance of the [NamedPipeTransport](xref:botframework-streaming.NamedPipeTransport) class. + * + * @param socket The socket object to build this connection on. + */ + public constructor(socket: Socket) { + this._socket = socket; + this._queue = []; + this._activeOffset = 0; + this._activeReceiveCount = 0; + if (socket) { + this._socket.on('data', (data): void => { + this.socketReceive(data); + }); + this._socket.on('close', (): void => { + this.socketClose(); + }); + this._socket.on('error', (err): void => { + this.socketError(err); + }); + } + } + + /** + * Writes to the pipe and sends. + * + * @param buffer The buffer full of data to send out across the socket. + */ + public send(buffer: Buffer): number { + if (this._socket && !this._socket.connecting && this._socket.writable) { + this._socket.write(buffer); + + return buffer.length; + } + + return 0; + } + + /** + * Returns true if currently connected. + */ + public isConnected(): boolean { + return !(!this._socket || this._socket.destroyed || this._socket.connecting); + } + + /** + * Closes the transport. + */ + public close(): void { + if (this._socket) { + this._socket.end('end'); + this._socket = null; + } + } + + /** + * Receive from the transport into the buffer. + */ + public receive(count: number): Promise { + if (this._activeReceiveResolve) { + throw new Error('Cannot call receive more than once before it has returned.'); + } + + this._activeReceiveCount = count; + + let promise = new Promise((resolve, reject): void => { + this._activeReceiveResolve = resolve; + this._activeReceiveReject = reject; + }); + + this.trySignalData(); + + return promise; + } + + private socketReceive(data: Buffer): void { + if (this._queue && data && data.length > 0) { + this._queue.push(data); + this.trySignalData(); + } + } + + private socketClose(): void { + if (this._activeReceiveReject) { + this._activeReceiveReject(new Error('Socket was closed.')); + } + + this._active = null; + this._activeOffset = 0; + this._activeReceiveResolve = null; + this._activeReceiveReject = null; + this._activeReceiveCount = 0; + this._socket = null; + } + + private socketError(err: Error): void { + if (this._activeReceiveReject) { + this._activeReceiveReject(err); + } + this.socketClose(); + } + + private trySignalData(): void { + if (this._activeReceiveResolve) { + if (!this._active && this._queue.length > 0) { + this._active = this._queue.shift(); + this._activeOffset = 0; + } + + if (this._active) { + if (this._activeOffset === 0 && this._active.length === this._activeReceiveCount) { + // can send the entire _active buffer + let buffer = this._active; + this._active = null; + + this._activeReceiveResolve(buffer); + } else { + // create a new buffer and copy some of the contents into it + let available = Math.min(this._activeReceiveCount, this._active.length - this._activeOffset); + let buffer = Buffer.alloc(available); + this._active.copy(buffer, 0, this._activeOffset, this._activeOffset + available); + this._activeOffset += available; + + // if we used all of active, set it to undefined + if (this._activeOffset >= this._active.length) { + this._active = null; + this._activeOffset = 0; + } + + this._activeReceiveResolve(buffer); + } + + this._activeReceiveCount = 0; + this._activeReceiveReject = null; + this._activeReceiveResolve = null; + } + } + } +} diff --git a/libraries/botframework-streaming/src/payloadTransport/index.ts b/libraries/botframework-streaming/src/payloadTransport/index.ts new file mode 100644 index 0000000000..461b0d0e0c --- /dev/null +++ b/libraries/botframework-streaming/src/payloadTransport/index.ts @@ -0,0 +1,14 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './payloadReceiver'; +export * from './payloadSender'; +export * from './payloadReceiver'; +export * from './payloadSender'; +export * from './transportDisconnectedEventArgs'; +export * from './transportDisconnectedEventHandler'; diff --git a/libraries/botframework-streaming/src/payloadTransport/payloadReceiver.ts b/libraries/botframework-streaming/src/payloadTransport/payloadReceiver.ts new file mode 100644 index 0000000000..50eb5b28e1 --- /dev/null +++ b/libraries/botframework-streaming/src/payloadTransport/payloadReceiver.ts @@ -0,0 +1,129 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { TransportDisconnectedEventHandler } from '.'; +import { PayloadTypes } from '../payloads/payloadTypes'; +import { HeaderSerializer } from '../payloads/headerSerializer'; +import { SubscribableStream } from '../subscribableStream'; +import { PayloadConstants } from '../payloads/payloadConstants'; +import { TransportDisconnectedEventArgs } from './TransportDisconnectedEventArgs'; +import { ITransportReceiver } from '../interfaces/ITransportReceiver'; +import { IHeader } from '../interfaces/IHeader'; + +/** + * Payload receiver for straming. + */ +export class PayloadReceiver { + public isConnected: boolean; + public disconnected: TransportDisconnectedEventHandler = function(sender, events){}; + private _receiver: ITransportReceiver; + private _receiveHeaderBuffer: Buffer; + private _receivePayloadBuffer: Buffer; + private _getStream: (header: IHeader) => SubscribableStream; + private _receiveAction: (header: IHeader, stream: SubscribableStream, length: number) => void; + + /** + * Connects to a transport receiver + * + * @param receiver The [ITransportReceiver](xref:botframework-streaming.ITransportReceiver) object to pull incoming data from. + */ + public connect(receiver: ITransportReceiver): void { + if (this.isConnected) { + throw new Error('Already connected.'); + } else { + this._receiver = receiver; + this.isConnected = true; + this.runReceive(); + } + } + + /** + * Allows subscribing to this receiver in order to be notified when new data comes in. + * + * @param getStream Callback when a new stream has been received. + * @param receiveAction Callback when a new message has been received. + */ + public subscribe(getStream: (header: IHeader) => SubscribableStream, receiveAction: (header: IHeader, stream: SubscribableStream, count: number) => void): void { + this._getStream = getStream; + this._receiveAction = receiveAction; + } + + /** + * Force this receiver to disconnect. + * + * @param e Event arguments to include when broadcasting disconnection event. + */ + public disconnect(e?: TransportDisconnectedEventArgs): void { + let didDisconnect; + try { + if (this.isConnected) { + this._receiver.close(); + didDisconnect = true; + this.isConnected = false; + } + } catch (error) { + this.isConnected = false; + this.disconnected(this, new TransportDisconnectedEventArgs(error.message)); + } + this._receiver = null; + this.isConnected = false; + + if (didDisconnect) { + this.disconnected(this, e || TransportDisconnectedEventArgs.Empty); + } + } + + private runReceive(): void { + this.receivePackets() + .catch(); + } + + private async receivePackets(): Promise { + let isClosed; + + while (this.isConnected && !isClosed) { + try { + let readSoFar = 0; + while (readSoFar < PayloadConstants.MaxHeaderLength) { + this._receiveHeaderBuffer = await this._receiver.receive(PayloadConstants.MaxHeaderLength - readSoFar); + + if (this._receiveHeaderBuffer) { + readSoFar += this._receiveHeaderBuffer.length; + } + } + + let header = HeaderSerializer.deserialize(this._receiveHeaderBuffer); + let isStream = header.payloadType === PayloadTypes.stream; + + if (header.payloadLength > 0) { + let bytesActuallyRead = 0; + + let contentStream = this._getStream(header); + + while (bytesActuallyRead < header.payloadLength && bytesActuallyRead < PayloadConstants.MaxPayloadLength) { + let count = Math.min(header.payloadLength - bytesActuallyRead, PayloadConstants.MaxPayloadLength); + this._receivePayloadBuffer = await this._receiver.receive(count); + bytesActuallyRead += this._receivePayloadBuffer.byteLength; + contentStream.write(this._receivePayloadBuffer); + + // If this is a stream we want to keep handing it up as it comes in + if (isStream) { + this._receiveAction(header, contentStream, bytesActuallyRead); + } + } + + if (!isStream) { + this._receiveAction(header, contentStream, bytesActuallyRead); + } + } + } catch (error) { + isClosed = true; + this.disconnect(new TransportDisconnectedEventArgs(error.message)); + } + } + } +} diff --git a/libraries/botframework-streaming/src/payloadTransport/payloadSender.ts b/libraries/botframework-streaming/src/payloadTransport/payloadSender.ts new file mode 100644 index 0000000000..68a1bc95f2 --- /dev/null +++ b/libraries/botframework-streaming/src/payloadTransport/payloadSender.ts @@ -0,0 +1,93 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { HeaderSerializer } from '../payloads/headerSerializer'; +import { SubscribableStream } from '../subscribableStream'; +import { PayloadConstants } from '../payloads/payloadConstants'; +import { TransportDisconnectedEventArgs } from './transportDisconnectedEventArgs'; +import { TransportDisconnectedEventHandler } from './transportDisconnectedEventHandler'; +import { ITransportSender } from '../interfaces/ITransportSender'; +import { IHeader } from '../interfaces/IHeader'; +import { ISendPacket } from '../interfaces/ISendPacket'; + +/** + * Streaming payload sender. + */ +export class PayloadSender { + public disconnected?: TransportDisconnectedEventHandler; + private sender: ITransportSender; + + /** + * Tests whether the transport sender is connected. + * + * @returns true if connected to a transport sender. + */ + public get isConnected(): boolean { + return !!this.sender; + } + + /** + * Connects to the given transport sender. + * + * @param sender The transport sender to connect this payload sender to. + */ + public connect(sender: ITransportSender): void { + this.sender = sender; + } + + /** + * Sends a payload out over the connected transport sender. + * + * @param header The header to attach to the outgoing payload. + * @param payload The stream of buffered data to send. + * @param sentCalback The function to execute when the send has completed. + */ + public sendPayload(header: IHeader, payload?: SubscribableStream, sentCallback?: () => Promise): void { + var packet: ISendPacket = {header, payload, sentCallback}; + this.writePacket(packet); + } + + /** + * Disconnects this payload sender. + * + * @param e The disconnected event arguments to include in the disconnected event broadcast. + */ + public disconnect(e?: TransportDisconnectedEventArgs): void { + if (this.isConnected) { + this.sender.close(); + this.sender = null; + + if (this.disconnected) { + this.disconnected(this, e || TransportDisconnectedEventArgs.Empty); + } + } + } + + private writePacket(packet: ISendPacket): void { + try { + let sendHeaderBuffer: Buffer = Buffer.alloc(PayloadConstants.MaxHeaderLength); + HeaderSerializer.serialize(packet.header, sendHeaderBuffer); + this.sender.send(sendHeaderBuffer); + + if (packet.header.payloadLength > 0 && packet.payload) { + let count = packet.header.payloadLength; + while (count > 0) { + let chunk = packet.payload.read(count); + this.sender.send(chunk); + count -= chunk.length; + } + + if (packet.sentCallback) { + packet.sentCallback(); + } + } + } catch (e) { + this.disconnect(new TransportDisconnectedEventArgs(e.message)); + } + } +} diff --git a/libraries/botframework-streaming/src/payloadTransport/transportDisconnectedEventArgs.ts b/libraries/botframework-streaming/src/payloadTransport/transportDisconnectedEventArgs.ts new file mode 100644 index 0000000000..11487cb676 --- /dev/null +++ b/libraries/botframework-streaming/src/payloadTransport/transportDisconnectedEventArgs.ts @@ -0,0 +1,15 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +export class TransportDisconnectedEventArgs { + public static Empty: TransportDisconnectedEventArgs = new TransportDisconnectedEventArgs(); + public reason: string; + + public constructor(reason?: string) { + this.reason = reason; + } +} diff --git a/libraries/botframework-streaming/src/payloadTransport/transportDisconnectedEventHandler.ts b/libraries/botframework-streaming/src/payloadTransport/transportDisconnectedEventHandler.ts new file mode 100644 index 0000000000..9d7ad02121 --- /dev/null +++ b/libraries/botframework-streaming/src/payloadTransport/transportDisconnectedEventHandler.ts @@ -0,0 +1,10 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { TransportDisconnectedEventArgs } from './transportDisconnectedEventArgs'; + +export type TransportDisconnectedEventHandler = (sender: any, e: TransportDisconnectedEventArgs) => void; diff --git a/libraries/botframework-streaming/src/payloads/headerSerializer.ts b/libraries/botframework-streaming/src/payloads/headerSerializer.ts new file mode 100644 index 0000000000..aea76c477f --- /dev/null +++ b/libraries/botframework-streaming/src/payloads/headerSerializer.ts @@ -0,0 +1,96 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { IHeader } from '../interfaces/IHeader'; +import { PayloadConstants } from './payloadConstants'; + +/** + * Streaming header serializer + */ +export class HeaderSerializer { + public static readonly Delimiter = '.'; + public static readonly Terminator = '\n'; + public static readonly End = '1'; + public static readonly NotEnd = '0'; + public static readonly TypeOffset: number = 0; + public static readonly TypeDelimiterOffset = 1; + public static readonly LengthOffset = 2; + public static readonly LengthLength = 6; + public static readonly LengthDelimeterOffset = 8; + public static readonly IdOffset = 9; + public static readonly IdLength = 36; + public static readonly IdDelimeterOffset = 45; + public static readonly EndOffset = 46; + public static readonly TerminatorOffset = 47; + public static readonly Encoding = 'utf8'; + + /** + * Serializes the header into a buffer + * + * @param header The header to serialize. + * @param buffer The buffer into which to serialize the header. + */ + public static serialize(header: IHeader, buffer: Buffer): void { + buffer.write(header.payloadType, this.TypeOffset, 1, this.Encoding); + buffer.write(this.Delimiter, this.TypeDelimiterOffset, 1, this.Encoding); + buffer.write(this.headerLengthPadder(header.payloadLength, this.LengthLength, '0'), this.LengthOffset, this.LengthLength, this.Encoding); + buffer.write(this.Delimiter, this.LengthDelimeterOffset, 1, this.Encoding); + buffer.write(header.id, this.IdOffset); + buffer.write(this.Delimiter, this.IdDelimeterOffset, 1, this.Encoding); + buffer.write(header.end ? this.End : this.NotEnd, this.EndOffset); + buffer.write(this.Terminator, this.TerminatorOffset); + } + + /** + * Deserializes a buffer containing header information. + * + * @param buffer The buffer from which to obtain the data to deserialize. + * @returns The deserialized header from the buffer. + */ + public static deserialize(buffer: Buffer): IHeader { + let jsonBuffer = buffer.toString(this.Encoding); + let headerArray = jsonBuffer.split(this.Delimiter); + + if (headerArray.length !== 4) { + throw Error(`Cannot parse header, header is malformed. Header: ${ jsonBuffer }`); + } + + const [payloadType, length, id, headerEnd] = headerArray; + + const end = headerEnd === '1\n'; + const payloadLength = Number(length); + + const header: IHeader = { end, payloadLength, payloadType, id }; + + if (!(header.payloadLength <= PayloadConstants.MaxPayloadLength && header.payloadLength >= PayloadConstants.MinLength)) { + throw Error(`Header length of ${ header.payloadLength } is missing or malformed`); + } + + if (header.payloadType.length !== this.TypeDelimiterOffset) { + throw Error(`Header type '${ header.payloadType.length }' is missing or malformed.`); + } + + if (!header.id || !header.id.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i) || header.id.length !== this.IdLength) { + throw Error(`Header ID '${ header.id }' is missing or malformed.`); + } + + if (!(headerEnd === '0\n' || headerEnd === '1\n')) { + throw Error(`Header End is missing or not a valid value. Header end: '${ headerEnd }'`); + } + + return header; + } + + public static headerLengthPadder(lengthValue: number, totalLength: number, padChar: string): string { + let result = Array(totalLength + 1) + .join(padChar); + + let lengthString = lengthValue.toString(); + + return (result + lengthString).slice(lengthString.length); + } +} diff --git a/libraries/botframework-streaming/src/payloads/index.ts b/libraries/botframework-streaming/src/payloads/index.ts new file mode 100644 index 0000000000..3f13e4f3e7 --- /dev/null +++ b/libraries/botframework-streaming/src/payloads/index.ts @@ -0,0 +1,15 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './headerSerializer'; +export * from './streamManager'; +export * from './payloadAssemblerManager'; +export * from './payloadTypes'; +export * from './requestManager'; +export * from './sendOperations'; +export * from './streamManager'; diff --git a/libraries/botframework-streaming/src/payloads/payloadAssemblerManager.ts b/libraries/botframework-streaming/src/payloads/payloadAssemblerManager.ts new file mode 100644 index 0000000000..480b292680 --- /dev/null +++ b/libraries/botframework-streaming/src/payloads/payloadAssemblerManager.ts @@ -0,0 +1,63 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { SubscribableStream } from '../subscribableStream'; +import { PayloadAssembler } from '../assemblers/payloadAssembler'; +import { StreamManager } from './streamManager'; +import { IHeader } from '../interfaces/IHeader'; +import { PayloadTypes } from './payloadTypes'; + +/** + * Orchestrates assemly of payloads + */ +export class PayloadAssemblerManager { + private readonly onReceiveRequest; + private readonly onReceiveResponse; + private readonly streamManager: StreamManager; + private readonly activeAssemblers: { [id: string]: PayloadAssembler } = {}; + + public constructor(streamManager: StreamManager, onReceiveResponse: Function, onReceiveRequest: Function) { + this.streamManager = streamManager; + this.onReceiveRequest = onReceiveRequest; + this.onReceiveResponse = onReceiveResponse; + } + + public getPayloadStream(header: IHeader): SubscribableStream { + if (header.payloadType === PayloadTypes.stream) { + return this.streamManager.getPayloadStream(header); + } else if (!this.activeAssemblers[header.id]) { + let assembler = this.createPayloadAssembler(header); + + if (assembler) { + this.activeAssemblers[header.id] = assembler; + return assembler.getPayloadStream(); + } + } + } + + public onReceive(header: IHeader, contentStream: SubscribableStream, contentLength: number): void { + if (header.payloadType === PayloadTypes.stream) { + this.streamManager.onReceive(header, contentStream, contentLength); + } else { + if (this.activeAssemblers && this.activeAssemblers[header.id]) { + let assembler = this.activeAssemblers[header.id]; + assembler.onReceive(header, contentStream, contentLength); + } + if (header.end) { + delete this.activeAssemblers[header.id]; + } + } + } + + private createPayloadAssembler(header: IHeader): PayloadAssembler { + if (header.payloadType === PayloadTypes.request) { + return new PayloadAssembler(this.streamManager, {header: header, onCompleted: this.onReceiveRequest}); + } else if (header.payloadType === PayloadTypes.response) { + return new PayloadAssembler(this.streamManager, {header: header, onCompleted: this.onReceiveResponse}); + } + } +} diff --git a/libraries/botframework-streaming/src/payloads/payloadConstants.ts b/libraries/botframework-streaming/src/payloads/payloadConstants.ts new file mode 100644 index 0000000000..9ac5d670c1 --- /dev/null +++ b/libraries/botframework-streaming/src/payloads/payloadConstants.ts @@ -0,0 +1,17 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Constants for streaming payloads. + */ +export enum PayloadConstants { + MaxPayloadLength = 4096, + MaxHeaderLength = 48, + MaxLength = 999999, + MinLength = 0, +} diff --git a/libraries/botframework-streaming/src/payloads/payloadTypes.ts b/libraries/botframework-streaming/src/payloads/payloadTypes.ts new file mode 100644 index 0000000000..2c98016885 --- /dev/null +++ b/libraries/botframework-streaming/src/payloads/payloadTypes.ts @@ -0,0 +1,18 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Typess of payloads supported in the streaming library. + */ +export enum PayloadTypes { + request = 'A', + response = 'B', + stream = 'S', + cancelAll = 'X', + cancelStream = 'C' +} diff --git a/libraries/botframework-streaming/src/payloads/requestManager.ts b/libraries/botframework-streaming/src/payloads/requestManager.ts new file mode 100644 index 0000000000..b072fe6c41 --- /dev/null +++ b/libraries/botframework-streaming/src/payloads/requestManager.ts @@ -0,0 +1,61 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { IReceiveResponse } from '../interfaces/IReceiveResponse'; + +/** + * A streaming pending request. + */ +class PendingRequest { + public requestId: string; + public resolve: (response: IReceiveResponse) => void; + public reject: (reason?: any) => void; +} + +/** + * Orchestrates and manages pending streaming requests. + */ +export class RequestManager { + private readonly _pendingRequests = {}; + + public pendingRequestCount(): number { + return Object.keys(this._pendingRequests).length; + } + + public async signalResponse(requestId: string, response: IReceiveResponse): Promise { + let pendingRequest = this._pendingRequests[requestId]; + + if (pendingRequest) { + pendingRequest.resolve(response); + delete this._pendingRequests[requestId]; + + return true; + } + + return false; + } + + public getResponse(requestId: string): Promise { + let pendingRequest = this._pendingRequests[requestId]; + + if (pendingRequest) { + return Promise.reject(`requestId '${ requestId }' already exists in RequestManager`); + } + + pendingRequest = new PendingRequest(); + pendingRequest.requestId = requestId; + + let promise = new Promise((resolve, reject): void => { + pendingRequest.resolve = resolve; + pendingRequest.reject = reject; + }); + + this._pendingRequests[requestId] = pendingRequest; + + return promise; + } +} diff --git a/libraries/botframework-streaming/src/payloads/sendOperations.ts b/libraries/botframework-streaming/src/payloads/sendOperations.ts new file mode 100644 index 0000000000..0ebe34042b --- /dev/null +++ b/libraries/botframework-streaming/src/payloads/sendOperations.ts @@ -0,0 +1,55 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { PayloadSender } from '../payloadTransport/payloadSender'; +import { StreamingRequest } from '../streamingRequest'; +import { StreamingResponse } from '../streamingResponse'; +import { CancelDisassembler } from '../disassemblers/cancelDisassembler'; +import { HttpContentStreamDisassembler } from '../disassemblers/httpContentStreamDisassembler'; +import { RequestDisassembler } from '../disassemblers/requestDisassembler'; +import { ResponseDisassembler } from '../disassemblers/responseDisassembler'; +import { PayloadTypes } from './payloadTypes'; + +/** + * Send operations for streaming payloads. + */ +export class SendOperations { + private readonly payloadSender: PayloadSender; + + public constructor(payloadSender: PayloadSender) { + this.payloadSender = payloadSender; + } + + public async sendRequest(id: string, request: StreamingRequest): Promise { + let disassembler = new RequestDisassembler(this.payloadSender, id, request); + + await disassembler.disassemble(); + + if (request.streams) { + request.streams.forEach(async (contentStream): Promise => { + await new HttpContentStreamDisassembler(this.payloadSender, contentStream).disassemble(); + }); + } + } + + public async sendResponse(id: string, response: StreamingResponse): Promise { + let disassembler = new ResponseDisassembler(this.payloadSender, id, response); + + await disassembler.disassemble(); + + if (response.streams) { + response.streams.forEach(async (contentStream): Promise => { + await new HttpContentStreamDisassembler(this.payloadSender, contentStream).disassemble(); + }); + } + } + + public async sendCancelStream(id: string): Promise { + let disassembler = new CancelDisassembler(this.payloadSender, id, PayloadTypes.cancelStream); + disassembler.disassemble(); + } +} diff --git a/libraries/botframework-streaming/src/payloads/streamManager.ts b/libraries/botframework-streaming/src/payloads/streamManager.ts new file mode 100644 index 0000000000..bac0bfb1bc --- /dev/null +++ b/libraries/botframework-streaming/src/payloads/streamManager.ts @@ -0,0 +1,61 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { IHeader } from '../interfaces/IHeader'; +import { SubscribableStream } from '../subscribableStream'; +import { PayloadAssembler } from '../assemblers/payloadAssembler'; + +/** + * Orchestrates and manages streams. + */ +export class StreamManager { + private readonly activeAssemblers = []; + private readonly onCancelStream: Function; + + public constructor(onCancelStream: Function) { + this.onCancelStream = onCancelStream; + } + + public getPayloadAssembler(id: string): PayloadAssembler { + if (!this.activeAssemblers[id]) { + // A new id has come in, kick off the process of tracking it. + let assembler = new PayloadAssembler(this, {id: id}); + this.activeAssemblers[id] = assembler; + + return assembler; + } else { + + return this.activeAssemblers[id]; + } + } + + public getPayloadStream(header: IHeader): SubscribableStream { + let assembler = this.getPayloadAssembler(header.id); + + return assembler.getPayloadStream(); + } + + public onReceive(header: IHeader, contentStream: SubscribableStream, contentLength: number): void { + if (!this.activeAssemblers[header.id]) { + return; + } + this.activeAssemblers[header.id].onReceive(header, contentStream, contentLength); + } + + public closeStream(id: string): void { + if (!this.activeAssemblers[id]) { + return; + } else { + let assembler: PayloadAssembler = this.activeAssemblers[id]; + this.activeAssemblers.splice(this.activeAssemblers.indexOf(id), 1); + let targetStream = assembler.getPayloadStream(); + if ((assembler.contentLength && targetStream.length < assembler.contentLength) || !assembler.end) { + this.onCancelStream(assembler); + } + } + } +} diff --git a/libraries/botframework-streaming/src/protocolAdapter.ts b/libraries/botframework-streaming/src/protocolAdapter.ts new file mode 100644 index 0000000000..2b98de551d --- /dev/null +++ b/libraries/botframework-streaming/src/protocolAdapter.ts @@ -0,0 +1,96 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { PayloadAssembler } from './assemblers/payloadAssembler'; +import { PayloadAssemblerManager } from './payloads/payloadAssemblerManager'; +import { RequestManager } from './payloads/requestManager'; +import { SendOperations } from './payloads/sendOperations'; +import { StreamManager } from './payloads/streamManager'; +import { PayloadReceiver } from './payloadTransport/payloadReceiver'; +import { PayloadSender } from './payloadTransport/payloadSender'; +import { RequestHandler } from './requestHandler'; +import { SubscribableStream } from './subscribableStream'; +import { StreamingRequest } from './streamingRequest'; +import { generateGuid } from './utilities/protocol-base'; +import { IReceiveResponse, IReceiveRequest } from './interfaces'; +import { IHeader } from './interfaces/IHeader'; + +export class ProtocolAdapter { + private readonly requestHandler: RequestHandler; + private readonly payloadSender: PayloadSender; + private readonly payloadReceiver: PayloadReceiver; + private readonly requestManager: RequestManager; + private readonly sendOperations: SendOperations; + private readonly streamManager: StreamManager; + private readonly assemblerManager: PayloadAssemblerManager; + + /// + /// Creates a new instance of the protocol adapter class. + /// + /// The handler that will process incoming requests. + /// The manager that will process outgoing requests. + /// The sender for use with outgoing requests. + /// The receiver for use with incoming requests. + public constructor(requestHandler: RequestHandler, requestManager: RequestManager, sender: PayloadSender, receiver: PayloadReceiver) { + this.requestHandler = requestHandler; + this.requestManager = requestManager; + this.payloadSender = sender; + this.payloadReceiver = receiver; + this.sendOperations = new SendOperations(this.payloadSender); + this.streamManager = new StreamManager(this.onCancelStream); + this.assemblerManager = new PayloadAssemblerManager(this.streamManager, (id: string, response: IReceiveResponse): Promise => this.onReceiveResponse(id, response),(id: string, request: IReceiveRequest): Promise => this.onReceiveRequest(id, request)); + this.payloadReceiver.subscribe((header: IHeader): SubscribableStream => this.assemblerManager.getPayloadStream(header),(header: IHeader, contentStream: SubscribableStream, contentLength: number): void => this.assemblerManager.onReceive(header, contentStream, contentLength)); + } + + /// + /// Sends a request over the attached request manager. + /// + /// The outgoing request to send. + /// Optional cancellation token. + public async sendRequest(request: StreamingRequest): Promise { + let requestId: string = generateGuid(); + await this.sendOperations.sendRequest(requestId, request); + + return this.requestManager.getResponse(requestId); + } + + /// + /// Executes the receive pipeline when a request comes in. + /// + /// The id the resources created for the response will be assigned. + /// The incoming request to process. + public async onReceiveRequest(id: string, request: IReceiveRequest): Promise { + if (this.requestHandler) { + let response = await this.requestHandler.processRequest(request); + + if (response) { + await this.sendOperations.sendResponse(id, response); + } + } + } + + /// + /// Executes the receive pipeline when a response comes in. + /// + /// The id the resources created for the response will be assigned. + /// The incoming response to process. + public async onReceiveResponse(id: string, response: IReceiveResponse): Promise { + await this.requestManager.signalResponse(id, response); + } + + /// + /// Executes the receive pipeline when a cancellation comes in. + /// + /// + /// The payload assembler processing the incoming data that this + /// cancellation request targets. + /// + public onCancelStream(contentStreamAssembler: PayloadAssembler): void { + this.sendOperations.sendCancelStream(contentStreamAssembler.id) + .catch(); + } +} diff --git a/libraries/botframework-streaming/src/requestHandler.ts b/libraries/botframework-streaming/src/requestHandler.ts new file mode 100644 index 0000000000..537a3f427d --- /dev/null +++ b/libraries/botframework-streaming/src/requestHandler.ts @@ -0,0 +1,23 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { IReceiveRequest } from './Interfaces/IReceiveRequest'; +import { StreamingResponse } from './streamingResponse'; + +/** + * Implemented by classes used to process incoming streaming requests sent over an [IStreamingTransport](xref:botframework-streaming.IStreamingTransport). + */ +export abstract class RequestHandler { + + /** + * The method that must be implemented in order to handle incoming requests. + * + * @param request A receipt request for this handler to process. + * @returns A promise that will produce a streaming response on successful completion. + */ + public abstract processRequest(request: IReceiveRequest): Promise; +} diff --git a/libraries/botframework-streaming/src/streamingRequest.ts b/libraries/botframework-streaming/src/streamingRequest.ts new file mode 100644 index 0000000000..22afcc2404 --- /dev/null +++ b/libraries/botframework-streaming/src/streamingRequest.ts @@ -0,0 +1,78 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { HttpContent, HttpContentStream } from './httpContentStream'; +import { SubscribableStream } from './subscribableStream'; + +export class StreamingRequest { + + /** + * Request verb, null on responses. + */ + public verb: string; + + /** + * Request path; null on responses. + */ + public path: string; + + /** + * List of associated streams. + */ + public streams: HttpContentStream[] = []; + + /** + * Creates a streaming request with the passed in method, path, and body. + * + * @param method The HTTP verb to use for this request. + * @param path Optional path where the resource can be found on the remote server. + * @param body Optional body to send to the remote server. + * @returns On success returns a streaming request with appropriate status code and body. + */ + public static create(method: string, path?: string, body?: HttpContent): StreamingRequest { + let request = new StreamingRequest(); + request.verb = method; + request.path = path; + if (body) { + request.setBody(body); + } + + return request; + } + + /** + * Adds a new stream attachment to this streaming request. + * + * @param content The Http content to include in the new stream attachment. + */ + public addStream(content: HttpContent): void { + if (!content) { + throw new Error('Argument Undefined Exception: content undefined.'); + } + + this.streams.push(new HttpContentStream(content)); + } + + /** + * Sets the contents of the body of this streamingRequest. + * + * @param body The JSON text to write to the body of the streamingRequest. + */ + public setBody(body: any): void { + if (typeof body === 'string') { + let stream = new SubscribableStream(); + stream.write(body, 'utf8'); + this.addStream(new HttpContent({ + type: 'application/json; charset=utf-8', + contentLength: stream.length + }, + stream)); + } else if (typeof body === 'object') { + this.addStream(body); + } + } +} diff --git a/libraries/botframework-streaming/src/streamingResponse.ts b/libraries/botframework-streaming/src/streamingResponse.ts new file mode 100644 index 0000000000..affaed9616 --- /dev/null +++ b/libraries/botframework-streaming/src/streamingResponse.ts @@ -0,0 +1,54 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { HttpContent, HttpContentStream } from './httpContentStream'; +import { SubscribableStream } from './subscribableStream'; + +export class StreamingResponse { + public statusCode: number; + public streams: HttpContentStream[] = []; + + /** + * Creates a streaming response with the passed in method, path, and body. + * + * @param statusCode The HTTP verb to use for this request. + * @param body Optional body containing additional information. + * @returns A streaming response with the appropriate statuscode and passed in body. + */ + public static create(statusCode: number, body?: HttpContent): StreamingResponse { + let response = new StreamingResponse(); + response.statusCode = statusCode; + if (body) { + response.addStream(body); + } + + return response; + } + + /** + * Adds a new stream attachment to this streaming request. + * + * @param content The Http content to include in the new stream attachment. + */ + public addStream(content: HttpContent): void { + this.streams.push(new HttpContentStream(content)); + } + + /** + * Sets the contents of the body of this streaming response. + * + * @param body The JSON text to write to the body of the streaming response. + */ + public setBody(body: any): void { + let stream = new SubscribableStream(); + stream.write(JSON.stringify(body), 'utf8'); + this.addStream(new HttpContent({ + type: 'application/json; charset=utf-8', + contentLength: stream.length + }, stream)); + } +} diff --git a/libraries/botframework-streaming/src/subscribableStream.ts b/libraries/botframework-streaming/src/subscribableStream.ts new file mode 100644 index 0000000000..9e8d5e773c --- /dev/null +++ b/libraries/botframework-streaming/src/subscribableStream.ts @@ -0,0 +1,48 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { Duplex, DuplexOptions } from 'stream'; + +export class SubscribableStream extends Duplex { + public length: number = 0; + + private readonly bufferList: Buffer[] = []; + private _onData: (chunk: any) => void; + + public constructor(options?: DuplexOptions) { + super(options); + } + + public _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void { + let buffer = Buffer.from(chunk); + this.bufferList.push(buffer); + this.length += chunk.length; + if (this._onData) { + this._onData(buffer); + } + callback(); + } + + public _read(size: number): void { + if (this.bufferList.length === 0) { + // null signals end of stream + this.push(null); + } else { + let total = 0; + while (total < size && this.bufferList.length > 0) { + let buffer = this.bufferList[0]; + this.push(buffer); + this.bufferList.splice(0, 1); + total += buffer.length; + } + } + } + + public subscribe(onData: (chunk: any) => void): void { + this._onData = onData; + } +} diff --git a/libraries/botframework-streaming/src/utilities/index.ts b/libraries/botframework-streaming/src/utilities/index.ts new file mode 100644 index 0000000000..68108e31c9 --- /dev/null +++ b/libraries/botframework-streaming/src/utilities/index.ts @@ -0,0 +1,9 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './protocol-base'; diff --git a/libraries/botframework-streaming/src/utilities/protocol-base.ts b/libraries/botframework-streaming/src/utilities/protocol-base.ts new file mode 100644 index 0000000000..e8346af231 --- /dev/null +++ b/libraries/botframework-streaming/src/utilities/protocol-base.ts @@ -0,0 +1,12 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import uuidv4 = require('uuid/v4'); + +export function generateGuid(): string { + return uuidv4(); +} diff --git a/libraries/botframework-streaming/src/webSocket/browserWebSocket.ts b/libraries/botframework-streaming/src/webSocket/browserWebSocket.ts new file mode 100644 index 0000000000..8e2b0073cd --- /dev/null +++ b/libraries/botframework-streaming/src/webSocket/browserWebSocket.ts @@ -0,0 +1,112 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { ISocket } from '../interfaces/ISocket'; + +export class BrowserWebSocket implements ISocket { + private webSocket: WebSocket; + + /** + * Creates a new instance of the [BrowserWebSocket](xref:botframework-streaming.BrowserWebSocket) class. + * + * @param socket The socket object to build this connection on. + */ + public constructor(socket?: WebSocket) { + if (socket) { + this.webSocket = socket; + } + } + + /** + * Connects to the supporting socket using WebSocket protocol. + * + * @param serverAddress The address the server is listening on. + */ + public async connect(serverAddress: string): Promise { + let resolver; + let rejector; + + if (!this.webSocket) { + this.webSocket = new WebSocket(serverAddress); + } + + this.webSocket.onerror = (e): void => { + rejector(e); + }; + + this.webSocket.onopen = (e): void => { + resolver(e); + }; + + return new Promise((resolve, reject): void => { + resolver = resolve; + rejector = reject; + }); + + } + + /** + * True if the socket is currently connected. + */ + public get isConnected(): boolean { + return this.webSocket.readyState === 1; + } + + /** + * Writes a buffer to the socket and sends it. + * + * @param buffer The buffer of data to send across the connection. + */ + public write(buffer: Buffer): void { + this.webSocket.send(buffer); + } + + /** + * Close the socket. + */ + public close(): void { + this.webSocket.close(); + } + + /** + * Set the handler for text and binary messages received on the socket. + */ + public setOnMessageHandler(handler: (x: any) => void): void { + const bufferKey: string = 'buffer'; + let packets = []; + this.webSocket.onmessage = (evt): void => { + let fileReader = new FileReader(); + let queueEntry = {buffer: null}; + packets.push(queueEntry); + fileReader.onload = (e): void => { + let t: FileReader = e.target as FileReader; + queueEntry[bufferKey] = t.result; + if (packets[0] === queueEntry) { + while(0 < packets.length && packets[0][bufferKey]) { + handler(packets[0][bufferKey]); + packets.splice(0, 1); + } + } + }; + fileReader.readAsArrayBuffer(evt.data); + }; + } + + /** + * Set the callback to call when encountering errors. + */ + public setOnErrorHandler(handler: (x: any) => void): void { + this.webSocket.onerror = (error): void => { if (error) { handler(error); } }; + } + + /** + * Set the callback to call when encountering socket closures. + */ + public setOnCloseHandler(handler: (x: any) => void): void { + this.webSocket.onclose = handler; + } +} diff --git a/libraries/botframework-streaming/src/webSocket/factories/index.ts b/libraries/botframework-streaming/src/webSocket/factories/index.ts new file mode 100644 index 0000000000..dc920f3854 --- /dev/null +++ b/libraries/botframework-streaming/src/webSocket/factories/index.ts @@ -0,0 +1,11 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './nodeWebSocketFactory'; +export * from './nodeWebSocketFactoryBase'; +export * from './wsNodeWebSocketFactory'; diff --git a/libraries/botframework-streaming/src/webSocket/factories/nodeWebSocketFactory.ts b/libraries/botframework-streaming/src/webSocket/factories/nodeWebSocketFactory.ts new file mode 100644 index 0000000000..a9f62b8eb8 --- /dev/null +++ b/libraries/botframework-streaming/src/webSocket/factories/nodeWebSocketFactory.ts @@ -0,0 +1,32 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { IncomingMessage } from 'http'; +import { Socket } from 'net'; + +import { NodeWebSocket } from '../nodeWebSocket'; +import { NodeWebSocketFactoryBase } from './nodeWebSocketFactoryBase'; + +export class NodeWebSocketFactory extends NodeWebSocketFactoryBase { + constructor() { + super(); + } + + /** + * Creates a NodeWebSocket instance. + * @param req + * @param socket + * @param head + */ + public createWebSocket(req: IncomingMessage, socket: Socket, head: Buffer): NodeWebSocket { + const s = new NodeWebSocket(); + s.create(req, socket, head); + + return s; + } +} diff --git a/libraries/botframework-streaming/src/webSocket/factories/nodeWebSocketFactoryBase.ts b/libraries/botframework-streaming/src/webSocket/factories/nodeWebSocketFactoryBase.ts new file mode 100644 index 0000000000..bd183546f7 --- /dev/null +++ b/libraries/botframework-streaming/src/webSocket/factories/nodeWebSocketFactoryBase.ts @@ -0,0 +1,15 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { IncomingMessage } from 'http'; +import { Socket } from 'net'; +import { ISocket } from '../../interfaces'; + +export abstract class NodeWebSocketFactoryBase { + public abstract createWebSocket(req: IncomingMessage, socket: Socket, head: Buffer): ISocket; +} diff --git a/libraries/botframework-streaming/src/webSocket/factories/wsNodeWebSocketFactory.ts b/libraries/botframework-streaming/src/webSocket/factories/wsNodeWebSocketFactory.ts new file mode 100644 index 0000000000..e0bf708a46 --- /dev/null +++ b/libraries/botframework-streaming/src/webSocket/factories/wsNodeWebSocketFactory.ts @@ -0,0 +1,27 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { IncomingMessage } from 'http'; +import { Socket } from 'net'; + +import { WsNodeWebSocket } from '../wsNodeWebSocket'; + +export class WsNodeWebSocketFactory { + /** + * Creates a WsNodeWebSocket instance. + * @param req + * @param socket + * @param head + */ + public async createWebSocket(req: IncomingMessage, socket: Socket, head: Buffer): Promise { + const s = new WsNodeWebSocket(); + await s.create(req, socket, head); + + return s; + } +} diff --git a/libraries/botframework-streaming/src/webSocket/index.ts b/libraries/botframework-streaming/src/webSocket/index.ts new file mode 100644 index 0000000000..7685d49f2c --- /dev/null +++ b/libraries/botframework-streaming/src/webSocket/index.ts @@ -0,0 +1,16 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './browserWebSocket'; +export * from './factories'; +export * from '../interfaces/ISocket'; +export * from './nodeWebSocket'; +export * from './webSocketClient'; +export * from './webSocketServer'; +export * from './webSocketTransport'; +export * from './wsNodeWebSocket'; diff --git a/libraries/botframework-streaming/src/webSocket/nodeWebSocket.ts b/libraries/botframework-streaming/src/webSocket/nodeWebSocket.ts new file mode 100644 index 0000000000..b68571c388 --- /dev/null +++ b/libraries/botframework-streaming/src/webSocket/nodeWebSocket.ts @@ -0,0 +1,119 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { IncomingMessage, request } from 'http'; +import { Socket } from 'net'; +import { Watershed } from 'watershed'; +import { ISocket } from '../interfaces/ISocket'; + +export class NodeWebSocket implements ISocket { + private waterShedSocket: any; + private connected: boolean; + protected watershedShed: Watershed; + + /** + * Creates a new instance of the [NodeWebSocket](xref:botframework-streaming.NodeWebSocket) class. + * + * @param socket The WaterShed socket object to build this connection on. + */ + public constructor(waterShedSocket?) { + this.waterShedSocket = waterShedSocket; + this.connected = !!waterShedSocket; + this.watershedShed = new Watershed(); + } + + /** + * Create and set a WaterShed WebSocket with an HTTP Request, Socket and Buffer. + * @param req IncomingMessage + * @param socket Socket + * @param head Buffer + */ + public create(req: IncomingMessage, socket: Socket, head: Buffer): void { + this.waterShedSocket = this.watershedShed.accept(req, socket, head); + this.connected = true; + } + + /** + * True if the socket is currently connected. + */ + public get isConnected(): boolean { + return this.connected; + } + + /** + * Writes a buffer to the socket and sends it. + * + * @param buffer The buffer of data to send across the connection. + */ + public write(buffer: Buffer): void { + this.waterShedSocket.send(buffer); + } + + /** + * Connects to the supporting socket using WebSocket protocol. + * + * @param serverAddress The address the server is listening on. + * @param port The port the server is listening on, defaults to 8082. + */ + public async connect(serverAddress, port = 8082): Promise { + // Following template from https://github.com/joyent/node-watershed#readme + const wskey = this.watershedShed.generateKey(); + const options = { + port: port, + hostname: serverAddress, + headers: { + connection: 'upgrade', + 'Sec-WebSocket-Key': wskey, + 'Sec-WebSocket-Version': '13' + } + }; + const req = request(options); + req.end(); + req.on('upgrade', function(res, socket, head): void { + this.watershedShed.connect(res, socket, head, wskey); + }); + + this.connected = true; + + return new Promise((resolve, reject): void => { + req.on('close', resolve); + req.on('error', reject); + }); + } + + /** + * Set the handler for text and binary messages received on the socket. + */ + public setOnMessageHandler(handler: (x: any) => void): void { + this.waterShedSocket.on('text', handler); + this.waterShedSocket.on('binary', handler); + } + + /** + * Close the socket. + */ + public close(): any { + this.connected = false; + + return this.waterShedSocket.end(); + } + + /** + * Set the callback to call when encountering socket closures. + */ + public setOnCloseHandler(handler: (x: any) => void): void { + this.waterShedSocket.on('end', handler); + } + + /** + * Set the callback to call when encountering errors. + */ + public setOnErrorHandler(handler: (x: any) => void): void { + this.waterShedSocket.on('error', (error): void => { if (error) { handler(error); } }); + } +} diff --git a/libraries/botframework-streaming/src/webSocket/webSocketClient.ts b/libraries/botframework-streaming/src/webSocket/webSocketClient.ts new file mode 100644 index 0000000000..aa8b34f349 --- /dev/null +++ b/libraries/botframework-streaming/src/webSocket/webSocketClient.ts @@ -0,0 +1,107 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { ProtocolAdapter } from '../protocolAdapter'; +import { RequestHandler } from '../requestHandler'; +import { StreamingRequest } from '../streamingRequest'; +import { RequestManager } from '../payloads'; +import { + PayloadReceiver, + PayloadSender, + TransportDisconnectedEventArgs +} from '../payloadTransport'; +import { BrowserWebSocket } from './BrowserWebSocket'; +import { NodeWebSocket } from './NodeWebSocket'; +import { WebSocketTransport } from './WebSocketTransport'; +import { IStreamingTransportClient, IReceiveResponse } from '../interfaces'; + +/** + * Web socket based client to be used as streaming transport. + */ +export class WebSocketClient implements IStreamingTransportClient { + private readonly _url: string; + private readonly _requestHandler: RequestHandler; + private readonly _sender: PayloadSender; + private readonly _receiver: PayloadReceiver; + private readonly _requestManager: RequestManager; + private readonly _protocolAdapter: ProtocolAdapter; + private readonly _disconnectionHandler: (message: string) => void; + + /** + * Creates a new instance of the [WebSocketClient](xref:botframework-streaming.WebSocketClient) class. + * + * @param url The URL of the remote server to connect to. + * @param requestHandler Optional [RequestHandler](xref:botframework-streaming.RequestHandler) to process incoming messages received by this server. + * @param disconnectionHandler Optional function to handle the disconnection message. + */ + public constructor({ url, requestHandler, disconnectionHandler = null}) { + this._url = url; + this._requestHandler = requestHandler; + this._disconnectionHandler = disconnectionHandler; + + this._requestManager = new RequestManager(); + + this._sender = new PayloadSender(); + this._sender.disconnected = this.onConnectionDisconnected.bind(this); + this._receiver = new PayloadReceiver(); + this._receiver.disconnected = this.onConnectionDisconnected.bind(this); + + this._protocolAdapter = new ProtocolAdapter(this._requestHandler, this._requestManager, this._sender, this._receiver); + } + + /** + * Establish a connection with no custom headers. + * + * @returns A promise that will not resolve until the client stops listening for incoming messages. + */ + public async connect(): Promise { + if (typeof WebSocket !== 'undefined') { + const ws = new BrowserWebSocket(); + await ws.connect(this._url); + const transport = new WebSocketTransport(ws); + this._sender.connect(transport); + this._receiver.connect(transport); + } else { + const ws = new NodeWebSocket(); + try { + await ws.connect(this._url); + const transport = new WebSocketTransport(ws); + this._sender.connect(transport); + this._receiver.connect(transport); + } catch (error) { + throw(new Error(`Unable to connect client to Node transport.`)); + } + } + } + + /** + * Stop this client from listening. + */ + public disconnect(): void { + this._sender.disconnect(new TransportDisconnectedEventArgs('Disconnect was called.')); + this._receiver.disconnect(new TransportDisconnectedEventArgs('Disconnect was called.')); + } + + /** + * Task used to send data over this client connection. + * + * @param request The streaming request to send. + * @returns A promise that will produce an instance of receive response on completion of the send operation. + */ + public async send(request: StreamingRequest): Promise { + return this._protocolAdapter.sendRequest(request); + } + + private onConnectionDisconnected(sender: object, args: any): void { + if (this._disconnectionHandler != null) { + this._disconnectionHandler("Disconnected"); + return; + } + + throw(new Error(`Unable to re-connect client to Node transport for url ${ this._url }. Sender: '${ sender }'. Args:' ${ args }`)); + } +} diff --git a/libraries/botframework-streaming/src/webSocket/webSocketServer.ts b/libraries/botframework-streaming/src/webSocket/webSocketServer.ts new file mode 100644 index 0000000000..b8e83c87ff --- /dev/null +++ b/libraries/botframework-streaming/src/webSocket/webSocketServer.ts @@ -0,0 +1,100 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { ProtocolAdapter } from '../protocolAdapter'; +import { RequestHandler } from '../requestHandler'; +import { StreamingRequest } from '../streamingRequest'; +import { RequestManager } from '../payloads'; +import { + PayloadReceiver, + PayloadSender, + TransportDisconnectedEventArgs +} from '../payloadTransport'; +import { ISocket } from '../interfaces/ISocket'; +import { WebSocketTransport } from './WebSocketTransport'; +import { IStreamingTransportServer, IReceiveResponse } from '../interfaces'; + +/** + * Web socket based server to be used as streaming transport. + */ +export class WebSocketServer implements IStreamingTransportServer { + private readonly _url: string; + private readonly _requestHandler: RequestHandler; + private readonly _sender: PayloadSender; + private readonly _receiver: PayloadReceiver; + private readonly _requestManager: RequestManager; + private readonly _protocolAdapter: ProtocolAdapter; + private readonly _webSocketTransport: WebSocketTransport; + private _closedSignal; + + /** + * Creates a new instance of the [WebSocketServer](xref:botframework-streaming.WebSocketServer) class. + * + * @param socket The underlying web socket. + * @param requestHandler Optional [RequestHandler](xref:botframework-streaming.RequestHandler) to process incoming messages received by this server. + */ + public constructor(socket: ISocket, requestHandler?: RequestHandler) { + this._webSocketTransport = new WebSocketTransport(socket); + this._requestHandler = requestHandler; + + this._requestManager = new RequestManager(); + + this._sender = new PayloadSender(); + this._sender.disconnected = this.onConnectionDisconnected.bind(this); + this._receiver = new PayloadReceiver(); + this._receiver.disconnected = this.onConnectionDisconnected.bind(this); + + this._protocolAdapter = new ProtocolAdapter(this._requestHandler, this._requestManager, this._sender, this._receiver); + + this._closedSignal = (x: string): string => { return x; }; + } + + /** + * Used to establish the connection used by this server and begin listening for incoming messages. + * + * @returns A promise to handle the server listen operation. This task will not resolve as long as the server is running. + */ + public async start(): Promise { + this._sender.connect(this._webSocketTransport); + this._receiver.connect(this._webSocketTransport); + + return this._closedSignal; + } + + /** + * Task used to send data over this server connection. + * + * @param request The streaming request to send. + * @returns A promise that will produce an instance of receive response on completion of the send operation. + */ + public async send(request: StreamingRequest): Promise { + return this._protocolAdapter.sendRequest(request); + } + + /** + * Stop this server. + */ + public disconnect(): void { + this._sender.disconnect(new TransportDisconnectedEventArgs('Disconnect was called.')); + this._receiver.disconnect(new TransportDisconnectedEventArgs('Disconnect was called.')); + } + + private onConnectionDisconnected(sender: PayloadReceiver | PayloadSender, e?: TransportDisconnectedEventArgs): void { + if (this._closedSignal) { + this._closedSignal('close'); + this._closedSignal = null; + } + + if (sender === this._sender) { + this._receiver.disconnect(e); + } + + if (sender === this._receiver) { + this._sender.disconnect(e); + } + } +} diff --git a/libraries/botframework-streaming/src/webSocket/webSocketTransport.ts b/libraries/botframework-streaming/src/webSocket/webSocketTransport.ts new file mode 100644 index 0000000000..3320054b4a --- /dev/null +++ b/libraries/botframework-streaming/src/webSocket/webSocketTransport.ts @@ -0,0 +1,167 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { ISocket } from '../interfaces'; +import { ITransportSender } from '../interfaces/ITransportSender'; +import { ITransportReceiver } from '../interfaces/ITransportReceiver'; + +/** + * Web socket based transport. + */ +export class WebSocketTransport implements ITransportSender, ITransportReceiver { + private _socket: ISocket; + private readonly _queue: Buffer[]; + private _active: Buffer; + private _activeOffset: number; + private _activeReceiveResolve: (resolve: Buffer) => void; + private _activeReceiveReject: (reason?: any) => void; + private _activeReceiveCount: number; + + /** + * Creates a new instance of the [WebSocketTransport](xref:botframework-streaming.WebSocketTransport) class. + * + * @param ws The ISocket to build this transport on top of. + */ + public constructor(ws: ISocket) { + this._socket = ws; + this._queue = []; + this._activeOffset = 0; + this._activeReceiveCount = 0; + this._socket.setOnMessageHandler((data): void => { + this.onReceive(data); + }); + this._socket.setOnErrorHandler((err): void => { + this.onError(err); + }); + this._socket.setOnCloseHandler((): void => { + this.onClose(); + }); + } + + /** + * Sends the given buffer out over the socket's connection. + * + * @param buffer The buffered data to send out over the connection. + */ + public send(buffer: Buffer): number { + if (this._socket && this._socket.isConnected) { + this._socket.write(buffer); + + return buffer.length; + } + + return 0; + } + + /** + * Returns true if the transport is connected to a socket. + */ + public isConnected(): boolean { + return this._socket.isConnected; + } + + /** + * Close the socket this transport is connected to. + */ + public close(): void { + if (this._socket && this._socket.isConnected) { + this._socket.close(); + } + } + + /** + * Attempt to receive incoming data from the connected socket. + * + * @param count The number of bytes to attempt to receive. + * @returns A buffer populated with the received data. + */ + public async receive(count: number): Promise { + if (this._activeReceiveResolve) { + throw new Error('Cannot call receiveAsync more than once before it has returned.'); + } + + this._activeReceiveCount = count; + + let promise = new Promise((resolve, reject): void => { + this._activeReceiveResolve = resolve; + this._activeReceiveReject = reject; + }); + + this.trySignalData(); + + return promise; + } + + /** + * Sets the transport to attempt to receive incoming data that has not yet arrived. + * + * @param data A buffer to store incoming data in. + */ + public onReceive(data: Buffer): void { + if (this._queue && data && data.byteLength > 0) { + this._queue.push(Buffer.from(data)); + this.trySignalData(); + } + } + + private onClose(): void { + if (this._activeReceiveReject) { + this._activeReceiveReject(new Error('Socket was closed.')); + } + + this._active = null; + this._activeOffset = 0; + this._activeReceiveResolve = null; + this._activeReceiveReject = null; + this._activeReceiveCount = 0; + this._socket = null; + } + + private onError(err: Error): void { + if (this._activeReceiveReject) { + this._activeReceiveReject(err); + } + this.onClose(); + } + + private trySignalData(): void { + if (this._activeReceiveResolve) { + if (!this._active && this._queue.length > 0) { + this._active = this._queue.shift(); + this._activeOffset = 0; + } + + if (this._active) { + if (this._activeOffset === 0 && this._active.length === this._activeReceiveCount) { + // can send the entire _active buffer + let buffer = this._active; + this._active = null; + + this._activeReceiveResolve(buffer); + } else { + // create a Buffer.from and copy some of the contents into it + let available = Math.min(this._activeReceiveCount, this._active.length - this._activeOffset); + let buffer = Buffer.alloc(available); + this._active.copy(buffer, 0, this._activeOffset, this._activeOffset + available); + this._activeOffset += available; + + // if we used all of active, set it to undefined + if (this._activeOffset >= this._active.length) { + this._active = null; + this._activeOffset = 0; + } + + this._activeReceiveResolve(buffer); + } + + this._activeReceiveCount = 0; + this._activeReceiveReject = null; + this._activeReceiveResolve = null; + } + } + } +} diff --git a/libraries/botframework-streaming/src/webSocket/wsNodeWebSocket.ts b/libraries/botframework-streaming/src/webSocket/wsNodeWebSocket.ts new file mode 100644 index 0000000000..854cee833e --- /dev/null +++ b/libraries/botframework-streaming/src/webSocket/wsNodeWebSocket.ts @@ -0,0 +1,139 @@ +/** + * @module botframework-streaming + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ISocket } from '../interfaces'; +import { IncomingMessage, request } from 'http'; +import { Socket } from 'net'; +import * as WebSocket from 'ws'; +import * as crypto from 'crypto'; + +// Taken from watershed, these needs to be investigated. +const NONCE_LENGTH = 16; + +export class WsNodeWebSocket implements ISocket { + private wsSocket: WebSocket; + private connected: boolean; + protected wsServer: WebSocket.Server; + + /** + * Creates a new instance of the [WsNodeWebSocket](xref:botframework-streaming.WsNodeWebSocket) class. + * + * @param socket The ws socket object to build this connection on. + */ + public constructor(wsSocket?: WebSocket) { + this.wsSocket = wsSocket; + this.connected = !!wsSocket; + this.wsServer = new WebSocket.Server({ noServer: true }); + } + + /** + * Create and set a `ws` WebSocket with an HTTP Request, Socket and Buffer. + * @param req IncomingMessage + * @param socket Socket + * @param head Buffer + */ + public async create(req: IncomingMessage, socket: Socket, head: Buffer): Promise { + return new Promise((resolve, reject) => { + try { + this.wsServer.handleUpgrade(req, socket, head, (websocket) => { + this.wsSocket = websocket; + this.connected = true; + resolve(); + }); + } catch (err) { + reject(err); + } + }); + } + + /** + * True if the socket is currently connected. + */ + public get isConnected(): boolean { + return this.connected; + } + + /** + * Writes a buffer to the socket and sends it. + * + * @param buffer The buffer of data to send across the connection. + */ + public write(buffer: Buffer): void { + this.wsSocket.send(buffer); + } + + /** + * Connects to the supporting socket using WebSocket protocol. + * + * @param serverAddress The address the server is listening on. + * @param port The port the server is listening on, defaults to 8082. + */ + public async connect(serverAddress, port = 8082): Promise { + // Taken from WaterShed, this needs to be investigated. + const wskey = crypto.randomBytes(NONCE_LENGTH).toString('base64'); + const options = { + port: port, + hostname: serverAddress, + headers: { + connection: 'upgrade', + 'Sec-WebSocket-Key': wskey, + 'Sec-WebSocket-Version': '13' + } + }; + const req = request(options); + req.end(); + req.on('upgrade', (res, socket, head): void => { + // @types/ws does not contain the signature for completeUpgrade + // https://github.com/websockets/ws/blob/0a612364e69fc07624b8010c6873f7766743a8e3/lib/websocket-server.js#L269 + (this.wsServer as any).completeUpgrade(wskey, undefined, res, socket, head, (websocket): void => { + this.wsSocket = websocket; + this.connected = true; + }); + }); + + return new Promise((resolve, reject): void => { + req.on('close', resolve); + req.on('error', reject); + }); + } + + /** + * Set the handler for `'data'` and `'message'` events received on the socket. + */ + public setOnMessageHandler(handler: (x: any) => void): void { + this.wsSocket.on('data', handler); + this.wsSocket.on('message', handler); + } + + /** + * Close the socket. + * @remarks + * Optionally pass in a status code and string explaining why the connection is closing. + * @param code + * @param data + */ + public close(code?: number, data?: string): void { + this.connected = false; + + return this.wsSocket.close(code, data); + } + + /** + * Set the callback to call when encountering socket closures. + */ + public setOnCloseHandler(handler: (x: any) => void): void { + this.wsSocket.on('close', handler); + } + + /** + * Set the callback to call when encountering errors. + */ + public setOnErrorHandler(handler: (x: any) => void): void { + this.wsSocket.on('error', (error): void => { if (error) { handler(error); } }); + } +} \ No newline at end of file diff --git a/libraries/botframework-streaming/tests/Assembler.test.js b/libraries/botframework-streaming/tests/Assembler.test.js new file mode 100644 index 0000000000..4f778cfcb2 --- /dev/null +++ b/libraries/botframework-streaming/tests/Assembler.test.js @@ -0,0 +1,168 @@ +const SubscribableStream = require('../lib/subscribableStream'); +const chai = require('chai'); +const StreamManager = require('../lib/payloads/streamManager'); +const PayloadTypes = require('../lib/payloads/payloadTypes'); +const PayloadAssembler = require('../lib/assemblers/payloadAssembler'); +const PayloadAssemblerManager = require('../lib/payloads/payloadAssemblerManager'); +var expect = chai.expect; + +describe('ReceiveRequestAssembler', () => { + +}); + +describe('PayloadAssembler', () => { + it('constructs correctly.', () => { + let header = {payloadType: PayloadTypes.PayloadTypes.request, payloadLength: '42', id: '100', end: true}; + let sm = new StreamManager.StreamManager(); + let rra = new PayloadAssembler.PayloadAssembler(sm, {header: header}); + + expect(rra.id).equals('100'); + expect(rra._streamManager).equals(sm); + }); + + it('closes without throwing', () => { + let header = {payloadType: PayloadTypes.PayloadTypes.request, payloadLength: '42', id: '100', end: true}; + let sm = new StreamManager.StreamManager(); + + let rra = new PayloadAssembler.PayloadAssembler(sm, {header: header}); + + expect(() => {rra.close();}).to.not.throw; + }); + + it('returns a new stream.', () => { + let header = {payloadType: PayloadTypes.PayloadTypes.response, payloadLength: '42', id: '100', end: true}; + let sm = new StreamManager.StreamManager(); + + let rra = new PayloadAssembler.PayloadAssembler(sm, {header: header}); + + expect(rra.createPayloadStream()).to.be.instanceOf(SubscribableStream.SubscribableStream); + }); + + it('processes a Request without throwing.', (done) => { + let header = {payloadType: PayloadTypes.PayloadTypes.request, payloadLength: '5', id: '100', end: true}; + let sm = new StreamManager.StreamManager(); + let s = new SubscribableStream.SubscribableStream(); + s.write('12345'); + let rp = {verb: 'POST', path: '/some/path'}; + rp.streams = s; + let rra = new PayloadAssembler.PayloadAssembler(sm, {header: header, onCompleted: function() {done();} }); + rra.onReceive(header, s, 5); + rra.close(); + }); + + it('processes a Response without throwing.', (done) => { + let header = {payloadType: PayloadTypes.PayloadTypes.response, payloadLength: '5', id: '100', end: true}; + let sm = new StreamManager.StreamManager(); + let s = new SubscribableStream.SubscribableStream(); + s.write('12345'); + let rp = {statusCode: 200}; + rp.streams = s; + let rra = new PayloadAssembler.PayloadAssembler(sm, {header: header, onCompleted: function() {done();} }); + rra.onReceive(header, s, 5); + }); + + it('assigns values when constructed', () => { + let header = {payloadType: PayloadTypes.PayloadTypes.stream, payloadLength: 50, id: '1', end: undefined}; + let csa = new PayloadAssembler.PayloadAssembler(new StreamManager.StreamManager(), {header: header}); + expect(csa.id) + .equals('1'); + expect(csa.contentLength) + .equals(50); + expect(csa.payloadType) + .equals('S'); + expect(csa.end) + .equals(undefined); + }); + + it('returns a Stream', () => { + let header = {payloadType: PayloadTypes.PayloadTypes.stream, payloadLength: 50, id: '1', end: true}; + let csa = new PayloadAssembler.PayloadAssembler(new StreamManager.StreamManager(), {header: header}); + expect(csa.createPayloadStream()) + .instanceOf(SubscribableStream.SubscribableStream); + }); + + it('closes a Stream', () => { + let header = {payloadType: PayloadTypes.PayloadTypes.stream, payloadLength: 50, id: '1', end: true}; + let csa = new PayloadAssembler.PayloadAssembler(new StreamManager.StreamManager(), {header: header}); + expect(csa.createPayloadStream()) + .instanceOf(SubscribableStream.SubscribableStream); + expect(csa.close()).to.not.throw; + }); +}); + +describe('PayloadAssemblerManager', () => { + it('cretes a response stream', (done) => { + let p = new PayloadAssemblerManager.PayloadAssemblerManager(new StreamManager.StreamManager(), () => done(), () => done()); + let head = {payloadType: PayloadTypes.PayloadTypes.response, payloadLength: '42', id: '100', end: true}; + expect(p.getPayloadStream(head)).to.be.instanceOf(SubscribableStream.SubscribableStream); + done(); + }); + + + it('cretes a request stream', (done) => { + let p = new PayloadAssemblerManager.PayloadAssemblerManager(new StreamManager.StreamManager(), () => done(), () => done()); + let head = {payloadType: PayloadTypes.PayloadTypes.request, payloadLength: '42', id: '100', end: true}; + expect(p.getPayloadStream(head)).to.be.instanceOf(SubscribableStream.SubscribableStream); + done(); + }); + + it('does not throw when receiving a request', (done) => { + let p = new PayloadAssemblerManager.PayloadAssemblerManager(new StreamManager.StreamManager(), () => done(), () => done()); + let head = {payloadType: PayloadTypes.PayloadTypes.request, payloadLength: '42', id: '100', end: true}; + let s = p.getPayloadStream(head); + expect(s).to.be.instanceOf(SubscribableStream.SubscribableStream); + expect(p.onReceive(head, s, 0)).to.not.throw; + done(); + }); + + it('does not throw when receiving a stream', (done) => { + let p = new PayloadAssemblerManager.PayloadAssemblerManager(new StreamManager.StreamManager(), () => done(), () => done()); + let head = {payloadType: PayloadTypes.PayloadTypes.stream, payloadLength: '42', id: '100', end: true}; + let s = p.getPayloadStream(head); + expect(s).to.be.instanceOf(SubscribableStream.SubscribableStream); + expect(p.onReceive(head, s, 0)).to.not.throw; + done(); + }); + + it('does not throw when receiving a response', (done) => { + let p = new PayloadAssemblerManager.PayloadAssemblerManager(new StreamManager.StreamManager(), () => done(), () => done()); + let head = {payloadType: PayloadTypes.PayloadTypes.response, payloadLength: '42', id: '100', end: true}; + let s = p.getPayloadStream(head); + expect(s).to.be.instanceOf(SubscribableStream.SubscribableStream); + expect(p.onReceive(head, s, 0)).to.not.throw; + done(); + }); + + it('returns undefined when asked to create an existing stream', (done) => { + let p = new PayloadAssemblerManager.PayloadAssemblerManager(new StreamManager.StreamManager(), () => done(), () => done()); + let head = {payloadType: PayloadTypes.PayloadTypes.request, payloadLength: '42', id: '100', end: true}; + let s = p.getPayloadStream(head); + expect(s).to.be.instanceOf(SubscribableStream.SubscribableStream); + expect(p.getPayloadStream(head)).to.be.undefined; + done(); + }); + + it('throws if not given an ID', () => { + let header = {payloadType: PayloadTypes.PayloadTypes.request, payloadLength: '5', id: undefined, end: true}; + let sm = new StreamManager.StreamManager(); + try{ + let rra = new PayloadAssembler.PayloadAssembler(sm, {header: header, onCompleted: function() {done();} }); + } catch(result) { + expect(result.message).to.equal('An ID must be supplied when creating an assembler.'); + } + }); + + it('processes a response with streams without throwing.', (done) => { + let header = {payloadType: PayloadTypes.PayloadTypes.response, payloadLength: '5', id: '100', end: true}; + let sm = new StreamManager.StreamManager(); + let s = new SubscribableStream.SubscribableStream(); + s.write('{"statusCode": "12345","streams": [{"id": "1","contentType": "text","length": "2"},{"id": "2","contentType": "text","length": "2"},{"id": "3","contentType": "text","length": "2"}]}'); + let rp = {verb: 'POST', path: '/some/path'}; + rp.streams = []; + rp.streams.push(s); + + let rra = new PayloadAssembler.PayloadAssembler(sm, {header: header, onCompleted: function() {done();} }); + rra.onReceive(header, s, 5); + rra.close(); + }); +}); diff --git a/libraries/botframework-streaming/tests/ContentStream.test.js b/libraries/botframework-streaming/tests/ContentStream.test.js new file mode 100644 index 0000000000..e826980e24 --- /dev/null +++ b/libraries/botframework-streaming/tests/ContentStream.test.js @@ -0,0 +1,147 @@ +const ContentStream = require('../lib/contentStream'); +const PayloadAssembler = require('../lib/assemblers/payloadAssembler'); +const chai = require('chai'); +const StreamManager = require('../lib/payloads/streamManager'); +const SubscribableStream = require('../lib/subscribableStream'); +const PayloadTypes = require('../lib/payloads/payloadTypes'); +const protocol = require('../lib'); +var expect = chai.expect; + +class TestPayloadAssembler{ + constructor(content){ + this.stream1 = new SubscribableStream.SubscribableStream(); + if(content){ + this.stream1.write(content); + } else { + this.stream1.write('hello'); + } + + this.contentType = 'application/text'; + this.contentLength = 5; + } + + getPayloadStream(){ + return this.stream1; + } + + close(){} +} + +describe('Streaming Extensions ContentStream Tests ', () => { + it('assigns ID when constructed', () => { + let csa = new PayloadAssembler.PayloadAssembler(new StreamManager.StreamManager(), {id:'csa1'}); + let cs = new ContentStream.ContentStream('1', csa); + + expect(cs.id) + .equal('1'); + }); + + it('throws if no assembler is passed in on construction', () => { + expect(() => new ContentStream.ContentStream('1', undefined)) + .throws('Null Argument Exception'); + }); + + it('can return payload type', () => { + let header = {payloadType: PayloadTypes.PayloadTypes.stream, payloadLength: 42, id: '68e999ca-a651-40f4-ad8f-3aaf781862b4', end: true}; + let cs = new ContentStream.ContentStream('1', new PayloadAssembler.PayloadAssembler(new StreamManager.StreamManager(), {id:'csa1', header: header})); + + expect(cs.contentType) + .equal('S'); + }); + + it('can return length', () => { + let header = {payloadType: PayloadTypes.PayloadTypes.stream, payloadLength: 42, id: '68e999ca-a651-40f4-ad8f-3aaf781862b4', end: true}; + let cs = new ContentStream.ContentStream('1', new PayloadAssembler.PayloadAssembler(new StreamManager.StreamManager(), {id:'csa1', header: header})); + + expect(cs.length) + .equal(42); + }); + + it('can return ID', () => { + let header = {payloadType: PayloadTypes.PayloadTypes.stream, payloadLength: 42, id: '68e999ca-a651-40f4-ad8f-3aaf781862b4', end: true}; + let cs = new ContentStream.ContentStream('1', new PayloadAssembler.PayloadAssembler(new StreamManager.StreamManager(), {id:'csa1', header: header})); + + expect(cs.id) + .equal('1'); + }); + + it('does not return the stream when it is is undefined', () => { + let header = {PayloadType: PayloadTypes.PayloadTypes.stream, payloadLength: 42, id: '68e999ca-a651-40f4-ad8f-3aaf781862b4', end: true}; + let cs = new ContentStream.ContentStream('1', new PayloadAssembler.PayloadAssembler(new StreamManager.StreamManager(), {id:'csa1', header: header})); + + expect(cs.getStream()) + .to + .not + .be + .undefined; + }); + + it('reads a stream of length 0 and returns an empty string', () => { + let header = {payloadType: PayloadTypes.PayloadTypes.stream, payloadLength: 0, id: '68e999ca-a651-40f4-ad8f-3aaf781862b4', end: true}; + let cs = new ContentStream.ContentStream('1', new PayloadAssembler.PayloadAssembler(new StreamManager.StreamManager(), {id:'csa1', header: header})); + + return cs.readAsString() + .then(data => { + expect(data) + .equal(''); + }); + }); + + it('throws when reading an empty stream as JSON', () => { + let header = {payloadType: PayloadTypes.PayloadTypes.stream, PayloadLength: 0, id: '68e999ca-a651-40f4-ad8f-3aaf781862b4', end: true}; + let cs = new ContentStream.ContentStream('1', new PayloadAssembler.PayloadAssembler(new StreamManager.StreamManager(), {id:'csa1', header: header})); + + return cs.readAsJson() + .then(data => { + expect(data) + .to + .not + .be + .undefined; + }) + .catch(err => { + expect(err.toString()) + .to + .equal('SyntaxError: Unexpected end of JSON input'); + }); + }); + + it('reads a stream as a string', () => { + let cs = new ContentStream.ContentStream('cs1', new TestPayloadAssembler()); + let result = cs.readAsString(); + + result.then(function(data) { + expect(data).to.equal('hello'); + + }); + }); + + it('reads a stream as a json', () => { + let cs = new ContentStream.ContentStream('cs1', new TestPayloadAssembler('{"message":"hello"}')); + let result = cs.readAsJson(); + + result.then(function(data) { + expect(data.message).to.equal('hello'); + + }); + }); + + it('reads a stream before receiving all the bits', () => { + let tcsa = new TestPayloadAssembler(); + tcsa.contentLength = 10; + let cs = new ContentStream.ContentStream('cs1', tcsa); + let result = cs.readAsString(); + + result.then(function(data) { + expect(data).to.equal('hello'); + + }); + }); + + it('can cancel', () => { + let cs = new ContentStream.ContentStream('cs1', new TestPayloadAssembler()); + let result = cs.readAsString(); + + expect(cs.cancel()).to.not.throw; + }); +}); diff --git a/libraries/botframework-streaming/tests/Disassembler.test.js b/libraries/botframework-streaming/tests/Disassembler.test.js new file mode 100644 index 0000000000..6e47578aff --- /dev/null +++ b/libraries/botframework-streaming/tests/Disassembler.test.js @@ -0,0 +1,54 @@ +const Disassemblers = require('../lib/disassemblers/requestDisassembler'); +const PayloadSender = require('../lib/payloadTransport/payloadSender'); +const Request = require('../lib/streamingRequest'); +const HttpContentStream = require('../lib/httpContentStream'); +const Stream = require('../lib/subscribableStream'); +const CancelDisassembler = require('../lib/disassemblers/cancelDisassembler'); +const PayloadTypes = require('../lib/payloads/payloadTypes'); +const chai = require('chai'); +var expect = chai.expect; + +describe('RequestDisassembler', () => { + + it('resolves calls to get stream.', (done) => { + let sender = new PayloadSender.PayloadSender(); + let req = new Request.StreamingRequest(); + let headers = {contentLength: 40, contentType: 'A'}; + let stream = new Stream.SubscribableStream(); + stream.write('This is the data inside of the stream.', 'UTF-8'); + let content = new HttpContentStream.HttpContent(headers, stream); + let contentStream = new HttpContentStream.HttpContentStream(content); + contentStream.content.headers = headers; + + req.addStream(contentStream); + let rd = new Disassemblers.RequestDisassembler(sender,'42', req); + + let result = rd.getStream() + .then(done()); + }); +}); + +describe('CancelDisassembler', () => { + + it('constructs correctly.', () => { + let sender = new PayloadSender.PayloadSender(); + let payloadType = PayloadTypes.PayloadTypes.cancelStream; + let cd = new CancelDisassembler.CancelDisassembler(sender, '42', payloadType); + + expect(cd.id).to.equal('42'); + expect(cd.payloadType).to.equal(payloadType); + expect(cd.sender).to.equal(sender); + }); + + it('sends payload without throwing.', () => { + let sender = new PayloadSender.PayloadSender(); + let payloadType = PayloadTypes.PayloadTypes.cancelStream; + let cd = new CancelDisassembler.CancelDisassembler(sender, '42', payloadType); + + expect(cd.id).to.equal('42'); + expect(cd.payloadType).to.equal(payloadType); + expect(cd.sender).to.equal(sender); + + expect(cd.disassemble()).to.not.throw; + }); +}); diff --git a/libraries/botframework-streaming/tests/HeaderSerializer.test.js b/libraries/botframework-streaming/tests/HeaderSerializer.test.js new file mode 100644 index 0000000000..247ba25714 --- /dev/null +++ b/libraries/botframework-streaming/tests/HeaderSerializer.test.js @@ -0,0 +1,133 @@ +const chai = require( 'chai'); +const HeaderSerializer = require( '../lib/payloads/headerSerializer'); +const PayloadTypes = require( '../lib/payloads/payloadTypes'); +const PayloadConstants = require( '../lib/payloads/payloadConstants'); +var expect = chai.expect; + +describe('HeaderSerializer', () => { + + it('serializes and deserializes correctly', () => { + let header = {payloadType: PayloadTypes.PayloadTypes.request, payloadLength: 168, id: '68e999ca-a651-40f4-ad8f-3aaf781862b4', end: true}; + let buffer = Buffer.alloc(Number(PayloadConstants.PayloadConstants.MaxHeaderLength)); + + HeaderSerializer.HeaderSerializer.serialize(header, buffer); + + let result = HeaderSerializer.HeaderSerializer.deserialize(buffer); + + expect(result) + .to + .deep + .equal(header); + }); + + it('can parse an ASCII header', () => { + let buffer = Buffer.alloc(Number(PayloadConstants.PayloadConstants.MaxHeaderLength)); + buffer.write('A.000168.68e999ca-a651-40f4-ad8f-3aaf781862b4.1\n'); + + let result = HeaderSerializer.HeaderSerializer.deserialize(buffer); + expect(result.payloadType) + .equal('A'); + expect(result.payloadLength) + .equal(168); + expect(result.id) + .equal('68e999ca-a651-40f4-ad8f-3aaf781862b4'); + expect(result.end) + .equal(true); + }); + + it('deserializes unknown types', () => { + let buffer = Buffer.alloc(Number(PayloadConstants.PayloadConstants.MaxHeaderLength)); + buffer.write('Z.000168.68e999ca-a651-40f4-ad8f-3aaf781862b4.1\n'); + let result = HeaderSerializer.HeaderSerializer.deserialize(buffer); + + expect(result.payloadType) + .equal('Z'); + }); + + it('throws if the header is missing a part', () => { + // expect.assertions(1); + let buffer = Buffer.alloc(Number(PayloadConstants.PayloadConstants.MaxHeaderLength)); + buffer.write('A.000168.68e999ca-a651-40f4-ad8f-3aaf781862b4\n'); + + expect(() => HeaderSerializer.HeaderSerializer.deserialize(buffer)) + .throws('Cannot parse header, header is malformed.'); + }); + + it('throws if the header has too many parts', () => { + // expect.assertions(1); + let buffer = Buffer.alloc(Number(PayloadConstants.PayloadConstants.MaxHeaderLength)); + buffer.write('A.000168.68e999ca-a651-40f4-ad8f-3aaf781862b4.1.2\n'); + + expect(() => HeaderSerializer.HeaderSerializer.deserialize(buffer)) + .throws('Cannot parse header, header is malformed.'); + }); + + it('throws if the header type is too long', () => { + // expect.assertions(1); + let buffer = Buffer.alloc(Number(PayloadConstants.PayloadConstants.MaxHeaderLength)); + buffer.write('ABCDE.000168.68e999ca-a651-40f4-ad8f-3aaf7b4.1\n'); + + expect(() => HeaderSerializer.HeaderSerializer.deserialize(buffer)) + .throws('Header type \'5\' is missing or malformed.'); + }); + + it('throws if the header length is malformed', () => { + // expect.assertions(1); + let buffer = Buffer.alloc(Number(PayloadConstants.PayloadConstants.MaxHeaderLength)); + buffer.write('A.00b168.68e999ca-a651-40f4-ad8f-3aaf781862b4.1\n'); + + expect(() => HeaderSerializer.HeaderSerializer.deserialize(buffer)) + .throws('Header length of NaN is missing or malformed'); + }); + + it('throws if the header length is too small', () => { + // expect.assertions(1); + let buffer = Buffer.alloc(Number(PayloadConstants.PayloadConstants.MaxHeaderLength)); + buffer.write('A.-100000.68e999ca-a651-40f4-ad8f-3aaf781862b4.1\n'); + + expect(() => HeaderSerializer.HeaderSerializer.deserialize(buffer)) + .throws('Header length of -100000 is missing or malformed'); + }); + + it('throws if the header length is too big', () => { + // expect.assertions(1); + let buffer = Buffer.alloc(Number(PayloadConstants.PayloadConstants.MaxHeaderLength)); + buffer.write('A.1111111.8e999ca-a651-40f4-ad8f-3aaf781862b4.1\n'); + + expect(() => HeaderSerializer.HeaderSerializer.deserialize(buffer)) + .throws('Header length of 1111111 is missing or malformed'); + }); + + it('throws if the header terminator is malformed', () => { + // expect.assertions(1); + let buffer = Buffer.alloc(Number(PayloadConstants.PayloadConstants.MaxHeaderLength)); + buffer.write('A.000168.68e999ca-a651-40f4-ad8f-3aaf781862b4.2\n'); + + expect(() => HeaderSerializer.HeaderSerializer.deserialize(buffer)) + .throws('Header End is missing or not a valid value.'); + }); + + it('throws if the header ID is malformed', () => { + // expect.assertions(1); + const headerId = '68e9p9ca-a651-40f4-ad8f-3aaf781862b4'; + const header = `A.000168.${ headerId }.1\n`; + + let buffer = Buffer.alloc(Number(PayloadConstants.PayloadConstants.MaxHeaderLength)); + buffer.write(header); + + expect(() => HeaderSerializer.HeaderSerializer.deserialize(buffer)) + .throws(`Header ID \'${ headerId }\' is missing or malformed.`); + }); + + it('accepts nil GUIDs as valid header IDs', () => { + let buffer = Buffer.alloc(Number(PayloadConstants.PayloadConstants.MaxHeaderLength)); + buffer.write('A.000168.00000000-0000-0000-0000-000000000000.1\n'); + + let result = HeaderSerializer.HeaderSerializer.deserialize(buffer); + + expect(result.id) + .to + .deep + .equal('00000000-0000-0000-0000-000000000000'); + }); +}); diff --git a/libraries/botframework-streaming/tests/NamedPipe.test.js b/libraries/botframework-streaming/tests/NamedPipe.test.js new file mode 100644 index 0000000000..5d59f2daae --- /dev/null +++ b/libraries/botframework-streaming/tests/NamedPipe.test.js @@ -0,0 +1,402 @@ +const net = require('net'); +const np = require('../lib'); +const npt = require('../lib/namedPipe/namedPipeTransport'); +const protocol = require('../lib'); +const chai = require('chai'); +var expect = chai.expect; + +class FauxSock{ + constructor(contentString){ + if(contentString){ + this.contentString = contentString; + this.position = 0; + } + this.connecting = false; + this.exists = true; + } + + write(buffer){ + this.buffer = buffer; + } + + send(buffer){ + return buffer.length; + }; + + receive(readLength){ + if(this.contentString[this.position]) + { + this.buff = Buffer.from(this.contentString[this.position]); + this.position++; + + return this.buff.slice(0, readLength); + } + + if(this.receiver.isConnected) + this.receiver.disconnect(); + } + close(){}; + end(){ + this.exists = false; + }; + destroyed(){ + return this.exists; + }; + + setReceiver(receiver){ + this.receiver = receiver; + } + + on(action, handler){ + if(action === 'error'){ + this.errorHandler = handler; + } + if(action === 'data'){ + this.messageHandler = handler; + } + if(action === 'close'){ + this.closeHandler = handler; + } + + }; +} +class TestServer { + constructor(baseName) { + let _baseName = undefined; + let _server = undefined; + let transport = undefined; + + this._baseName = baseName; + } + + connect() { + let pipeName = npt.NamedPipeTransport.PipePath + this._baseName; + + let connectResolve = undefined; + + let result = new Promise((resolve, reject) => { + connectResolve = resolve; + }); + + this._server = net.createServer(() => { + this.transport = new npt.NamedPipeTransport(new FauxSock , pipeName); + connectResolve(); + }); + this._server.listen(pipeName); + + return result; + } + + disconnect() { + if (this.transport) { + this.transport.close(); + this.transport = undefined; + } + + if (this._server) { + this._server.close(); + this._server = undefined; + } + } +} + +class TestClient { + + + constructor(baseName) { + let _baseName = undefined; + let transport = undefined; + + this._baseName = baseName; + } + + connect() { + let pipeName = npt.NamedPipeTransport.PipePath + this._baseName; + + let socket = new FauxSock; + this.transport = new npt.NamedPipeTransport(socket, ''); + + return Promise.resolve(); + } + + disconnect() { + if (this.transport) { + this.transport.close(); + this.transport = undefined; + } + } +} + +function connect(s, c) { + var p = new Promise((resolve, reject) => { + var clientConnected = false; + var serverConnected = false; + + s.connect().then(() => { + serverConnected = true; + if (clientConnected && serverConnected) { + resolve(true); + } + }); + + c.connect().then(() => { + clientConnected = true; + if (clientConnected && serverConnected) { + resolve(true); + } + }); + }); + + return p; +} + +describe('Streaming Extensions NamedPipe Library Tests', () => { + describe('NamedPipe Transport Tests', () => { + it('Client connect', () => { + let pipeName = 't1'; + let c = new TestClient(pipeName); + let t = c.connect(); + expect(t).to.not.be.undefined; + c.disconnect(); + }); + + it('Client cannot send while connecting', async (done) => { + let pipeName = 't1'; + let c = new TestClient(pipeName); + c.connect(); + + var b = Buffer.from('12345', 'utf8'); + + let count = c.transport.send(b); + + expect(count).to.equal(0); + + c.disconnect(); + done(); + }); + + it('creates a new transport', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new npt.NamedPipeTransport(sock, 'fakeSocket1'); + expect(transport).to.be.instanceOf(npt.NamedPipeTransport); + expect( () => transport.close()).to.not.throw; + }); + + it('creates a new transport and connects', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new npt.NamedPipeTransport(sock, 'fakeSocket2'); + expect(transport).to.be.instanceOf(npt.NamedPipeTransport); + expect(transport.isConnected()).to.be.true; + expect( () => transport.close()).to.not.throw; + }); + + it('closes the transport without throwing', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new npt.NamedPipeTransport(sock, 'fakeSocket3'); + expect(transport).to.be.instanceOf(npt.NamedPipeTransport); + expect( transport.close()).to.not.throw; + let exists = transport.isConnected(); + expect(exists).to.be.false; + }); + + it('writes to the socket', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new npt.NamedPipeTransport(sock, 'fakeSocket4'); + expect(transport).to.be.instanceOf(npt.NamedPipeTransport); + expect(transport.isConnected()).to.be.true; + let buff = Buffer.from('hello', 'utf8'); + let sent = transport.send(buff); + expect(sent).to.equal(5); + expect( () => transport.close()).to.not.throw; + }); + + it('returns 0 when attepmting to write to a closed socket', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new npt.NamedPipeTransport(sock, 'fakeSocket5'); + expect(transport).to.be.instanceOf(npt.NamedPipeTransport); + expect(transport.isConnected()).to.be.true; + sock.writable = false; + let buff = Buffer.from('hello', 'utf8'); + let sent = transport.send(buff); + expect(sent).to.equal(0); + expect( () => transport.close()).to.not.throw; + }); + + it('throws when reading from a dead socket', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new npt.NamedPipeTransport(sock, 'fakeSocket5'); + expect(transport).to.be.instanceOf(npt.NamedPipeTransport); + expect(transport.isConnected()).to.be.true; + expect(transport.receive(5)).to.throw; + expect( () => transport.close()).to.not.throw; + }); + + it('can read from the socket', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new npt.NamedPipeTransport(sock); + expect(transport).to.be.instanceOf(npt.NamedPipeTransport); + expect(transport.isConnected()).to.be.true; + transport.receive(12).catch(); + transport.socketReceive(Buffer.from('Hello World!', 'utf8')); + + expect( () => transport.close()).to.not.throw; + }); + + + it('cleans up when onClose is fired', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new npt.NamedPipeTransport(sock, 'fakeSocket6'); + expect(transport).to.be.instanceOf(npt.NamedPipeTransport); + expect(transport.isConnected()).to.be.true; + transport.socketClose(); + expect(transport._active).to.be.null; + expect(transport._activeReceiveResolve).to.be.null; + expect(transport._activeReceiveReject).to.be.null; + expect(transport._socket).to.be.null; + expect(transport._activeOffset).to.equal(0); + expect(transport._activeReceiveCount).to.equal(0); + }); + + it('cleans up when socketError is fired', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new npt.NamedPipeTransport(sock, 'fakeSocket6'); + expect(transport).to.be.instanceOf(npt.NamedPipeTransport); + expect(transport.isConnected()).to.be.true; + transport.socketError(); + expect(transport._active).to.be.null; + expect(transport._activeReceiveResolve).to.be.null; + expect(transport._activeReceiveReject).to.be.null; + expect(transport._socket).to.be.null; + expect(transport._activeOffset).to.equal(0); + expect(transport._activeReceiveCount).to.equal(0); + }); + + it('does not throw when socketReceive is fired', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new npt.NamedPipeTransport(sock, 'fakeSocket6'); + expect(transport).to.be.instanceOf(npt.NamedPipeTransport); + expect(transport.isConnected()).to.be.true; + let buff = Buffer.from('hello', 'utf8'); + expect(transport.socketReceive(buff)).to.not.throw; + }); + }); + + describe('NamedPipe Client Tests', () => { + it('creates a new client', () => { + let client = new np.NamedPipeClient('pipeA', new protocol.RequestHandler(), false); + expect(client).to.be.instanceOf(np.NamedPipeClient); + expect(client.disconnect()).to.not.throw; + }); + + it('connects without throwing', () => { + let client = new np.NamedPipeClient('pipeA', new protocol.RequestHandler(), false); + expect(client.connect()).to.not.throw; + expect(client.disconnect()).to.not.throw; + }); + + it('disconnects without throwing', () => { + let client = new np.NamedPipeClient('pipeA', new protocol.RequestHandler(), false); + expect(client.disconnect()).to.not.throw; + }); + + it('sends without throwing', (done) => { + let client = new np.NamedPipeClient('pipeA', new protocol.RequestHandler(), false); + let req = new protocol.StreamingRequest(); + req.Verb = 'POST'; + req.Path = 'some/path'; + req.setBody('Hello World!'); + client.send(req).catch(err => {expect(err).to.be.undefined;}).then(done()); + }); + + }); + + describe('NamedPipe Server Tests', () => { + + it('creates a new server', () => { + let server = new np.NamedPipeServer('pipeA', new protocol.RequestHandler(), false); + expect(server).to.be.instanceOf(np.NamedPipeServer); + expect(server.disconnect()).to.not.throw; + }); + + it('starts the server without throwing', () => { + let server = new np.NamedPipeServer('pipeA', new protocol.RequestHandler(), false); + expect(server).to.be.instanceOf(np.NamedPipeServer); + + expect(server.start()).to.not.throw; + expect(server.disconnect()).to.not.throw; + }); + + it('disconnects without throwing', () => { + let server = new np.NamedPipeServer('pipeA', new protocol.RequestHandler(), false); + expect(server).to.be.instanceOf(np.NamedPipeServer); + expect(server.start()).to.not.throw; + expect(server.disconnect()).to.not.throw; + }); + + + it('sends without throwing', (done) => { + let server = new np.NamedPipeServer('pipeA', new protocol.RequestHandler(), false); + expect(server).to.be.instanceOf(np.NamedPipeServer); + expect(server.start()).to.not.throw; + let req = {verb: 'POST', path: '/api/messages', streams: []}; + server.send(req).catch(err => {expect(err).to.be.undefined;}).then( + expect(server.disconnect()).to.not.throw).then(done()); + }); + + it('handles being disconnected', (done) => { + let server = new np.NamedPipeServer('pipeA', new protocol.RequestHandler(), false); + expect(server).to.be.instanceOf(np.NamedPipeServer); + server.start(); + try { + server.onConnectionDisconnected(); + } catch (error) { + expect(err).to.equal(`address already in use \\.\pipe\pipeA.incoming`); + } + expect(server.disconnect()).to.not.throw; + done(); + }); + + it('handles being disconnected and tries to reconnect', (done) => { + let server = new np.NamedPipeServer('pipeA', new protocol.RequestHandler(), true); + expect(server).to.be.instanceOf(np.NamedPipeServer); + server.start(); + try { + server.onConnectionDisconnected(); + } catch (err) { + expect(err).to.equal(`address already in use \\.\pipe\pipeA.incoming`); + } + expect(server.disconnect()).to.not.throw; + done(); + }); + }); +}); diff --git a/libraries/botframework-streaming/tests/NodeWebSocket.test.js b/libraries/botframework-streaming/tests/NodeWebSocket.test.js new file mode 100644 index 0000000000..47b4884fbb --- /dev/null +++ b/libraries/botframework-streaming/tests/NodeWebSocket.test.js @@ -0,0 +1,77 @@ +const { NodeWebSocket } = require('../'); +const { expect } = require('chai'); +const { FauxSock, TestRequest } = require('./helpers'); + +describe('NodeSocket', () => { + it('creates a new NodeSocket', () => { + const ns = new NodeWebSocket(new FauxSock); + expect(ns).to.be.instanceOf(NodeWebSocket); + expect(ns.close()).to.not.be.undefined; + }); + + it('requires a valid URL', () => { + try { + const ns = new NodeWebSocket(new FauxSock); + } catch (error) { + expect(error.message).to.equal('Invalid URL: fakeURL'); + } + }); + + it('starts out connected', () => { + const ns = new NodeWebSocket(new FauxSock); + expect(ns.isConnected).to.be.true; + }); + + it('writes to the socket', () => { + const ns = new NodeWebSocket(new FauxSock); + const buff = Buffer.from('hello'); + expect(ns.write(buff)).to.not.throw; + }); + + it('attempts to open a connection', () => { + const ns = new NodeWebSocket(new FauxSock); + expect(ns.connect().catch((error) => { + expect(error.message).to.equal('connect ECONNREFUSED 127.0.0.1:8082'); + })); + }); + + it('can set message handlers on the socket', () => { + const sock = new FauxSock(); + const ns = new NodeWebSocket(sock); + expect(sock.textHandler).to.be.undefined; + expect(sock.binaryHandler).to.be.undefined; + expect(ns.setOnMessageHandler(() => { })).to.not.throw; + expect(sock.textHandler).to.not.be.undefined; + expect(sock.binaryHandler).to.not.be.undefined; + }); + + it('can set error handler on the socket', () => { + const sock = new FauxSock(); + const ns = new NodeWebSocket(sock); + expect(sock.errorHandler).to.be.undefined; + expect(ns.setOnErrorHandler(() => { })).to.not.throw; + expect(sock.errorHandler).to.not.be.undefined; + }); + + it('can set end handler on the socket', () => { + const sock = new FauxSock(); + const ns = new NodeWebSocket(sock); + expect(sock.endHandler).to.be.undefined; + expect(ns.setOnCloseHandler(() => { })).to.not.throw; + expect(sock.endHandler).to.not.be.undefined; + }); + + it('create() should be successful and set a WebSocket', () => { + const sock = new FauxSock(); + const nodeSocket = new NodeWebSocket(); + const request = new TestRequest(); + request.setIsUpgradeRequest(true); + request.headers = []; + request.headers['upgrade'] = 'websocket'; + request.headers['sec-websocket-key'] = 'BFlat'; + request.headers['sec-websocket-version'] = '13'; + request.headers['sec-websocket-protocol'] = ''; + nodeSocket.create(request, sock, Buffer.from([])); + nodeSocket.waterShedSocket.destroy(); + }); +}); diff --git a/libraries/botframework-streaming/tests/NodeWebSocketFactory.test.js b/libraries/botframework-streaming/tests/NodeWebSocketFactory.test.js new file mode 100644 index 0000000000..aa7391ce84 --- /dev/null +++ b/libraries/botframework-streaming/tests/NodeWebSocketFactory.test.js @@ -0,0 +1,22 @@ +const { NodeWebSocket, NodeWebSocketFactory } = require('../'); +const { FauxSock, TestRequest } = require('./helpers'); +const { expect } = require('chai'); + +describe('NodeWebSocketFactory', () => { + it('createWebSocket() should create a new NodeWebSocket', () => { + const factory = new NodeWebSocketFactory(); + const sock = new FauxSock(); + const request = new TestRequest(); + + request.setIsUpgradeRequest(true); + request.headers = []; + request.headers['upgrade'] = 'websocket'; + request.headers['sec-websocket-key'] = 'BFlat'; + request.headers['sec-websocket-version'] = '13'; + request.headers['sec-websocket-protocol'] = ''; + + const socket = factory.createWebSocket(request, sock, Buffer.from([])); + expect(socket).to.be.instanceOf(NodeWebSocket); + socket.waterShedSocket.destroy(); + }); +}); diff --git a/libraries/botframework-streaming/tests/PayloadSender.test.js b/libraries/botframework-streaming/tests/PayloadSender.test.js new file mode 100644 index 0000000000..4a5cf13c4f --- /dev/null +++ b/libraries/botframework-streaming/tests/PayloadSender.test.js @@ -0,0 +1,152 @@ +const SubscribableStream = require('../lib/subscribableStream'); +const StreamManager = require('../lib/payloads/streamManager'); +const PayloadSender = require('../lib/PayloadTransport/payloadSender'); +const PayloadReceiver = require('../lib/PayloadTransport/payloadReceiver'); +const PayloadTypes = require('../lib/payloads/payloadTypes'); +const PayloadAssemblerManager = require('../lib/payloads/payloadAssemblerManager'); +const chai = require('chai'); +var expect = chai.expect; + +class FauxSock{ + constructor(contentString){ + if(contentString){ + this.contentString = contentString; + this.position = 0; + } + } + send(buffer){ + return buffer.length; + }; + + receive(readLength){ + if(this.contentString[this.position]) + { + this.buff = Buffer.from(this.contentString[this.position]); + this.position++; + + return this.buff.slice(0, readLength); + } + + if(this.receiver.isConnected) + this.receiver.disconnect(); + } + close(){}; + + setReceiver(receiver){ + this.receiver = receiver; + } +} +describe('PayloadTransport', () => { + describe('PayloadSender', () => { + + it('starts out disconnected.', () => { + let ps = new PayloadSender.PayloadSender(); + expect(ps.isConnected).to.equal(false); + }); + + it('connects to its sender.', () => { + let ps = new PayloadSender.PayloadSender(); + ps.connect(new FauxSock); + expect(ps.isConnected).to.equal(true); + }); + + it('writes to its sender.', (done) => { + let ps = new PayloadSender.PayloadSender(); + ps.connect(new FauxSock); + expect(ps.isConnected).to.equal(true); + + let stream = new SubscribableStream.SubscribableStream(); + stream.write('This is a test stream.'); + let header = {payloadType: PayloadTypes.PayloadTypes.request, payloadLength: 22, id: '100', end: true}; + let packet = {header: header, payload: stream, sendCallBack: undefined}; + + expect(ps.sendPayload(header, stream, ()=>done())); + }); + + it('calls the packet sent callback.', (done) => { + let ps = new PayloadSender.PayloadSender(); + ps.connect(new FauxSock); + expect(ps.isConnected).to.equal(true); + + let stream = new SubscribableStream.SubscribableStream(); + stream.write('This is a test stream.'); + let header = {payloadType: PayloadTypes.PayloadTypes.request, payloadLength: 22, id: '100', end: true}; + + ps.sendPayload(header, stream, ()=>done()); + }); + + it('disconnects when header length is longer than packet length.', () => { + let ps = new PayloadSender.PayloadSender(); + ps.connect(new FauxSock); + expect(ps.isConnected).to.equal(true); + + let stream = new SubscribableStream.SubscribableStream(); + stream.write('This is a test stream.'); + let header = {payloadType: PayloadTypes.PayloadTypes.request, payloadLength: 42, id: '100', end: true}; + let packet = {header: header, payload: stream, sendCallBack: undefined}; + + ps.sendPayload(header, stream, ()=>done()); + + expect(ps.isConnected).to.equal(false); + }); + + it('gracefully fails when trying to write before connecting.', (done) => { + let ps = new PayloadSender.PayloadSender(); + ps.disconnected = () => done(); + expect(ps.isConnected).to.equal(false); + ps.connect(new FauxSock); + expect(ps.isConnected).to.equal(true); + expect(ps.disconnected).to.not.be.undefined; + + let stream = new SubscribableStream.SubscribableStream(); + stream.write('This is a test stream.'); + let header = {payloadType: PayloadTypes.PayloadTypes.request, payloadLength: 22, id: '100', end: true}; + let packet = {header: header, payload: stream, sendCallBack: ()=>done()}; + + ps.sendPayload(header, stream, ()=>done()); + }); + + }); + + describe('PayloadReceiver', () => { + + it('begins disconnected.', () => { + let pr = new PayloadReceiver.PayloadReceiver(); + expect(pr.isConnected).to.be.undefined; + }); + + it('connects to and reads a header with no payload from the transport.', () => { + let pr = new PayloadReceiver.PayloadReceiver(); + expect(pr.isConnected).to.be.undefined; + + let sock = new FauxSock(['A.000000.68e999ca-a651-40f4-ad8f-3aaf781862b4.1\n']); + sock.setReceiver(pr); + + pr.connect(sock); + expect(pr.isConnected).to.be.true; + }); + + it('connects to and reads a header with a stream the transport.', (done) => { + let pr = new PayloadReceiver.PayloadReceiver(); + expect(pr.isConnected).to.be.undefined; + + let sock = new FauxSock(['S.000005.68e999ca-a651-40f4-ad8f-3aaf781862b4.1\n', '12345']); + sock.setReceiver(pr); + + this.streamManager = new StreamManager.StreamManager(undefined); + assemblerManager = new PayloadAssemblerManager.PayloadAssemblerManager( + this.streamManager, + (id, response) => onReceiveResponse(id, response), + (id, request) => onReceiveRequest(id, request) + ); + + pr.subscribe((header) => assemblerManager.getPayloadStream(header), + (header, contentStream, contentLength) => assemblerManager.onReceive(header, contentStream, contentLength)); + + expect(pr.connect(sock)).to.not.throw; + + pr.disconnected = () => done(); + expect(pr.isConnected).to.be.true; + }); + }); +}); diff --git a/libraries/botframework-streaming/tests/ProtocolAdapter.test.js b/libraries/botframework-streaming/tests/ProtocolAdapter.test.js new file mode 100644 index 0000000000..a6c63ced4d --- /dev/null +++ b/libraries/botframework-streaming/tests/ProtocolAdapter.test.js @@ -0,0 +1,200 @@ +const PayloadAssembler = require('../lib/assemblers/payloadAssembler'); +const ProtocolAdapter = require('../lib/protocolAdapter'); +const RequestManager = require('../lib/payloads/requestManager'); +const PayloadSender = require('../lib/payloadTransport/payloadSender'); +const PayloadReceiver = require('../lib/payloadTransport/payloadReceiver'); +const PayloadConstants = require('../lib/payloads/payloadConstants'); +const SubscribableStream = require('../lib/subscribableStream'); +const RequestHandler = require('../lib/requestHandler'); +const Response = require('../lib/streamingResponse'); +const Request = require('../lib/streamingRequest'); +const StreamManager = require('../lib/payloads/streamManager'); +const chai = require('chai'); +var sinon = require('sinon'); +var expect = chai.expect; + +class TestRequestHandler extends RequestHandler.RequestHandler { + constructor(){ + super(); + } + processRequest(request, logger) { + let response = new Response.StreamingResponse(); + response.statusCode = 111; + response.setBody('Test body.'); + + return response; + } +} + +class TestRequestManager { + constructor(){ } + getResponse() { + let response = {statusCode: 200}; + return response; + } +} + +class TestPayloadSender { + constructor() {} + sendPayload(){ + return; + } +} + +describe('Streaming Extensions ProtocolAdapter', () => { + it('constructs properly.', () => { + let requestHandler = new RequestHandler.RequestHandler(); + let requestManager = new RequestManager.RequestManager(); + let payloadSender = new PayloadSender.PayloadSender(); + let paylaodReceiver = new PayloadReceiver.PayloadReceiver(); + let protocolAdapter = new ProtocolAdapter.ProtocolAdapter( + requestHandler, + requestManager, + payloadSender, + paylaodReceiver); + + expect(protocolAdapter.assemblerManager) + .to + .not + .be + .undefined; + + expect(protocolAdapter.payloadReceiver) + .to + .not + .be + .undefined; + + expect(protocolAdapter.payloadSender) + .to + .not + .be + .undefined; + + expect(protocolAdapter.sendOperations) + .to + .not + .be + .undefined; + + expect(protocolAdapter.streamManager) + .to + .not + .be + .undefined; + + expect(protocolAdapter.requestHandler) + .to + .not + .be + .undefined; + + expect(protocolAdapter.requestManager) + .to + .not + .be + .undefined; + }); + + it('processes requests.', async () => { + let requestHandler = new TestRequestHandler(); + let requestManager = new RequestManager.RequestManager(); + let payloadSender = new PayloadSender.PayloadSender(); + let paylaodReceiver = { + subscribe: function(){}, + }; + let protocolAdapter = new ProtocolAdapter.ProtocolAdapter( + requestHandler, + requestManager, + payloadSender, + paylaodReceiver); + + var requestHandlerSpy = sinon.spy(requestHandler, 'processRequest'); + + protocolAdapter.onReceiveRequest('42', {verb: 'POST', path: '/api/messages', streams: [] }); + expect(requestHandlerSpy.called).to.be.true; + }); + + it('processes responses.', async () => { + let requestHandler = new TestRequestHandler(); + let requestManager = new RequestManager.RequestManager(); + let payloadSender = new PayloadSender.PayloadSender(); + let paylaodReceiver = { + subscribe: function(){}, + }; + let protocolAdapter = new ProtocolAdapter.ProtocolAdapter( + requestHandler, + requestManager, + payloadSender, + paylaodReceiver); + + var requestManagerSpy = sinon.spy(requestManager, 'signalResponse'); + + protocolAdapter.onReceiveResponse('42', {statusCode: '200', streams: [] }); + expect(requestManagerSpy.called).to.be.true; + }); + + it('does not throw when processing a cancellation for an already processed stream', async () => { + let requestHandler = new TestRequestHandler(); + let requestManager = new RequestManager.RequestManager(); + let payloadSender = new PayloadSender.PayloadSender(); + let paylaodReceiver = { + subscribe: function(){}, + }; + let protocolAdapter = new ProtocolAdapter.ProtocolAdapter( + requestHandler, + requestManager, + payloadSender, + paylaodReceiver); + let header = {payloadType: 'A', payloadLength: '5', id: '100', end: true}; + let assembler = new PayloadAssembler.PayloadAssembler(new StreamManager.StreamManager(), {header: header, onCompleted: function() {} }); + + expect(protocolAdapter.onCancelStream(assembler)).to.not.throw; + }); + + it('sends requests.', async (done) => { + let requestHandler = new TestRequestHandler(); + let requestManager = new TestRequestManager(); + let payloadSender = new TestPayloadSender(); + let paylaodReceiver = new PayloadReceiver.PayloadReceiver(); + let protocolAdapter = new ProtocolAdapter.ProtocolAdapter( + requestHandler, + requestManager, + payloadSender, + paylaodReceiver); + + expect(protocolAdapter.sendRequest(new Request.StreamingRequest())) + .to.not.throw; + done(); + }); + + it('payloadreceiver responds with an error when told to connect twice', () => { + let pa = new PayloadReceiver.PayloadReceiver(); + let buffer =Buffer.alloc(Number(PayloadConstants.PayloadConstants.MaxHeaderLength)); + buffer.write('A.000168.68e999ca-a651-40f4-ad8f-3aaf781862b4.1\n'); + let receiver = { + + receive: function(){return buffer;}, + close: function(){throw new Error('Test error!');}, + + }; + let s = new SubscribableStream.SubscribableStream(); + s.write('{"statusCode": "12345","streams": [{"id": "1","contentType": "text","length": "2"},{"id": "2","contentType": "text","length": "2"},{"id": "3","contentType": "text","length": "2"}]}'); + let rp = {verb: 'POST', path: '/some/path'}; + rp.streams = []; + rp.streams.push(s); + + + pa.connect(receiver); + + expect(pa.isConnected).to.be.true; + try + { + pa.connect(receiver); + } catch(result) { + expect(result.message).to.equal('Already connected.'); + } + + pa.disconnect(); + }); +}); diff --git a/libraries/botframework-streaming/tests/RequestManager.test.js b/libraries/botframework-streaming/tests/RequestManager.test.js new file mode 100644 index 0000000000..83825ea816 --- /dev/null +++ b/libraries/botframework-streaming/tests/RequestManager.test.js @@ -0,0 +1,61 @@ +const RequestManager = require( '../lib/payloads/requestManager'); +const chai = require( 'chai'); +var expect = chai.expect; + +describe('RequestManager', () => { + + it('RequestManager starts empty', () => { + let rm = new RequestManager.RequestManager(); + + let count = rm.pendingRequestCount(); + expect(count) + .to + .equal(0); + }); + + it('RequestManager.getResponseAsync called twice throws', async () => { + let rm = new RequestManager.RequestManager(); + let requestId = '123'; + rm.getResponse(requestId, undefined); + + rm.getResponse(requestId, undefined) + .catch((reason) => expect(reason) + .to + .equal(`requestId \'${ requestId }\' already exists in RequestManager`)); + + }); + + it('RequestManager.signalResponse with no requestId returns false', async () => { + let rm = new RequestManager.RequestManager(); + let requestId = '123'; + let response; + let result = await rm.signalResponse(requestId, response); + + expect(result) + .to + .equal(false); + }); + + it('RequestManager end to end success', async () => { + let rm = new RequestManager.RequestManager(); + let requestId = '123'; + let response; + + let promise = rm.getResponse(requestId, undefined); + + let result = await rm.signalResponse(requestId, response); + expect(result) + .to + .equal(true); + + let receiveResponse = await promise; + + expect(receiveResponse) + .to + .equal(response); + expect(rm.pendingRequestCount()) + .to + .equal(0); + }); + +}); diff --git a/libraries/botframework-streaming/tests/SendOperations.test.js b/libraries/botframework-streaming/tests/SendOperations.test.js new file mode 100644 index 0000000000..f52324938c --- /dev/null +++ b/libraries/botframework-streaming/tests/SendOperations.test.js @@ -0,0 +1,66 @@ +const HttpContent = require('../lib/httpContentStream'); +const PayloadSender = require('../lib/payloadTransport/payloadSender'); +const SubscribableStream = require('../lib/subscribableStream'); +const SendOperations = require('../lib/payloads/sendOperations'); +const StreamingRequest = require('../lib/streamingRequest'); +const StreamingResponse = require('../lib/streamingResponse'); +const chai = require('chai'); +var expect = chai.expect; + +describe('Streaming Extension SendOperations Tests', () => { + it('constructs a new instance', () => { + let ps = new PayloadSender.PayloadSender(); + let so = new SendOperations.SendOperations(ps); + + expect(so).to.be.instanceOf(SendOperations.SendOperations); + }); + + it('processes a send request operation', async (done) => { + let ps = new PayloadSender.PayloadSender(); + let so = new SendOperations.SendOperations(ps); + let r = new StreamingRequest.StreamingRequest(); + let stream1 = new SubscribableStream.SubscribableStream(); + stream1.write('hello'); + let headers = {contentLength: '5', contentType: 'text/plain'}; + let hc = new HttpContent.HttpContent(headers, stream1); + r.addStream(hc); + expect(so).to.be.instanceOf(SendOperations.SendOperations); + so.sendRequest('test1', r).then(done()); + }); + + it('processes a send response with streams operation', (done) => { + let ps = new PayloadSender.PayloadSender(); + let so = new SendOperations.SendOperations(ps); + let stream1 = new SubscribableStream.SubscribableStream(); + stream1.write('hello'); + let headers = {contentLength: '5', contentType: 'text/plain'}; + let hc = new HttpContent.HttpContent(headers, stream1); + let r = StreamingResponse.StreamingResponse.create(200, hc); + r.setBody('This is a new body.'); + + expect(r).to.be.instanceOf(StreamingResponse.StreamingResponse); + expect(r.streams[1].content.stream.bufferList[0].toString()).to.contain('This is a new body.'); + expect(r.statusCode).to.equal(200); + + so.sendResponse('test1', r).then(done()); + }); + + it('processes a send response with no streams operation', (done) => { + let ps = new PayloadSender.PayloadSender(); + let so = new SendOperations.SendOperations(ps); + let stream1 = new SubscribableStream.SubscribableStream(); + stream1.write('hello'); + let headers = {contentLength: '5', contentType: 'text/plain'}; + let hc = new HttpContent.HttpContent(headers, stream1); + let r = StreamingResponse.StreamingResponse.create(200, hc); + + so.sendResponse('test1', r).then(done()); + }); + + it('processes a cancel stream operation', async (done) => { + let ps = new PayloadSender.PayloadSender(); + let so = new SendOperations.SendOperations(ps); + + so.sendCancelStream('test1').then(done()); + }); +}); diff --git a/libraries/botframework-streaming/tests/StreamManager.test.js b/libraries/botframework-streaming/tests/StreamManager.test.js new file mode 100644 index 0000000000..6f591dcdf0 --- /dev/null +++ b/libraries/botframework-streaming/tests/StreamManager.test.js @@ -0,0 +1,95 @@ +const StreamManager = require('../lib/payloads/streamManager'); +const PayloadTypes = require('../lib/payloads/payloadTypes'); +const SubscribableStream = require('../lib/subscribableStream'); +const PayloadAssembler = require('../lib/assemblers/payloadAssembler'); +const chai = require('chai'); +var expect = chai.expect; + +describe('Streaming Protocol StreamManager Tests', () => { + + it('properly constructs a new instance', () => { + let sm = new StreamManager.StreamManager(undefined); + expect(sm).to.be.instanceOf(StreamManager.StreamManager); + }); + + it('creates and returns a new assembler when none currently exist', () => { + let sm = new StreamManager.StreamManager(undefined); + expect(sm).to.be.instanceOf(StreamManager.StreamManager); + + let pa = sm.getPayloadAssembler('bob'); + + expect(pa).to.be.instanceOf(PayloadAssembler.PayloadAssembler); + expect(pa.id).to.equal('bob'); + }); + + it('creates and returns a new assembler when others already exist', () => { + let sm = new StreamManager.StreamManager(undefined); + expect(sm).to.be.instanceOf(StreamManager.StreamManager); + + let pa = sm.getPayloadAssembler('Huey'); + + expect(pa).to.be.instanceOf(PayloadAssembler.PayloadAssembler); + expect(pa.id).to.equal('Huey'); + + let pa2 = sm.getPayloadAssembler('Dewey'); + expect(pa2).to.be.instanceOf(PayloadAssembler.PayloadAssembler); + expect(pa2.id).to.equal('Dewey'); + + let pa3 = sm.getPayloadAssembler('Louie'); + expect(pa3).to.be.instanceOf(PayloadAssembler.PayloadAssembler); + expect(pa3.id).to.equal('Louie'); + }); + + it('looks up the correct assembler and returns the stream', () => { + let sm = new StreamManager.StreamManager(undefined); + expect(sm).to.be.instanceOf(StreamManager.StreamManager); + let head = {payloadType: PayloadTypes.PayloadTypes.request, payloadLength: '0', id: 'bob', end: true}; + let ps = sm.getPayloadStream(head); + + expect(ps).to.be.instanceOf(SubscribableStream.SubscribableStream); + + let pa = sm.getPayloadAssembler('bob'); + expect(pa).to.be.instanceOf(PayloadAssembler.PayloadAssembler); + expect(pa.id).to.equal('bob'); + }); + + it('does not throw when asked to receive on a non-existant stream', () => { + let sm = new StreamManager.StreamManager(undefined); + expect(sm).to.be.instanceOf(StreamManager.StreamManager); + let head = {payloadType: PayloadTypes.PayloadTypes.request, payloadLength: '0', id: 'bob', end: true}; + let stream1 = new SubscribableStream.SubscribableStream(); + stream1.write('hello'); + expect(sm.onReceive(head, stream1, 5)).to.not.throw; + }); + + it('attempts to receive from an existing stream', () => { + let sm = new StreamManager.StreamManager(undefined); + expect(sm).to.be.instanceOf(StreamManager.StreamManager); + let head = {payloadType: PayloadTypes.PayloadTypes.request, payloadLength: '0', id: 'bob', end: true}; + let pa = sm.getPayloadAssembler('bob'); + expect(pa).to.be.instanceOf(PayloadAssembler.PayloadAssembler); + expect(pa.id).to.equal('bob'); + let stream1 = new SubscribableStream.SubscribableStream(); + stream1.write('hello'); + expect(sm.onReceive(head, stream1, 5)).to.not.throw; + }); + + it('can close a stream', (done) => { + let sm = new StreamManager.StreamManager(done()); + expect(sm).to.be.instanceOf(StreamManager.StreamManager); + let head = {payloadType: PayloadTypes.PayloadTypes.request, payloadLength: '0', id: 'bob', end: true}; + let pa = sm.getPayloadAssembler('bob'); + expect(pa).to.be.instanceOf(PayloadAssembler.PayloadAssembler); + expect(pa.id).to.equal('bob'); + let stream1 = new SubscribableStream.SubscribableStream(); + stream1.write('hello'); + expect(sm.closeStream(pa.id)).to.not.throw; + }); + + it('does not throw when asked to close a stream that does not exist', (done) => { + let sm = new StreamManager.StreamManager(done()); + expect(sm).to.be.instanceOf(StreamManager.StreamManager); + let head = {payloadType: PayloadTypes.PayloadTypes.request, payloadLength: '0', id: 'bob', end: true}; + expect(sm.closeStream(head.id)).to.not.throw; + }); +}); diff --git a/libraries/botframework-streaming/tests/StreamingRequest.test.js b/libraries/botframework-streaming/tests/StreamingRequest.test.js new file mode 100644 index 0000000000..4ae5890cfc --- /dev/null +++ b/libraries/botframework-streaming/tests/StreamingRequest.test.js @@ -0,0 +1,120 @@ +const StreamingRequest = require( '../lib/streamingRequest'); +const HttpContent = require('../lib/httpContentStream'); +const SubscribableStream = require('../lib/subscribableStream'); +const chai = require( 'chai'); +var expect = chai.expect; + + +describe('Streaming Extensions Request tests', () => { + it('creates a new request with undefined properties', () => { + let r = new StreamingRequest.StreamingRequest(); + expect(r.path) + .equal(undefined); + expect(r.verb) + .equal(undefined); + }); + + it('creates a new instance and a new stream', () => { + let stream1 = new SubscribableStream.SubscribableStream(); + stream1.write('hello'); + let headers = {contentLength: '5', contentType: 'text/plain'}; + let hc = new HttpContent.HttpContent(headers, stream1); + let r = StreamingRequest.StreamingRequest.create('POST', 'some/where', 'hello'); + + expect(r).to.be.instanceOf(StreamingRequest.StreamingRequest); + expect(r.verb).to.equal('POST'); + expect(r.path).to.equal('some/where'); + }); + + it('creates a new instance with an existing stream', () => { + let stream1 = new SubscribableStream.SubscribableStream(); + stream1.write('hello'); + let headers = {contentLength: '5', contentType: 'text/plain'}; + let hc = new HttpContent.HttpContent(headers, stream1); + let r = StreamingRequest.StreamingRequest.create('POST', 'some/where', hc); + + expect(r).to.be.instanceOf(StreamingRequest.StreamingRequest); + expect(r.verb).to.equal('POST'); + expect(r.path).to.equal('some/where'); + }); + + it('throws when adding an undefined stream to an existing request', () => { + let stream1 = new SubscribableStream.SubscribableStream(); + stream1.write('hello'); + let headers = {contentLength: '5', contentType: 'text/plain'}; + let hc = new HttpContent.HttpContent(headers, stream1); + let r = StreamingRequest.StreamingRequest.create('POST', 'some/where', 'hello'); + + expect(r).to.be.instanceOf(StreamingRequest.StreamingRequest); + expect(r.verb).to.equal('POST'); + expect(r.path).to.equal('some/where'); + + try{ + r.addStream(undefined); + } + catch(err) { + expect(err.message).to.equal('Argument Undefined Exception: content undefined.'); + } + }); + + it('throws when attempting to add undefined streams', () => { + let r = new StreamingRequest.StreamingRequest(); + + expect(() => { r.addStream(undefined); }) + .throws; + }); + + it('is able to add streams to the request', () => { + let r = new StreamingRequest.StreamingRequest(); + let h; + let s = new SubscribableStream.SubscribableStream(); + let c = new HttpContent.HttpContent(h, s); + + r.addStream(c); + + expect(r.streams.length) + .equals(1); + expect(r.streams[0].content.getStream()) + .equals(c.getStream()); + }); + + it('creates the right verb', () => { + let r = StreamingRequest.StreamingRequest.create('GET'); + + expect(r.verb) + .equals('GET'); + }); + + it('creates the right path', () => { + let r = StreamingRequest.StreamingRequest.create('GET', 'happy'); + + expect(r.path) + .equals('happy'); + }); + + it('gets the unaltered stream', () => { + let h = {contentType: 'stuff'}; + let s = new SubscribableStream.SubscribableStream(); + s.push('text'); + let b = new HttpContent.HttpContent(h, s); + let r = StreamingRequest.StreamingRequest.create('POST'); + r.addStream(b); + + expect(b.getStream()) + .equals(s); + }); + + it('can create a request with a body', () => { + let h = {contentType: 'stuff'}; + let s = new SubscribableStream.SubscribableStream(); + s.push('text'); + let b = new HttpContent.HttpContent(h, s); + let sb = JSON.stringify(b); + let r = StreamingRequest.StreamingRequest.create('POST'); + r.addStream(b); + let c = new HttpContent.HttpContentStream(b); + + expect(r.streams[0].content.getStream()) + .equals(b.getStream()); + }); +}); diff --git a/libraries/botframework-streaming/tests/StreamingResponse.test.js b/libraries/botframework-streaming/tests/StreamingResponse.test.js new file mode 100644 index 0000000000..dff862a5b4 --- /dev/null +++ b/libraries/botframework-streaming/tests/StreamingResponse.test.js @@ -0,0 +1,32 @@ +const SubscribableStream = require('../lib/subscribableStream'); +const HttpContentStream = require('../lib/httpContentStream'); +const StreamingResponse = require('../lib/streamingResponse'); +const chai = require('chai'); +var expect = chai.expect; + +describe('Streaming Extensions Response Tests', () => { + + it('creates a new instance', () => { + let stream1 = new SubscribableStream.SubscribableStream(); + stream1.write('hello'); + let headers = {contentLength: '5', contentType: 'text/plain'}; + let hc = new HttpContentStream.HttpContent(headers, stream1); + let r = StreamingResponse.StreamingResponse.create(200, hc); + + expect(r).to.be.instanceOf(StreamingResponse.StreamingResponse); + expect(r.statusCode).to.equal(200); + }); + + it('can set the response body', () => { + let stream1 = new SubscribableStream.SubscribableStream(); + stream1.write('hello'); + let headers = {contentLength: '5', contentType: 'text/plain'}; + let hc = new HttpContentStream.HttpContent(headers, stream1); + let r = StreamingResponse.StreamingResponse.create(200, hc); + r.setBody('This is a new body.'); + + expect(r).to.be.instanceOf(StreamingResponse.StreamingResponse); + expect(r.streams[1].content.stream.bufferList[0].toString()).to.contain('This is a new body.'); + expect(r.statusCode).to.equal(200); + }); +}); diff --git a/libraries/botframework-streaming/tests/SubscribableStream.test.js b/libraries/botframework-streaming/tests/SubscribableStream.test.js new file mode 100644 index 0000000000..f610f7790a --- /dev/null +++ b/libraries/botframework-streaming/tests/SubscribableStream.test.js @@ -0,0 +1,33 @@ +const Stream = require( '../lib/subscribableStream'); +const chai = require( 'chai'); +var expect = chai.expect; + +describe('Streaming Extensions Stream Tests', () => { + it('throws on invalid encoding types', () => { + // expect.assertions(1); + let s = new Stream.SubscribableStream(); + + expect(() => s.write('data', 'supercoolencoding')) + .to + .throw(); + + }); + + it('does not throw on valid on valid encoding types', () => { + // expect.assertions(1); + let s = new Stream.SubscribableStream(); + + expect(() => s.write('data', 'utf8')) + .not + .to + .throw(); + + }); + + it('subscribes to data events', (done) => { + let s = new Stream.SubscribableStream(); + s.subscribe((data) => done()); + + s._write('hello', 'utf8', () => {}); + }); +}); diff --git a/libraries/botframework-streaming/tests/TransportConstants.test.js b/libraries/botframework-streaming/tests/TransportConstants.test.js new file mode 100644 index 0000000000..d5818141a6 --- /dev/null +++ b/libraries/botframework-streaming/tests/TransportConstants.test.js @@ -0,0 +1,29 @@ +const Constants = require( '../lib/payloads/payloadConstants'); +const chai = require( 'chai'); +var expect = chai.expect; +describe('TransportConstants', () => { + + it('has the proper value for MaxPayloadLength', () => { + expect(Constants.PayloadConstants.MaxPayloadLength) + .equal(4096); + }); + + it('has the proper value for MaxHeaderLength', () => { + expect(Constants.PayloadConstants.MaxHeaderLength) + .equal(48); + }); + it('has the proper value for MaxLength', () => { + expect(Constants.PayloadConstants.MaxLength) + .equal(999999); + }); + it('has the proper value for MinLength', () => { + expect(Constants.PayloadConstants.MinLength) + .equal(0); + }); + + it('throws when attempting to change value for MaxPayloadLength', () => { + expect(Constants.PayloadConstants.MaxPayloadLength) + .equal(4096); + }); + +}); diff --git a/libraries/botframework-streaming/tests/WebSocket.test.js b/libraries/botframework-streaming/tests/WebSocket.test.js new file mode 100644 index 0000000000..d8ad86cdeb --- /dev/null +++ b/libraries/botframework-streaming/tests/WebSocket.test.js @@ -0,0 +1,292 @@ +const ws = require('../lib'); +const protocol = require('../lib'); +const wst = require('../lib/webSocket/webSocketTransport'); +const chai = require('chai'); +const { FauxSock } = require('./helpers'); +var expect = chai.expect; + +describe('Streaming Extensions WebSocket Library Tests', () => { + describe('WebSocket Transport Tests', () => { + + it('creates a new transport', () => { + let sock = new FauxSock(); + let transport = new wst.WebSocketTransport(sock); + expect(transport).to.be.instanceOf(wst.WebSocketTransport); + expect( () => transport.close()).to.not.throw; + }); + + it('creates a new transport2', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new wst.WebSocketTransport(sock); + expect(transport).to.be.instanceOf(wst.WebSocketTransport); + expect( () => transport.close()).to.not.throw; + }); + + it('creates a new transport and connects', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new wst.WebSocketTransport(sock); + expect(transport).to.be.instanceOf(wst.WebSocketTransport); + expect(transport.isConnected()).to.be.true; + expect( () => transport.close()).to.not.throw; + }); + + it('closes the transport without throwing', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new wst.WebSocketTransport(sock); + expect(transport).to.be.instanceOf(wst.WebSocketTransport); + expect( transport.close()).to.not.throw; + let exists = transport.isConnected(); + expect(exists).to.be.false; + }); + + it('writes to the socket', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new wst.WebSocketTransport(sock); + expect(transport).to.be.instanceOf(wst.WebSocketTransport); + expect(transport.isConnected()).to.be.true; + let buff = Buffer.from('hello', 'utf8'); + let sent = transport.send(buff); + expect(sent).to.equal(5); + expect( () => transport.close()).to.not.throw; + }); + + it('returns 0 when attepmting to write to a closed socket', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new wst.WebSocketTransport(sock); + expect(transport).to.be.instanceOf(wst.WebSocketTransport); + expect(transport.isConnected()).to.be.true; + sock.writable = false; + sock.connected = false; + let buff = Buffer.from('hello', 'utf8'); + let sent = transport.send(buff); + expect(sent).to.equal(0); + expect( () => transport.close()).to.not.throw; + }); + + it('throws when reading from a dead socket', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new wst.WebSocketTransport(sock); + expect(transport).to.be.instanceOf(wst.WebSocketTransport); + expect(transport.isConnected()).to.be.true; + expect(transport.receive(5)).to.throw; + expect( () => transport.close()).to.not.throw; + }); + + it('can read from the socket', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new wst.WebSocketTransport(sock); + expect(transport).to.be.instanceOf(wst.WebSocketTransport); + expect(transport.isConnected()).to.be.true; + transport.receive(12).catch(); + transport.onReceive(Buffer.from('{"VERB":"POST", "PATH":"somewhere/something"}', 'utf8')); + + expect( () => transport.close()).to.not.throw; + }); + + it('cleans up when onClose is fired', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new wst.WebSocketTransport(sock); + expect(transport).to.be.instanceOf(wst.WebSocketTransport); + expect(transport.isConnected()).to.be.true; + transport.onClose(); + expect(transport._active).to.be.null; + expect(transport._activeReceiveResolve).to.be.null; + expect(transport._activeReceiveReject).to.be.null; + expect(transport._socket).to.be.null; + expect(transport._activeOffset).to.equal(0); + expect(transport._activeReceiveCount).to.equal(0); + }); + + it('cleans up when onError is fired', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new wst.WebSocketTransport(sock); + expect(transport).to.be.instanceOf(wst.WebSocketTransport); + expect(transport.isConnected()).to.be.true; + transport.onError(); + expect(transport._active).to.be.null; + expect(transport._activeReceiveResolve).to.be.null; + expect(transport._activeReceiveReject).to.be.null; + expect(transport._socket).to.be.null; + expect(transport._activeOffset).to.equal(0); + expect(transport._activeReceiveCount).to.equal(0); + }); + + it('does not throw when socketReceive is fired', () => { + let sock = new FauxSock(); + sock.destroyed = false; + sock.connecting = false; + sock.writable = true; + let transport = new wst.WebSocketTransport(sock); + expect(transport).to.be.instanceOf(wst.WebSocketTransport); + expect(transport.isConnected()).to.be.true; + let buff = Buffer.from('hello', 'utf8'); + expect(transport.onReceive(buff)).to.not.throw; + }); + + + }); + + describe('WebSocket Client Tests', () => { + it('creates a new client', () => { + let client = new ws.WebSocketClient('fakeURL', new protocol.RequestHandler()); + expect(client).to.be.instanceOf(ws.WebSocketClient); + expect(client.disconnect()).to.not.throw; + }); + + it('selects the right websocket and attempts to connect to the transport layer', (done) => { + let client = new ws.WebSocketClient('fakeURL', new protocol.RequestHandler()); + expect(client).to.be.instanceOf(ws.WebSocketClient); + client.connect() + .catch( + (err) => + { expect(err.message).to + .equal('Unable to connect client to Node transport.');}) //We don't want to really open a connection. + .then(done()); + }); + + it('sends', (done) => { + let client = new ws.WebSocketClient('fakeURL', new protocol.RequestHandler()); + expect(client).to.be.instanceOf(ws.WebSocketClient); + let req = new protocol.StreamingRequest(); + req.Verb = 'POST'; + req.Path = 'some/path'; + req.setBody('Hello World!'); + client.send(req).catch(err => {expect(err).to.be.undefined;}).then(done()); + }); + + it('disconnects', (done) => { + let client = new ws.WebSocketClient('fakeURL', new protocol.RequestHandler()); + expect(client).to.be.instanceOf(ws.WebSocketClient); + expect(client.disconnect()).to.not.throw; + done(); + }); + }); + + describe('WebSocket Server Tests', () => { + it('creates a new server', () => { + let server = new ws.WebSocketServer(new FauxSock, new protocol.RequestHandler()); + expect(server).to.be.instanceOf(ws.WebSocketServer); + expect(server.disconnect()).to.not.throw; + }); + + it('connects', (done) => { + let server = new ws.WebSocketServer(new FauxSock, new protocol.RequestHandler()); + expect(server).to.be.instanceOf(ws.WebSocketServer); + expect(server.start()).to.not.throw; + done(); + }); + + it('sends', (done) => { + let server = new ws.WebSocketServer(new FauxSock, new protocol.RequestHandler()); + expect(server).to.be.instanceOf(ws.WebSocketServer); + let req = new protocol.StreamingRequest(); + req.Verb = 'POST'; + req.Path = 'some/path'; + req.setBody('Hello World!'); + server.send(req).catch(err => {expect(err).to.be.undefined;}).then(done()); + }); + + it('disconnects', (done) => { + let server = new ws.WebSocketServer(new FauxSock, new protocol.RequestHandler()); + expect(server).to.be.instanceOf(ws.WebSocketServer); + expect(server.disconnect()).to.not.throw; + done(); + }); + }); + + describe('BrowserSocket Tests', () => { + it('creates a new BrowserSocket', () => { + let bs = new ws.BrowserWebSocket( new FauxSock()); + expect(bs).to.be.instanceOf(ws.BrowserWebSocket); + expect(() => bs.close()).to.not.throw; + }); + + it('knows its connected', () => { + let bs = new ws.BrowserWebSocket( new FauxSock()); + bs.connect('fakeUrl'); + expect(bs.isConnected).to.be.true; + }); + + it('writes to the socket', () => { + let bs = new ws.BrowserWebSocket( new FauxSock()); + let buff = Buffer.from('hello'); + expect(bs.write(buff)).to.not.throw; + }); + + it('always thinks it connects', () => { + let bs = new ws.BrowserWebSocket( new FauxSock()); + expect(bs.connect()).to.not.throw; + }); + + it('can set error handler on the socket', () => { + let sock = new FauxSock(); + let bs = new ws.BrowserWebSocket( sock); + expect(sock.onerror).to.be.undefined; + expect(bs.setOnErrorHandler(() => {})).to.not.throw; + expect(sock.onerror).to.not.be.undefined; + }); + + it('can set end handler on the socket', () => { + let sock = new FauxSock(); + let bs = new ws.BrowserWebSocket( sock); + expect(sock.onclose).to.be.undefined; + expect(bs.setOnCloseHandler(() => {})).to.not.throw; + expect(sock.onclose).to.not.be.undefined; + }); + + it('can set onerror on the socket', () => { + let sock = new FauxSock(); + let bs = new ws.BrowserWebSocket( sock); + bs.connect('nowhere'); + expect(sock.onerror).to.not.be.undefined; + expect(sock.onopen).to.not.be.undefined; + }); + + it('can set onopen on the socket', () => { + let sock = new FauxSock(); + let bs = new ws.BrowserWebSocket( sock); + bs.connect('nowhere'); + expect(sock.onerror).to.not.be.undefined; + expect(sock.onopen).to.not.be.undefined; + }); + + it('can close', () => { + let sock = new FauxSock(); + let bs = new ws.BrowserWebSocket( sock); + bs.connect('nowhere'); + expect(sock.onerror).to.not.be.undefined; + expect(sock.onopen).to.not.be.undefined; + let sinon = require('sinon'); + let spy = sinon.spy(sock, 'close'); + bs.close(); + expect(spy.called).to.be.true; + }); + }); +}); diff --git a/libraries/botframework-streaming/tests/helpers/fauxSock.js b/libraries/botframework-streaming/tests/helpers/fauxSock.js new file mode 100644 index 0000000000..18b1bdc76b --- /dev/null +++ b/libraries/botframework-streaming/tests/helpers/fauxSock.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +// Test code for a WebSocket. +class FauxSock { + constructor(contentString) { + if (contentString) { + this.contentString = contentString; + this.position = 0; + } + this.connecting = false; + this.connected = true; + this.readyState = 1; + this.exists = true; + + this.onmessage = undefined; + this.onerror = undefined; + this.onclose = undefined; + + // `ws` specific check in WebSocketServer.completeUpgrade + this.readable = true; + this.writable = true; + } + + /* Start of `ws` specific methods. */ + removeListener(event, handler) { + switch (event) { + case 'error': + return; + default: + console.error(`FauxSock.removeListener(): Reached default case: ${event}`); + } + } + + setTimeout(value) { + this.timeoutValue = value; + return; + } + + setNoDelay() { + } + /* End of `ws` specific methods. */ + + get isConnected() { + return this.connected; + } + + write(buffer) { + this.buffer = buffer; + } + + send(buffer) { + return buffer.length; + }; + + receive(readLength) { + if (this.contentString[this.position]) { + this.buff = Buffer.from(this.contentString[this.position]); + this.position++; + + return this.buff.slice(0, readLength); + } + + if (this.receiver.isConnected) + this.receiver.disconnect(); + } + close() { }; + close() { + this.connected = false; + }; + end() { + this.exists = false; + return true; + }; + destroyed() { + return this.exists; + }; + + /** WaterShed Socket Specific? */ + destroy() { + return true; + }; + + /** WaterShed Socket Specific? */ + removeAllListeners() { + return true; + } + + on(action, handler) { + if (action === 'error') { + this.errorHandler = handler; + } + if (action === 'data') { + this.messageHandler = handler; + } + if (action === 'close') { + this.closeHandler = handler; + } + if (action === 'end') { + this.endHandler = handler; + } + // Required for `watershed` WebSockets + if (action === 'text') { + this.textHandler = handler; + } + // Required for `watershed` WebSockets + if (action === 'binary') { + this.binaryHandler = handler; + } + // Required for `ws` WebSockets + if (action === 'data') { + this.dataHandler = handler; + } + // Required for `ws` WebSockets + if (action === 'message') { + this._messageHandler = handler; + } + }; + + setReceiver(receiver) { + this.receiver = receiver; + } + + setOnMessageHandler(handler) { + this.messageHandler = handler; + }; + setOnErrorHandler(handler) { + this.errorHandler = handler; + }; + setOnCloseHandler(handler) { + this.closeHandler = handler; + }; +} + +module.exports.FauxSock = FauxSock; diff --git a/libraries/botframework-streaming/tests/helpers/index.js b/libraries/botframework-streaming/tests/helpers/index.js new file mode 100644 index 0000000000..c6bdb28c8e --- /dev/null +++ b/libraries/botframework-streaming/tests/helpers/index.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +const { FauxSock } = require('./fauxSock'); +const { TestRequest } = require('./testRequest'); + +module.exports.FauxSock = FauxSock; +module.exports.TestRequest = TestRequest; diff --git a/libraries/botframework-streaming/tests/helpers/testRequest.js b/libraries/botframework-streaming/tests/helpers/testRequest.js new file mode 100644 index 0000000000..778add1d19 --- /dev/null +++ b/libraries/botframework-streaming/tests/helpers/testRequest.js @@ -0,0 +1,67 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +class TestRequest { + constructor() { + let headers = []; + } + + setMethod(verb) { + this.method = 'GET'; + } + + isUpgradeRequest() { + return this.upgradeRequestVal; + } + + setIsUpgradeRequest(value) { + // `ws` specific check + this.method = 'GET'; + this.upgradeRequestVal = value; + } + + status() { + return this.statusVal; + } + + status(value) { + this.statusVal = value; + } + + path(value) { + this.pathVal = value; + } + + path() { + return this.pathVal; + } + + verb(value) { + this.verbVal = value; + } + + verb() { + return this.verbVal; + } + + streams(value) { + this.streamsVal = value; + } + + streams() { + return this.streamsVal; + } + + setHeaders() { + return this.headersVal; + } + + setHeaders(value) { + this.headers = value; + } + +} + +module.exports.TestRequest = TestRequest; \ No newline at end of file diff --git a/libraries/botframework-streaming/tests/mocha.opts b/libraries/botframework-streaming/tests/mocha.opts new file mode 100644 index 0000000000..78c964bd43 --- /dev/null +++ b/libraries/botframework-streaming/tests/mocha.opts @@ -0,0 +1,5 @@ +# mocha.opts +--require ts-node/register +--require source-map-support/register +--recursive +**/*.js \ No newline at end of file diff --git a/libraries/botframework-streaming/tests/streamingAdatper.test.js b/libraries/botframework-streaming/tests/streamingAdatper.test.js new file mode 100644 index 0000000000..1386e475b9 --- /dev/null +++ b/libraries/botframework-streaming/tests/streamingAdatper.test.js @@ -0,0 +1,519 @@ +const { StreamingAdapter } = require('../'); +const { StatusCodes } = require('botbuilder'); +const { ActivityHandler } = require('botbuilder-core'); +const chai = require('chai'); +var expect = chai.expect; + +class FauxSock { + constructor(contentString) { + if (contentString) { + this.contentString = contentString; + this.position = 0; + } + this.connected = true; + } + isConnected() { return this.connected; } + write(buffer) { return; } + connect(serverAddress) { return new Promise(); } + close() { this.connected = false; return; } + setOnMessageHandler(handler) { return; } //(x: any) => void); + setOnErrorHandler(handler) { return; } + setOnCloseHandler(handler) { return; } +} + +class TestRequest { + constructor() { + let headers = []; + } + + isUpgradeRequest() { + return this.upgradeRequestVal; + } + + setIsUpgradeRequest(value) { + this.upgradeRequestVal = value; + } + + status() { + return this.statusVal; + } + + status(value) { + this.statusVal = value; + } + + path(value) { + this.pathVal = value; + } + + path() { + return this.pathVal; + } + + verb(value) { + this.verbVal = value; + } + + verb() { + return this.verbVal; + } + + streams(value) { + this.streamsVal = value; + } + + streams() { + return this.streamsVal; + } + + setHeaders() { + return this.headersVal; + } + + setHeaders(value) { + this.headers = value; + } + +} + +class TestResponse { + send(value) { + this.sendVal = value; + return this.sendVal; + } + + status(value) { + this.statusVal = value; + return this.statusVal; + } + + setClaimUpgrade(value) { + this.claimUpgradeVal = value; + } + + claimUpgrade() { + return this.claimUpgradeVal; + } +} + +class TestAdapterSettings { + constructor(appId = undefined, appPassword = undefined, channelAuthTenant, oAuthEndpoint, openIdMetadata, channelServce) { + this.appId = appId; + this.appPassword = appPassword; + this.enableWebSockets = true; + } +} + +describe('BotFrameworkStreamingAdapter tests', () => { + + it('has the correct status codes', () => { + expect(StatusCodes.OK).to.equal(200); + expect(StatusCodes.BAD_REQUEST).to.equal(400); + expect(StatusCodes.UNAUTHORIZED).to.equal(401); + expect(StatusCodes.NOT_FOUND).to.equal(404); + expect(StatusCodes.METHOD_NOT_ALLOWED).to.equal(405); + expect(StatusCodes.UPGRADE_REQUIRED).to.equal(426); + expect(StatusCodes.INTERNAL_SERVER_ERROR).to.equal(500); + expect(StatusCodes.NOT_IMPLEMENTED).to.equal(501); + }); + + it('gets constructed properly', () => { + let handler = new StreamingAdapter(); + + expect(handler).to.be.instanceOf(StreamingAdapter); + }); + + it('starts and stops a namedpipe server', () => { + let handler = new StreamingAdapter(); + + handler.useNamedPipe('PipeyMcPipeface', async (context) => { + // Route to bot + await bot.run(context); + }); + expect(handler.streamingServer.disconnect()).to.not.throw; + }); + + it('starts and stops a websocket server', async () => { + const bot = new ActivityHandler(); + const handler = new StreamingAdapter(new TestAdapterSettings()); + const request = new TestRequest(); + request.setIsUpgradeRequest(true); + request.headers = []; + request.headers['upgrade'] = 'websocket'; + request.headers['sec-websocket-key'] = 'BFlat'; + request.headers['sec-websocket-version'] = '13'; + request.headers['sec-websocket-protocol'] = ''; + + const response = new TestResponse({ claimUpgrade: 'anything' }); + const fakeSocket = { + unshift: function () { return true; }, + write: function (value) { }, + on: function (value) { }, + read: function () { return new Buffer.from('data', 'utf8'); }, + end: function () { return; }, + }; + response.setClaimUpgrade({ socket: fakeSocket, head: 'websocket' }); + await handler.useWebSocket(request, response, async (context) => { + // Route to bot + await bot.run(context); + }); + }); + + it('returns a connector client', async () => { + let bot = new ActivityHandler(); + let handler = new StreamingAdapter(new TestAdapterSettings()); + let request = new TestRequest(); + request.setIsUpgradeRequest(true); + request.headers = []; + request.headers['upgrade'] = 'websocket'; + request.headers['sec-websocket-key'] = 'BFlat'; + request.headers['sec-websocket-version'] = '13'; + request.headers['sec-websocket-protocol'] = ''; + let response = new TestResponse(); + let fakeSocket = { + unshift: function () { return true; }, + write: function (value) { }, + on: function (value) { }, + read: function () { return new Buffer.from('data', 'utf8'); }, + end: function () { return; }, + }; + response.socket = fakeSocket; + response.setClaimUpgrade({ socket: fakeSocket, head: 'websocket' }); + + await handler.useWebSocket(request, response, async (context) => { + // Route to bot + await bot.run(context); + }); + const cc = handler.createConnectorClient('urn:test'); + expect(cc.baseUri).to.equal('urn:test'); + }); + + describe('useWebSocket()', () => { + it('connects', async () => { + let bot = new ActivityHandler(); + let handler = new StreamingAdapter(new TestAdapterSettings()); + let request = new TestRequest(); + request.setIsUpgradeRequest(true); + request.headers = []; + request.headers['upgrade'] = 'websocket'; + request.headers['sec-websocket-key'] = 'BFlat'; + request.headers['sec-websocket-version'] = '13'; + request.headers['sec-websocket-protocol'] = ''; + let response = new TestResponse(); + let fakeSocket = { + unshift: function () { return true; }, + write: function (value) { }, + on: function (value) { }, + read: function () { return new Buffer.from('data', 'utf8'); }, + end: function () { return; }, + }; + response.socket = fakeSocket; + response.setClaimUpgrade({ socket: fakeSocket, head: 'websocket' }); + + await handler.useWebSocket(request, response, async (context) => { + // Route to bot + await bot.run(context); + }); + }); + + it('returns status code 401 when request is not authorized', async () => { + let bot = new ActivityHandler(); + const settings = new TestAdapterSettings('appId', 'password'); + let handler = new StreamingAdapter(settings); + let request = new TestRequest(); + request.setIsUpgradeRequest(true); + request.setHeaders({ channelid: 'fakechannel', authorization: 'donttrustme' }); + let response = new TestResponse(); + + await handler.useWebSocket(request, response, async (context) => { + // Route to bot + await bot.run(context); + throw new Error('useWebSocket should have thrown an error'); + }).catch(err => { + expect(err.message).to.equal('Unauthorized. Is not authenticated'); + }); + }); + }); + + describe('processRequest()', () => { + it('returns a 400 when the request is missing verb', async () => { + let handler = new StreamingAdapter(); + let request = new TestRequest(); + request.verb = undefined; + request.path = '/api/messages'; + let fakeStream = { + readAsJson: function () { return { type: 'Invoke', serviceUrl: 'somewhere/', channelId: 'test' }; }, + }; + request.streams[0] = fakeStream; + + const response = await handler.processRequest(request); + expect(response.statusCode).to.equal(400); + }); + + it('returns a 400 when the request is missing path', async () => { + let handler = new StreamingAdapter(); + let request = new TestRequest(); + request.verb = 'POST'; + request.path = undefined; + let fakeStream = { + readAsJson: function () { return { type: 'Invoke', serviceUrl: 'somewhere/', channelId: 'test' }; }, + }; + request.streams[0] = fakeStream; + + const response = await handler.processRequest(request); + expect(response.statusCode).to.equal(400); + }); + + it('returns a 400 when the request body is missing', async () => { + let handler = new StreamingAdapter(); + let request = new TestRequest('POST', '/api/messages'); + request.verb = 'POST'; + request.path = '/api/messages'; + request.streams = undefined; + + const response = await handler.processRequest(request); + expect(response.statusCode).to.equal(400); + }); + + it('returns user agent information when a GET hits the version endpoint', async () => { + let handler = new StreamingAdapter(); + let request = new TestRequest(); + request.verb = 'GET'; + request.path = '/api/version'; + let fakeStream = { + readAsJson: function () { return { type: 'Invoke', serviceUrl: 'somewhere/', channelId: 'test' }; }, + }; + request.streams[0] = fakeStream; + + const response = await handler.processRequest(request); + expect(response.statusCode).to.equal(200); + expect(response.streams[0].content).to.not.be.undefined; + }); + + it('returns user agent information from cache when a GET hits the version endpoint more than once', async () => { + let handler = new StreamingAdapter(); + let request = new TestRequest(); + request.verb = 'GET'; + request.path = '/api/version'; + let fakeStream = { + readAsJson: function () { return { type: 'Invoke', serviceUrl: 'somewhere/', channelId: 'test' }; }, + }; + request.streams[0] = fakeStream; + + const response = await handler.processRequest(request); + expect(response.statusCode).to.equal(200); + expect(response.streams[0].content).to.not.be.undefined; + + const response2 = await handler.processRequest(request); + expect(response2.statusCode).to.equal(200); + expect(response2.streams[0].content).to.not.be.undefined; + }); + + it('returns 405 for unsupported methods', async () => { + let handler = new StreamingAdapter(); + let request = new TestRequest(); + request.verb = 'UPDATE'; + request.path = '/api/version'; + let fakeStream = { + readAsJson: function () { return { type: 'Invoke', serviceUrl: 'somewhere/', channelId: 'test' }; }, + }; + request.streams[0] = fakeStream; + + const response = await handler.processRequest(request); + expect(response.statusCode).to.equal(405); + }); + + it('returns 404 for unsupported paths', async () => { + let handler = new StreamingAdapter(); + let request = new TestRequest(); + request.verb = 'POST'; + request.path = '/api/supersecretbackdoor'; + let fakeStream = { + readAsJson: function () { return { type: 'Invoke', serviceUrl: 'somewhere/', channelId: 'test' }; }, + }; + request.streams[0] = fakeStream; + + const response = await handler.processRequest(request); + expect(response.statusCode).to.equal(404); + }); + + it('processes a well formed request when there is no middleware with a non-Invoke activity type', async () => { + let bot = new ActivityHandler(); + let handler = new StreamingAdapter(); + let request = new TestRequest(); + request.verb = 'POST'; + request.path = '/api/messages'; + let fakeStream = { + readAsJson: function () { return { type: 'something', serviceUrl: 'somewhere/', channelId: 'test' }; }, + }; + request.streams[0] = fakeStream; + + handler.logic = async (context) => { + // Route to bot + await bot.run(context); + }; + + const response = await handler.processRequest(request); + expect(response.statusCode).to.equal(200); + }); + + it('returns a 501 when activity type is invoke, but the activity is invalid', async () => { + let bot = new ActivityHandler(); + let handler = new StreamingAdapter(); + let request = new TestRequest(); + request.verb = 'POST'; + request.path = '/api/messages'; + let fakeStream = { + readAsJson: function () { return { type: 'invoke', serviceUrl: 'somewhere/', channelId: 'test' }; }, + }; + request.streams[0] = fakeStream; + + handler.logic = async (context) => { + // Route to bot + await bot.run(context); + }; + + const response = await handler.processRequest(request); + expect(response.statusCode).to.equal(501); + }); + + it('returns a 500 when bot can not run', async () => { + const MiddleWare = require('botbuilder-core'); + let bot = {}; + let mw = { + async onTurn(context, next) { + console.log('Middleware executed!'); + await next(); + } + }; + let mwset = []; + mwset.push(mw); + let handler = new StreamingAdapter({ bot: bot, middleWare: mwset }); + let request = new TestRequest(); + request.verb = 'POST'; + request.path = '/api/messages'; + let fakeStream = { + readAsJson: function () { return { type: 'invoke', serviceUrl: 'somewhere/', channelId: 'test' }; }, + }; + request.streams[0] = fakeStream; + + const response = await handler.processRequest(request); + expect(response.statusCode).to.equal(500); + }); + + it('executes middleware', async () => { + var sinon = require('sinon'); + let bot = new ActivityHandler(); + bot.run = function (turnContext) { return Promise.resolve(); }; + + let handler = new StreamingAdapter(); + let middlewareCalled = false; + const middleware = { + async onTurn(context, next) { + middlewareCalled = true; + return next(); + } + } + + handler.use(middleware); + + const runSpy = sinon.spy(bot, 'run'); + let request = new TestRequest(); + request.verb = 'POST'; + request.path = '/api/messages'; + let fakeStream = { + readAsJson: function () { return { type: 'invoke', serviceUrl: 'somewhere/', channelId: 'test' }; }, + }; + request.streams[0] = fakeStream; + + handler.logic = async (context) => { + // Route to bot + await bot.run(context); + }; + + const response = await handler.processRequest(request); + expect(response.statusCode).to.equal(501); + expect(runSpy.called).to.be.true; + expect(middlewareCalled).to.be.true; + }); + }); + + it('sends a request', async () => { + let bot = new ActivityHandler(); + let handler = new StreamingAdapter(new TestAdapterSettings()); + let request = new TestRequest(); + request.setIsUpgradeRequest(true); + request.headers = []; + request.headers['upgrade'] = 'websocket'; + request.headers['sec-websocket-key'] = 'BFlat'; + request.headers['sec-websocket-version'] = '13'; + request.headers['sec-websocket-protocol'] = ''; + let response = new TestResponse(); + let fakeSocket = { + unshift: function () { return true; }, + write: function () { return Promise.resolve; }, + on: function () { return; }, + read: function () { return new Buffer.from('data', 'utf8'); }, + end: function () { return Promise.resolve; }, + }; + response.socket = fakeSocket; + const sinon = require('sinon'); + const spy = sinon.spy(fakeSocket, "write"); + response.setClaimUpgrade({ socket: fakeSocket, head: 'websocket' }); + + try { + await handler.useWebSocket(request, response, async (context) => { + // Route to bot + await bot.run(context); + }) + } catch (err) { + throw err; + } + + let connection = handler.createConnectorClient('fakeUrl'); + connection.sendRequest({ method: 'POST', url: 'testResultDotCom', body: 'Test body!' }); + expect(spy.called).to.be.true; + }).timeout(2000); + + describe('private methods', () => { + it('should identify streaming connections', function () { + let activity = { + type: 'message', + text: 'TestOAuth619 test activity', + recipient: { id: 'TestOAuth619' }, + }; + + const streaming = [ + 'urn:botframework:WebSocket:wss://beep.com', + 'urn:botframework:WebSocket:http://beep.com', + 'URN:botframework:WebSocket:wss://beep.com', + 'URN:botframework:WebSocket:http://beep.com', + ]; + + streaming.forEach(s => { + activity.serviceUrl = s; + expect(StreamingAdapter.isFromStreamingConnection(activity)).to.be.true; + }); + }); + + it('should identify http connections', function () { + let activity = { + type: 'message', + text: 'TestOAuth619 test activity', + recipient: { id: 'TestOAuth619' }, + }; + + const streaming = [ + 'http://yayay.com', + 'https://yayay.com', + 'HTTP://yayay.com', + 'HTTPS://yayay.com', + ]; + + streaming.forEach(s => { + activity.serviceUrl = s; + expect(StreamingAdapter.isFromStreamingConnection(activity)).to.be.false; + }); + }); + }); +}); diff --git a/libraries/botframework-streaming/tests/tokenResolver.test.js b/libraries/botframework-streaming/tests/tokenResolver.test.js new file mode 100644 index 0000000000..a621c0158f --- /dev/null +++ b/libraries/botframework-streaming/tests/tokenResolver.test.js @@ -0,0 +1,266 @@ +const assert = require('assert'); +const { TurnContext, CardFactory, BotCallbackHandlerKey } = require('botbuilder-core'); +const { StreamingAdapter, TokenResolver } = require('../'); + +class MockAdapter extends StreamingAdapter { + constructor(botLogic, getUserTokenCallback) { + super(undefined); + + this.botLogic = async (ctx) => { botLogic(ctx); }; + this.getUserTokenCallback = getUserTokenCallback; + } + + createTurnContext(activity) { + const context = new TurnContext(this, activity); + context.turnState.set(BotCallbackHandlerKey, this.botLogic); + return context; + } + + getUserToken(context, connectionName, magicCode) { + return Promise.resolve(this.getUserTokenCallback()); + } +} + +function createOAuthCardActivity() { + let activity = { + activityId: '1234', + channelId: 'test', + serviceUrl: 'urn:botframework.com:websocket:wss://channel.com/blah', + user: { id: 'user', name: 'User Name' }, + bot: { id: 'bot', name: 'Bot Name' }, + conversation: { + id: 'convo1', + properties: { + 'foo': 'bar' + } + }, + attachments: [], + }; + activity.attachments.push(CardFactory.oauthCard('foo', 'sign-in')); + return activity; +} + +describe(`TokenResolver`, function () { + this.timeout(50000000); + + it(`should throw on empty connectionName`, async function () { + const returnTokenResponse = () => { return { token: '1234', connectionName: 'foo' }; }; + const botLogic= (ctx) => { + if (ctx.activity.type === 'event' && ctx.activity.value.token) { + gotToken = true; + } + }; + const adapter = new MockAdapter(botLogic, returnTokenResponse); + const activity = createOAuthCardActivity(); + activity.attachments[0].content.connectionName = undefined; + const context = adapter.createTurnContext(activity); + + try + { + TokenResolver.checkForOAuthCards(adapter, context, activity); + assert(false, 'did not throw when should have'); + } + catch(e) + { + assert(e.message === 'The OAuthPrompt\'s ConnectionName property is missing a value.', 'did not receive token'); + } + }); + + it(`no attachements is a no-op`, async function () { + let fail = false; + const returnTokenResponse = () => { fail = true; return { token: '1234', connectionName: 'foo' }; }; + const botLogic= (ctx) => { + fail = true; + }; + const adapter = new MockAdapter(botLogic, returnTokenResponse); + const activity = createOAuthCardActivity(); + activity.attachments = []; + const context = adapter.createTurnContext(activity); + const log = []; + + TokenResolver.checkForOAuthCards(adapter, context, activity, log); + + assert(!fail, 'called bot methods'); + assert(log.length === 0, 'logged actions, should be zero'); + }); + + it(`should get the token`, async function () { + let gotToken = false; + const returnTokenResponse = () => { return { token: '1234', connectionName: 'foo' }; }; + let doneResolve, doneReject; + let done = new Promise((resolve, reject) => { + doneResolve = resolve; + doneReject = reject; + }); + const botLogic= (ctx) => { + if (ctx.activity.type === 'event' && ctx.activity.value.token) { + gotToken = true; + doneResolve('done'); + } else { + doneReject('error'); + } + }; + const adapter = new MockAdapter(botLogic, returnTokenResponse); + const activity = createOAuthCardActivity(); + const context = adapter.createTurnContext(activity); + const log = []; + + TokenResolver.checkForOAuthCards(adapter, context, activity, log); + + await done; + + assert(gotToken, 'did not receive token'); + }); + + it(`should call onTurnError with process throw Error`, async function () { + let calledOnTurnError = false; + const returnTokenResponse = () => { return { token: '1234', connectionName: 'foo' }; }; + let doneResolve, doneReject; + let done = new Promise((resolve, reject) => { + doneResolve = resolve; + doneReject = reject; + }); + const botLogic= (ctx) => { + if (ctx.activity.type === 'event' && ctx.activity.value.token) { + throw 'this is the error'; + } else { + doneReject('error'); + } + }; + const adapter = new MockAdapter(botLogic, returnTokenResponse); + adapter.onTurnError = async (context, error) => { + calledOnTurnError = true; + doneResolve('done'); + }; + const activity = createOAuthCardActivity(); + const context = adapter.createTurnContext(activity); + const log = []; + + TokenResolver.checkForOAuthCards(adapter, context, activity, log); + + await done; + + assert(calledOnTurnError, 'did not receive error'); + }); + + it(`should call onTurnError with process throw other`, async function () { + let calledOnTurnError = false; + const returnTokenResponse = () => { return { token: '1234', connectionName: 'foo' }; }; + let doneResolve, doneReject; + let done = new Promise((resolve, reject) => { + doneResolve = resolve; + doneReject = reject; + }); + const botLogic= (ctx) => { + if (ctx.activity.type === 'event' && ctx.activity.value.token) { + throw new Error('this is the error'); + } else { + doneReject('error'); + } + }; + const adapter = new MockAdapter(botLogic, returnTokenResponse); + adapter.onTurnError = async (context, error) => { + calledOnTurnError = true; + doneResolve('done'); + }; + const activity = createOAuthCardActivity(); + const context = adapter.createTurnContext(activity); + const log = []; + + TokenResolver.checkForOAuthCards(adapter, context, activity, log); + + await done; + + assert(calledOnTurnError, 'did not receive error'); + }); + + it(`should get the token on the second try`, async function () { + let gotToken = false; + let i = 0; + const returnTokenResponse = () => + { + i++; + if ( i < 2 ) + return undefined; + return { token: '1234', connectionName: 'foo' }; + }; + let doneResolve, doneReject; + let done = new Promise((resolve, reject) => { + doneResolve = resolve; + doneReject = reject; + }); + const botLogic= (ctx) => { + if (ctx.activity.type === 'event' && ctx.activity.value.token) { + gotToken = true; + doneResolve('done'); + } else { + doneReject('error'); + } + }; + const adapter = new MockAdapter(botLogic, returnTokenResponse); + const activity = createOAuthCardActivity(); + const context = adapter.createTurnContext(activity); + + TokenResolver.checkForOAuthCards(adapter, context, activity); + + await done; + + assert(gotToken, 'did not receive token'); + }); + + it(`should end polling`, async function () { + let doneResolve, doneReject; + let done = new Promise((resolve, reject) => { + doneResolve = resolve; + doneReject = reject; + }); + const returnTokenResponse = () => + { + doneResolve('done'); + return { properties: { tokenPollingSettings: { timeout: 0 } } }; + }; + const botLogic= (ctx) => { + }; + const adapter = new MockAdapter(botLogic, returnTokenResponse); + const activity = createOAuthCardActivity(); + const context = adapter.createTurnContext(activity); + const log = []; + + TokenResolver.checkForOAuthCards(adapter, context, activity, log); + + await done; + + assert(log.indexOf('End polling') !== -1, 'did not end polling'); + }); + + it(`should change interval polling`, async function () { + let doneResolve, doneReject; + let done = new Promise((resolve, reject) => { + doneResolve = resolve; + doneReject = reject; + }); + let i = 0; + const returnTokenResponse = () => + { + i++; + if (i < 2) { + return { properties: { tokenPollingSettings: { interval: 100 } } }; + } else { + doneResolve('done'); + return { properties: { tokenPollingSettings: { timeout: 0 } } }; + } + }; + const botLogic= (ctx) => { + }; + const adapter = new MockAdapter(botLogic, returnTokenResponse); + const activity = createOAuthCardActivity(); + const context = adapter.createTurnContext(activity); + const log = []; + + TokenResolver.checkForOAuthCards(adapter, context, activity, log); + + await done; + + assert(log.indexOf('Changing polling interval to 100') !== -1, 'did not end polling'); + }); +}); diff --git a/libraries/botframework-streaming/tests/wsNodeWebSocket.test.js b/libraries/botframework-streaming/tests/wsNodeWebSocket.test.js new file mode 100644 index 0000000000..755aa787e8 --- /dev/null +++ b/libraries/botframework-streaming/tests/wsNodeWebSocket.test.js @@ -0,0 +1,82 @@ +const { WsNodeWebSocket } = require('../'); +const { expect } = require('chai'); +const { FauxSock, TestRequest } = require('./helpers'); +const { randomBytes } = require('crypto'); + +describe('WsNodeWebSocket', () => { + it('creates a new WsNodeWebSocket', () => { + const wsSocket = new WsNodeWebSocket(new FauxSock); + expect(wsSocket).to.be.instanceOf(WsNodeWebSocket); + expect(wsSocket.close()).to.not.throw; + }); + + it('requires a valid URL', () => { + try { + const wsSocket = new WsNodeWebSocket(new FauxSock); + } catch (error) { + expect(error.message).to.equal('Invalid URL: fakeURL'); + } + }); + + it('starts out connected', () => { + const wsSocket = new WsNodeWebSocket(new FauxSock); + expect(wsSocket.isConnected).to.be.true; + }); + + it('writes to the socket', () => { + const wsSocket = new WsNodeWebSocket(new FauxSock); + const buff = Buffer.from('hello'); + expect(wsSocket.write(buff)).to.not.throw; + }); + + it('attempts to open a connection', () => { + const wsSocket = new WsNodeWebSocket(new FauxSock); + expect(wsSocket.connect().catch((error) => { + expect(error.message).to.equal('connect ECONNREFUSED 127.0.0.1:8082'); + })); + }); + + it('can set message handlers on the socket', () => { + const sock = new FauxSock(); + const wsSocket = new WsNodeWebSocket(sock); + expect(sock.dataHandler).to.be.undefined; + expect(sock._messageHandler).to.be.undefined; + expect(wsSocket.setOnMessageHandler(() => { })).to.not.throw; + expect(sock.dataHandler).to.not.be.undefined; + expect(sock._messageHandler).to.not.be.undefined; + }); + + it('can set error handler on the socket', () => { + const sock = new FauxSock(); + const wsSocket = new WsNodeWebSocket(sock); + expect(sock.errorHandler).to.be.undefined; + expect(wsSocket.setOnErrorHandler(() => { })).to.not.throw; + expect(sock.errorHandler).to.not.be.undefined; + }); + + it('can set end handler on the socket', () => { + const sock = new FauxSock(); + const wsSocket = new WsNodeWebSocket(sock); + expect(sock.closeHandler).to.be.undefined; + expect(wsSocket.setOnCloseHandler(() => { })).to.not.throw; + expect(sock.closeHandler).to.not.be.undefined; + }); + + it('create() should be successful and set a WebSocket', async () => { + const sock = new FauxSock(); + const nodeSocket = new WsNodeWebSocket(); + const request = new TestRequest(); + + // Configure a proper upgrade request for `ws`. + request.setIsUpgradeRequest(true); + request.headers = { upgrade: 'websocket' }; + // Use Node.js `crypto` module to calculate a valid 'sec-websocket-key' value. + // The key must pass this RegExp: + // https://github.com/websockets/ws/blob/0a612364e69fc07624b8010c6873f7766743a8e3/lib/websocket-server.js#L12 + request.headers['sec-websocket-key'] = randomBytes(16).toString('base64'); + request.headers['sec-websocket-version'] = '13'; + request.headers['sec-websocket-protocol'] = ''; + + await nodeSocket.create(request, sock, Buffer.from([])); + }); +}); diff --git a/libraries/botframework-streaming/tsconfig.json b/libraries/botframework-streaming/tsconfig.json new file mode 100644 index 0000000000..74e5f85fc1 --- /dev/null +++ b/libraries/botframework-streaming/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": ["es2015", "dom"], + "module": "commonjs", + "declaration": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + "types" : ["node"] + }, +} \ No newline at end of file diff --git a/libraries/functional-tests/functionaltestbot/index.js b/libraries/functional-tests/functionaltestbot/index.js index 56e2419291..78657dd7d0 100644 --- a/libraries/functional-tests/functionaltestbot/index.js +++ b/libraries/functional-tests/functionaltestbot/index.js @@ -5,7 +5,6 @@ const restify = require('restify'); const path = require('path'); const { BotFrameworkAdapter, MemoryStorage, UserState, ConversationState, InspectionState, InspectionMiddleware } = require('botbuilder'); -const { MicrosoftAppCredentials } = require('botframework-connector'); const { MyBot } = require('./bots/myBot') const ENV_FILE = path.join(__dirname, '.env'); @@ -22,7 +21,7 @@ var inspectionState = new InspectionState(memoryStorage); var userState = new UserState(memoryStorage); var conversationState = new ConversationState(memoryStorage); -adapter.use(new InspectionMiddleware(inspectionState, userState, conversationState, new MicrosoftAppCredentials(process.env.MicrosoftAppId, process.env.MicrosoftAppPassword))); +adapter.use(new InspectionMiddleware(inspectionState, userState, conversationState, { appId: process.env.MicrosoftAppId, appPassword: process.env.MicrosoftAppPassword })); adapter.onTurnError = async (context, error) => { console.error(`\n [onTurnError]: ${ error }`); diff --git a/libraries/functional-tests/package.json b/libraries/functional-tests/package.json index 72b79524e1..0654b5224f 100644 --- a/libraries/functional-tests/package.json +++ b/libraries/functional-tests/package.json @@ -9,9 +9,6 @@ "directories": { "test": "tests" }, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, "keywords": [], "author": "", "license": "MIT" diff --git a/libraries/testbot/tests/dialogs/cancelAndHelpDialog.test.js b/libraries/testbot/tests/dialogs/cancelAndHelpDialog.test.js index d5ad6d1dd9..5f848bd363 100644 --- a/libraries/testbot/tests/dialogs/cancelAndHelpDialog.test.js +++ b/libraries/testbot/tests/dialogs/cancelAndHelpDialog.test.js @@ -48,7 +48,7 @@ describe('CancelAndHelpDialog', () => { reply = await client.sendActivity(testData); assert.strictEqual(reply.text, 'Cancelling...'); - assert.strictEqual(client.dialogTurnResult.status, 'cancelled'); + assert.strictEqual(client.dialogTurnResult.status, 'complete'); }); }); }); diff --git a/libraries/testbot/tests/dialogs/testData/bookingDialogTestCases.js b/libraries/testbot/tests/dialogs/testData/bookingDialogTestCases.js index 8dcbc3e0c6..ed2084f5cb 100644 --- a/libraries/testbot/tests/dialogs/testData/bookingDialogTestCases.js +++ b/libraries/testbot/tests/dialogs/testData/bookingDialogTestCases.js @@ -110,7 +110,7 @@ module.exports = [ ['hi', 'To what city would you like to travel?'], ['cancel', 'Cancelling...'] ], - expectedStatus: 'cancelled', + expectedStatus: 'complete', expectedResult: undefined, }, { @@ -121,7 +121,7 @@ module.exports = [ ['Seattle','From what city will you be travelling?'], ['cancel', 'Cancelling...'] ], - expectedStatus: 'cancelled', + expectedStatus: 'complete', expectedResult: undefined, }, { @@ -133,7 +133,7 @@ module.exports = [ ['New York', 'On what date would you like to travel?'], ['cancel', 'Cancelling...'] ], - expectedStatus: 'cancelled', + expectedStatus: 'complete', expectedResult: undefined, }, { @@ -146,7 +146,7 @@ module.exports = [ ['tomorrow', `Please confirm, I have you traveling to: Seattle from: New York on: ${ tomorrow }. Is this correct? (1) Yes or (2) No`], ['cancel', 'Cancelling...'] ], - expectedStatus: 'cancelled', + expectedStatus: 'complete', expectedResult: undefined, } ] \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9a6e3130f4..2e7f2184b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,7072 +1,12322 @@ { - "name": "botbuilder-packages", - "requires": true, - "lockfileVersion": 1, - "dependencies": { - "@azure/ms-rest-js": { - "version": "1.8.13", - "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-1.8.13.tgz", - "integrity": "sha512-jAa6Y2XrvwbEqkaEXDHK+ReNo0WnCPS+LgQ1dRAJUUNxK4CghF5u+SXsVtPENritilVE7FVteqsLOtlhTk+haA==", - "requires": { - "@types/tunnel": "0.0.0", - "axios": "^0.19.0", - "form-data": "^2.3.2", - "tough-cookie": "^2.4.3", - "tslib": "^1.9.2", - "tunnel": "0.0.6", - "uuid": "^3.2.1", - "xml2js": "^0.4.19" - }, - "dependencies": { - "tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" - } - } - }, - "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "@babel/highlight": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", - "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - } - } - }, - "@lerna/add": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/add/-/add-3.13.1.tgz", - "integrity": "sha512-cXk42YbuhzEnADCK8Qte5laC9Qo03eJLVnr0qKY85jQUM/T4URe3IIUemqpg0CpVATrB+Vz+iNdeqw9ng1iALw==", - "requires": { - "@lerna/bootstrap": "3.13.1", - "@lerna/command": "3.13.1", - "@lerna/filter-options": "3.13.0", - "@lerna/npm-conf": "3.13.0", - "@lerna/validation-error": "3.13.0", - "dedent": "^0.7.0", - "npm-package-arg": "^6.1.0", - "p-map": "^1.2.0", - "pacote": "^9.5.0", - "semver": "^5.5.0" - } - }, - "@lerna/batch-packages": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/batch-packages/-/batch-packages-3.13.0.tgz", - "integrity": "sha512-TgLBTZ7ZlqilGnzJ3xh1KdAHcySfHytgNRTdG9YomfriTU6kVfp1HrXxKJYVGs7ClPUNt2CTFEOkw0tMBronjw==", - "requires": { - "@lerna/package-graph": "3.13.0", - "@lerna/validation-error": "3.13.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/bootstrap": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.13.1.tgz", - "integrity": "sha512-mKdi5Ds5f82PZwEFyB9/W60I3iELobi1i87sTeVrbJh/um7GvqpSPy7kG/JPxyOdMpB2njX6LiJgw+7b6BEPWw==", - "requires": { - "@lerna/batch-packages": "3.13.0", - "@lerna/command": "3.13.1", - "@lerna/filter-options": "3.13.0", - "@lerna/has-npm-version": "3.13.0", - "@lerna/npm-install": "3.13.0", - "@lerna/package-graph": "3.13.0", - "@lerna/pulse-till-done": "3.13.0", - "@lerna/rimraf-dir": "3.13.0", - "@lerna/run-lifecycle": "3.13.0", - "@lerna/run-parallel-batches": "3.13.0", - "@lerna/symlink-binary": "3.13.0", - "@lerna/symlink-dependencies": "3.13.0", - "@lerna/validation-error": "3.13.0", - "dedent": "^0.7.0", - "get-port": "^3.2.0", - "multimatch": "^2.1.0", - "npm-package-arg": "^6.1.0", - "npmlog": "^4.1.2", - "p-finally": "^1.0.0", - "p-map": "^1.2.0", - "p-map-series": "^1.0.0", - "p-waterfall": "^1.0.0", - "read-package-tree": "^5.1.6", - "semver": "^5.5.0" - } - }, - "@lerna/changed": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/changed/-/changed-3.13.1.tgz", - "integrity": "sha512-BRXitEJGOkoudbxEewW7WhjkLxFD+tTk4PrYpHLyCBk63pNTWtQLRE6dc1hqwh4emwyGncoyW6RgXfLgMZgryw==", - "requires": { - "@lerna/collect-updates": "3.13.0", - "@lerna/command": "3.13.1", - "@lerna/listable": "3.13.0", - "@lerna/output": "3.13.0", - "@lerna/version": "3.13.1" - } - }, - "@lerna/check-working-tree": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/check-working-tree/-/check-working-tree-3.13.0.tgz", - "integrity": "sha512-dsdO15NXX5To+Q53SYeCrBEpiqv4m5VkaPZxbGQZNwoRen1MloXuqxSymJANQn+ZLEqarv5V56gydebeROPH5A==", - "requires": { - "@lerna/describe-ref": "3.13.0", - "@lerna/validation-error": "3.13.0" - } - }, - "@lerna/child-process": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/child-process/-/child-process-3.13.0.tgz", - "integrity": "sha512-0iDS8y2jiEucD4fJHEzKoc8aQJgm7s+hG+0RmDNtfT0MM3n17pZnf5JOMtS1FJp+SEXOjMKQndyyaDIPFsnp6A==", - "requires": { - "chalk": "^2.3.1", - "execa": "^1.0.0", - "strong-log-transformer": "^2.0.0" - } - }, - "@lerna/clean": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/clean/-/clean-3.13.1.tgz", - "integrity": "sha512-myGIaXv7RUO2qCFZXvx8SJeI+eN6y9SUD5zZ4/LvNogbOiEIlujC5lUAqK65rAHayQ9ltSa/yK6Xv510xhZXZQ==", - "requires": { - "@lerna/command": "3.13.1", - "@lerna/filter-options": "3.13.0", - "@lerna/prompt": "3.13.0", - "@lerna/pulse-till-done": "3.13.0", - "@lerna/rimraf-dir": "3.13.0", - "p-map": "^1.2.0", - "p-map-series": "^1.0.0", - "p-waterfall": "^1.0.0" - } - }, - "@lerna/cli": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/cli/-/cli-3.13.0.tgz", - "integrity": "sha512-HgFGlyCZbYaYrjOr3w/EsY18PdvtsTmDfpUQe8HwDjXlPeCCUgliZjXLOVBxSjiOvPeOSwvopwIHKWQmYbwywg==", - "requires": { - "@lerna/global-options": "3.13.0", - "dedent": "^0.7.0", - "npmlog": "^4.1.2", - "yargs": "^12.0.1" - } - }, - "@lerna/collect-updates": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-3.13.0.tgz", - "integrity": "sha512-uR3u6uTzrS1p46tHQ/mlHog/nRJGBqskTHYYJbgirujxm6FqNh7Do+I1Q/7zSee407G4lzsNxZdm8IL927HemQ==", - "requires": { - "@lerna/child-process": "3.13.0", - "@lerna/describe-ref": "3.13.0", - "minimatch": "^3.0.4", - "npmlog": "^4.1.2", - "slash": "^1.0.0" - } - }, - "@lerna/command": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/command/-/command-3.13.1.tgz", - "integrity": "sha512-SYWezxX+iheWvzRoHCrbs8v5zHPaxAx3kWvZhqi70vuGsdOVAWmaG4IvHLn11ztS+Vpd5PM+ztBWSbnykpLFKQ==", - "requires": { - "@lerna/child-process": "3.13.0", - "@lerna/package-graph": "3.13.0", - "@lerna/project": "3.13.1", - "@lerna/validation-error": "3.13.0", - "@lerna/write-log-file": "3.13.0", - "dedent": "^0.7.0", - "execa": "^1.0.0", - "is-ci": "^1.0.10", - "lodash": "^4.17.5", - "npmlog": "^4.1.2" - } - }, - "@lerna/conventional-commits": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-3.13.0.tgz", - "integrity": "sha512-BeAgcNXuocmLhPxnmKU2Vy8YkPd/Uo+vu2i/p3JGsUldzrPC8iF3IDxH7fuXpEFN2Nfogu7KHachd4tchtOppA==", - "requires": { - "@lerna/validation-error": "3.13.0", - "conventional-changelog-angular": "^5.0.3", - "conventional-changelog-core": "^3.1.6", - "conventional-recommended-bump": "^4.0.4", - "fs-extra": "^7.0.0", - "get-stream": "^4.0.0", - "npm-package-arg": "^6.1.0", - "npmlog": "^4.1.2", - "pify": "^3.0.0", - "semver": "^5.5.0" - }, - "dependencies": { - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { - "pump": "^3.0.0" - } - } - } - }, - "@lerna/create": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/create/-/create-3.13.1.tgz", - "integrity": "sha512-pLENMXgTkQuvKxAopjKeoLOv9fVUCnpTUD7aLrY5d95/1xqSZlnsOcQfUYcpMf3GpOvHc8ILmI5OXkPqjAf54g==", - "requires": { - "@lerna/child-process": "3.13.0", - "@lerna/command": "3.13.1", - "@lerna/npm-conf": "3.13.0", - "@lerna/validation-error": "3.13.0", - "camelcase": "^5.0.0", - "dedent": "^0.7.0", - "fs-extra": "^7.0.0", - "globby": "^8.0.1", - "init-package-json": "^1.10.3", - "npm-package-arg": "^6.1.0", - "p-reduce": "^1.0.0", - "pacote": "^9.5.0", - "pify": "^3.0.0", - "semver": "^5.5.0", - "slash": "^1.0.0", - "validate-npm-package-license": "^3.0.3", - "validate-npm-package-name": "^3.0.0", - "whatwg-url": "^7.0.0" - }, - "dependencies": { - "whatwg-url": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", - "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - } - } - }, - "@lerna/create-symlink": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/create-symlink/-/create-symlink-3.13.0.tgz", - "integrity": "sha512-PTvg3jAAJSAtLFoZDsuTMv1wTOC3XYIdtg54k7uxIHsP8Ztpt+vlilY/Cni0THAqEMHvfiToel76Xdta4TU21Q==", - "requires": { - "cmd-shim": "^2.0.2", - "fs-extra": "^7.0.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/describe-ref": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/describe-ref/-/describe-ref-3.13.0.tgz", - "integrity": "sha512-UJefF5mLxLae9I2Sbz5RLYGbqbikRuMqdgTam0MS5OhXnyuuKYBUpwBshCURNb1dPBXTQhSwc7+oUhORx8ojCg==", - "requires": { - "@lerna/child-process": "3.13.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/diff": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/diff/-/diff-3.13.1.tgz", - "integrity": "sha512-cKqmpONO57mdvxtp8e+l5+tjtmF04+7E+O0QEcLcNUAjC6UR2OSM77nwRCXDukou/1h72JtWs0jjcdYLwAmApg==", - "requires": { - "@lerna/child-process": "3.13.0", - "@lerna/command": "3.13.1", - "@lerna/validation-error": "3.13.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/exec": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/exec/-/exec-3.13.1.tgz", - "integrity": "sha512-I34wEP9lrAqqM7tTXLDxv/6454WFzrnXDWpNDbiKQiZs6SIrOOjmm6I4FiQsx+rU3o9d+HkC6tcUJRN5mlJUgA==", - "requires": { - "@lerna/batch-packages": "3.13.0", - "@lerna/child-process": "3.13.0", - "@lerna/command": "3.13.1", - "@lerna/filter-options": "3.13.0", - "@lerna/run-parallel-batches": "3.13.0", - "@lerna/validation-error": "3.13.0" - } - }, - "@lerna/filter-options": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-3.13.0.tgz", - "integrity": "sha512-SRp7DCo9zrf+7NkQxZMkeyO1GRN6GICoB9UcBAbXhLbWisT37Cx5/6+jh49gYB63d/0/WYHSEPMlheUrpv1Srw==", - "requires": { - "@lerna/collect-updates": "3.13.0", - "@lerna/filter-packages": "3.13.0", - "dedent": "^0.7.0" - } - }, - "@lerna/filter-packages": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/filter-packages/-/filter-packages-3.13.0.tgz", - "integrity": "sha512-RWiZWyGy3Mp7GRVBn//CacSnE3Kw82PxE4+H6bQ3pDUw/9atXn7NRX+gkBVQIYeKamh7HyumJtyOKq3Pp9BADQ==", - "requires": { - "@lerna/validation-error": "3.13.0", - "multimatch": "^2.1.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/get-npm-exec-opts": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-3.13.0.tgz", - "integrity": "sha512-Y0xWL0rg3boVyJk6An/vurKzubyJKtrxYv2sj4bB8Mc5zZ3tqtv0ccbOkmkXKqbzvNNF7VeUt1OJ3DRgtC/QZw==", - "requires": { - "npmlog": "^4.1.2" - } - }, - "@lerna/get-packed": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/get-packed/-/get-packed-3.13.0.tgz", - "integrity": "sha512-EgSim24sjIjqQDC57bgXD9l22/HCS93uQBbGpkzEOzxAVzEgpZVm7Fm1t8BVlRcT2P2zwGnRadIvxTbpQuDPTg==", - "requires": { - "fs-extra": "^7.0.0", - "ssri": "^6.0.1", - "tar": "^4.4.8" - }, - "dependencies": { - "tar": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", - "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" - } - } - }, - "@lerna/github-client": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/github-client/-/github-client-3.13.1.tgz", - "integrity": "sha512-iPLUp8FFoAKGURksYEYZzfuo9TRA+NepVlseRXFaWlmy36dCQN20AciINpoXiXGoHcEUHXUKHQvY3ARFdMlf3w==", - "requires": { - "@lerna/child-process": "3.13.0", - "@octokit/plugin-enterprise-rest": "^2.1.1", - "@octokit/rest": "^16.16.0", - "git-url-parse": "^11.1.2", - "npmlog": "^4.1.2" - } - }, - "@lerna/global-options": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/global-options/-/global-options-3.13.0.tgz", - "integrity": "sha512-SlZvh1gVRRzYLVluz9fryY1nJpZ0FHDGB66U9tFfvnnxmueckRQxLopn3tXj3NU1kc3QANT2I5BsQkOqZ4TEFQ==" - }, - "@lerna/has-npm-version": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/has-npm-version/-/has-npm-version-3.13.0.tgz", - "integrity": "sha512-Oqu7DGLnrMENPm+bPFGOHnqxK8lCnuYr6bk3g/CoNn8/U0qgFvHcq6Iv8/Z04TsvleX+3/RgauSD2kMfRmbypg==", - "requires": { - "@lerna/child-process": "3.13.0", - "semver": "^5.5.0" - } - }, - "@lerna/import": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/import/-/import-3.13.1.tgz", - "integrity": "sha512-A1Vk1siYx1XkRl6w+zkaA0iptV5TIynVlHPR9S7NY0XAfhykjztYVvwtxarlh6+VcNrO9We6if0+FXCrfDEoIg==", - "requires": { - "@lerna/child-process": "3.13.0", - "@lerna/command": "3.13.1", - "@lerna/prompt": "3.13.0", - "@lerna/pulse-till-done": "3.13.0", - "@lerna/validation-error": "3.13.0", - "dedent": "^0.7.0", - "fs-extra": "^7.0.0", - "p-map-series": "^1.0.0" - } - }, - "@lerna/init": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/init/-/init-3.13.1.tgz", - "integrity": "sha512-M59WACqim8WkH5FQEGOCEZ89NDxCKBfFTx4ZD5ig3LkGyJ8RdcJq5KEfpW/aESuRE9JrZLzVr0IjKbZSxzwEMA==", - "requires": { - "@lerna/child-process": "3.13.0", - "@lerna/command": "3.13.1", - "fs-extra": "^7.0.0", - "p-map": "^1.2.0", - "write-json-file": "^2.3.0" - } - }, - "@lerna/link": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/link/-/link-3.13.1.tgz", - "integrity": "sha512-N3h3Fj1dcea+1RaAoAdy4g2m3fvU7m89HoUn5X/Zcw5n2kPoK8kTO+NfhNAatfRV8VtMXst8vbNrWQQtfm0FFw==", - "requires": { - "@lerna/command": "3.13.1", - "@lerna/package-graph": "3.13.0", - "@lerna/symlink-dependencies": "3.13.0", - "p-map": "^1.2.0", - "slash": "^1.0.0" - } - }, - "@lerna/list": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/list/-/list-3.13.1.tgz", - "integrity": "sha512-635iRbdgd9gNvYLLIbYdQCQLr+HioM5FGJLFS0g3DPGygr6iDR8KS47hzCRGH91LU9NcM1mD1RoT/AChF+QbiA==", - "requires": { - "@lerna/command": "3.13.1", - "@lerna/filter-options": "3.13.0", - "@lerna/listable": "3.13.0", - "@lerna/output": "3.13.0" - } - }, - "@lerna/listable": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/listable/-/listable-3.13.0.tgz", - "integrity": "sha512-liYJ/WBUYP4N4MnSVZuLUgfa/jy3BZ02/1Om7xUY09xGVSuNVNEeB8uZUMSC+nHqFHIsMPZ8QK9HnmZb1E/eTA==", - "requires": { - "@lerna/batch-packages": "3.13.0", - "chalk": "^2.3.1", - "columnify": "^1.5.4" - } - }, - "@lerna/log-packed": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/log-packed/-/log-packed-3.13.0.tgz", - "integrity": "sha512-Rmjrcz+6aM6AEcEVWmurbo8+AnHOvYtDpoeMMJh9IZ9SmZr2ClXzmD7wSvjTQc8BwOaiWjjC/ukcT0UYA2m7wg==", - "requires": { - "byte-size": "^4.0.3", - "columnify": "^1.5.4", - "has-unicode": "^2.0.1", - "npmlog": "^4.1.2" - } - }, - "@lerna/npm-conf": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-conf/-/npm-conf-3.13.0.tgz", - "integrity": "sha512-Jg2kANsGnhg+fbPEzE0X9nX5oviEAvWj0nYyOkcE+cgWuT7W0zpnPXC4hA4C5IPQGhwhhh0IxhWNNHtjTuw53g==", - "requires": { - "config-chain": "^1.1.11", - "pify": "^3.0.0" - } - }, - "@lerna/npm-dist-tag": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-dist-tag/-/npm-dist-tag-3.13.0.tgz", - "integrity": "sha512-mcuhw34JhSRFrbPn0vedbvgBTvveG52bR2lVE3M3tfE8gmR/cKS/EJFO4AUhfRKGCTFn9rjaSEzlFGYV87pemQ==", - "requires": { - "figgy-pudding": "^3.5.1", - "npm-package-arg": "^6.1.0", - "npm-registry-fetch": "^3.9.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/npm-install": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-3.13.0.tgz", - "integrity": "sha512-qNyfts//isYQxore6fsPorNYJmPVKZ6tOThSH97tP0aV91zGMtrYRqlAoUnDwDdAjHPYEM16hNujg2wRmsqqIw==", - "requires": { - "@lerna/child-process": "3.13.0", - "@lerna/get-npm-exec-opts": "3.13.0", - "fs-extra": "^7.0.0", - "npm-package-arg": "^6.1.0", - "npmlog": "^4.1.2", - "signal-exit": "^3.0.2", - "write-pkg": "^3.1.0" - } - }, - "@lerna/npm-publish": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-3.13.0.tgz", - "integrity": "sha512-y4WO0XTaf9gNRkI7as6P2ItVDOxmYHwYto357fjybcnfXgMqEA94c3GJ++jU41j0A9vnmYC6/XxpTd9sVmH9tA==", - "requires": { - "@lerna/run-lifecycle": "3.13.0", - "figgy-pudding": "^3.5.1", - "fs-extra": "^7.0.0", - "libnpmpublish": "^1.1.1", - "npmlog": "^4.1.2", - "pify": "^3.0.0", - "read-package-json": "^2.0.13" - } - }, - "@lerna/npm-run-script": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-3.13.0.tgz", - "integrity": "sha512-hiL3/VeVp+NFatBjkGN8mUdX24EfZx9rQlSie0CMgtjc7iZrtd0jCguLomSCRHYjJuvqgbp+LLYo7nHVykfkaQ==", - "requires": { - "@lerna/child-process": "3.13.0", - "@lerna/get-npm-exec-opts": "3.13.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/output": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/output/-/output-3.13.0.tgz", - "integrity": "sha512-7ZnQ9nvUDu/WD+bNsypmPG5MwZBwu86iRoiW6C1WBuXXDxM5cnIAC1m2WxHeFnjyMrYlRXM9PzOQ9VDD+C15Rg==", - "requires": { - "npmlog": "^4.1.2" - } - }, - "@lerna/pack-directory": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/pack-directory/-/pack-directory-3.13.1.tgz", - "integrity": "sha512-kXnyqrkQbCIZOf1054N88+8h0ItC7tUN5v9ca/aWpx298gsURpxUx/1TIKqijL5TOnHMyIkj0YJmnH/PyBVLKA==", - "requires": { - "@lerna/get-packed": "3.13.0", - "@lerna/package": "3.13.0", - "@lerna/run-lifecycle": "3.13.0", - "figgy-pudding": "^3.5.1", - "npm-packlist": "^1.4.1", - "npmlog": "^4.1.2", - "tar": "^4.4.8", - "temp-write": "^3.4.0" - }, - "dependencies": { - "tar": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", - "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" - } - } - }, - "@lerna/package": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/package/-/package-3.13.0.tgz", - "integrity": "sha512-kSKO0RJQy093BufCQnkhf1jB4kZnBvL7kK5Ewolhk5gwejN+Jofjd8DGRVUDUJfQ0CkW1o6GbUeZvs8w8VIZDg==", - "requires": { - "load-json-file": "^4.0.0", - "npm-package-arg": "^6.1.0", - "write-pkg": "^3.1.0" - } - }, - "@lerna/package-graph": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-3.13.0.tgz", - "integrity": "sha512-3mRF1zuqFE1HEFmMMAIggXy+f+9cvHhW/jzaPEVyrPNLKsyfJQtpTNzeI04nfRvbAh+Gd2aNksvaW/w3xGJnnw==", - "requires": { - "@lerna/validation-error": "3.13.0", - "npm-package-arg": "^6.1.0", - "semver": "^5.5.0" - } - }, - "@lerna/project": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/project/-/project-3.13.1.tgz", - "integrity": "sha512-/GoCrpsCCTyb9sizk1+pMBrIYchtb+F1uCOn3cjn9yenyG/MfYEnlfrbV5k/UDud0Ei75YBLbmwCbigHkAKazQ==", - "requires": { - "@lerna/package": "3.13.0", - "@lerna/validation-error": "3.13.0", - "cosmiconfig": "^5.1.0", - "dedent": "^0.7.0", - "dot-prop": "^4.2.0", - "glob-parent": "^3.1.0", - "globby": "^8.0.1", - "load-json-file": "^4.0.0", - "npmlog": "^4.1.2", - "p-map": "^1.2.0", - "resolve-from": "^4.0.0", - "write-json-file": "^2.3.0" - }, - "dependencies": { - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "@lerna/prompt": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/prompt/-/prompt-3.13.0.tgz", - "integrity": "sha512-P+lWSFokdyvYpkwC3it9cE0IF2U5yy2mOUbGvvE4iDb9K7TyXGE+7lwtx2thtPvBAfIb7O13POMkv7df03HJeA==", - "requires": { - "inquirer": "^6.2.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/publish": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/publish/-/publish-3.13.1.tgz", - "integrity": "sha512-KhCJ9UDx76HWCF03i5TD7z5lX+2yklHh5SyO8eDaLptgdLDQ0Z78lfGj3JhewHU2l46FztmqxL/ss0IkWHDL+g==", - "requires": { - "@lerna/batch-packages": "3.13.0", - "@lerna/check-working-tree": "3.13.0", - "@lerna/child-process": "3.13.0", - "@lerna/collect-updates": "3.13.0", - "@lerna/command": "3.13.1", - "@lerna/describe-ref": "3.13.0", - "@lerna/log-packed": "3.13.0", - "@lerna/npm-conf": "3.13.0", - "@lerna/npm-dist-tag": "3.13.0", - "@lerna/npm-publish": "3.13.0", - "@lerna/output": "3.13.0", - "@lerna/pack-directory": "3.13.1", - "@lerna/prompt": "3.13.0", - "@lerna/pulse-till-done": "3.13.0", - "@lerna/run-lifecycle": "3.13.0", - "@lerna/run-parallel-batches": "3.13.0", - "@lerna/validation-error": "3.13.0", - "@lerna/version": "3.13.1", - "figgy-pudding": "^3.5.1", - "fs-extra": "^7.0.0", - "libnpmaccess": "^3.0.1", - "npm-package-arg": "^6.1.0", - "npm-registry-fetch": "^3.9.0", - "npmlog": "^4.1.2", - "p-finally": "^1.0.0", - "p-map": "^1.2.0", - "p-pipe": "^1.2.0", - "p-reduce": "^1.0.0", - "pacote": "^9.5.0", - "semver": "^5.5.0" - } - }, - "@lerna/pulse-till-done": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/pulse-till-done/-/pulse-till-done-3.13.0.tgz", - "integrity": "sha512-1SOHpy7ZNTPulzIbargrgaJX387csN7cF1cLOGZiJQA6VqnS5eWs2CIrG8i8wmaUavj2QlQ5oEbRMVVXSsGrzA==", - "requires": { - "npmlog": "^4.1.2" - } - }, - "@lerna/resolve-symlink": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/resolve-symlink/-/resolve-symlink-3.13.0.tgz", - "integrity": "sha512-Lc0USSFxwDxUs5JvIisS8JegjA6SHSAWJCMvi2osZx6wVRkEDlWG2B1JAfXUzCMNfHoZX0/XX9iYZ+4JIpjAtg==", - "requires": { - "fs-extra": "^7.0.0", - "npmlog": "^4.1.2", - "read-cmd-shim": "^1.0.1" - } - }, - "@lerna/rimraf-dir": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-3.13.0.tgz", - "integrity": "sha512-kte+pMemulre8cmPqljxIYjCmdLByz8DgHBHXB49kz2EiPf8JJ+hJFt0PzEubEyJZ2YE2EVAx5Tv5+NfGNUQyQ==", - "requires": { - "@lerna/child-process": "3.13.0", - "npmlog": "^4.1.2", - "path-exists": "^3.0.0", - "rimraf": "^2.6.2" - } - }, - "@lerna/run": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/run/-/run-3.13.1.tgz", - "integrity": "sha512-nv1oj7bsqppWm1M4ifN+/IIbVu9F4RixrbQD2okqDGYne4RQPAXyb5cEZuAzY/wyGTWWiVaZ1zpj5ogPWvH0bw==", - "requires": { - "@lerna/batch-packages": "3.13.0", - "@lerna/command": "3.13.1", - "@lerna/filter-options": "3.13.0", - "@lerna/npm-run-script": "3.13.0", - "@lerna/output": "3.13.0", - "@lerna/run-parallel-batches": "3.13.0", - "@lerna/timer": "3.13.0", - "@lerna/validation-error": "3.13.0", - "p-map": "^1.2.0" - } - }, - "@lerna/run-lifecycle": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/run-lifecycle/-/run-lifecycle-3.13.0.tgz", - "integrity": "sha512-oyiaL1biZdjpmjh6X/5C4w07wNFyiwXSSHH5GQB4Ay4BPwgq9oNhCcxRoi0UVZlZ1YwzSW8sTwLgj8emkIo3Yg==", - "requires": { - "@lerna/npm-conf": "3.13.0", - "figgy-pudding": "^3.5.1", - "npm-lifecycle": "^2.1.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/run-parallel-batches": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/run-parallel-batches/-/run-parallel-batches-3.13.0.tgz", - "integrity": "sha512-bICFBR+cYVF1FFW+Tlm0EhWDioTUTM6dOiVziDEGE1UZha1dFkMYqzqdSf4bQzfLS31UW/KBd/2z8jy2OIjEjg==", - "requires": { - "p-map": "^1.2.0", - "p-map-series": "^1.0.0" - } - }, - "@lerna/symlink-binary": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/symlink-binary/-/symlink-binary-3.13.0.tgz", - "integrity": "sha512-obc4Y6jxywkdaCe+DB0uTxYqP0IQ8mFWvN+k/YMbwH4G2h7M7lCBWgPy8e7xw/50+1II9tT2sxgx+jMus1sTJg==", - "requires": { - "@lerna/create-symlink": "3.13.0", - "@lerna/package": "3.13.0", - "fs-extra": "^7.0.0", - "p-map": "^1.2.0" - } - }, - "@lerna/symlink-dependencies": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/symlink-dependencies/-/symlink-dependencies-3.13.0.tgz", - "integrity": "sha512-7CyN5WYEPkbPLbqHBIQg/YiimBzb5cIGQB0E9IkLs3+racq2vmUNQZn38LOaazQacAA83seB+zWSxlI6H+eXSg==", - "requires": { - "@lerna/create-symlink": "3.13.0", - "@lerna/resolve-symlink": "3.13.0", - "@lerna/symlink-binary": "3.13.0", - "fs-extra": "^7.0.0", - "p-finally": "^1.0.0", - "p-map": "^1.2.0", - "p-map-series": "^1.0.0" - } - }, - "@lerna/timer": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/timer/-/timer-3.13.0.tgz", - "integrity": "sha512-RHWrDl8U4XNPqY5MQHkToWS9jHPnkLZEt5VD+uunCKTfzlxGnRCr3/zVr8VGy/uENMYpVP3wJa4RKGY6M0vkRw==" - }, - "@lerna/validation-error": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/validation-error/-/validation-error-3.13.0.tgz", - "integrity": "sha512-SiJP75nwB8GhgwLKQfdkSnDufAaCbkZWJqEDlKOUPUvVOplRGnfL+BPQZH5nvq2BYSRXsksXWZ4UHVnQZI/HYA==", - "requires": { - "npmlog": "^4.1.2" - } - }, - "@lerna/version": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/version/-/version-3.13.1.tgz", - "integrity": "sha512-WpfKc5jZBBOJ6bFS4atPJEbHSiywQ/Gcd+vrwaEGyQHWHQZnPTvhqLuq3q9fIb9sbuhH5pSY6eehhuBrKqTnjg==", - "requires": { - "@lerna/batch-packages": "3.13.0", - "@lerna/check-working-tree": "3.13.0", - "@lerna/child-process": "3.13.0", - "@lerna/collect-updates": "3.13.0", - "@lerna/command": "3.13.1", - "@lerna/conventional-commits": "3.13.0", - "@lerna/github-client": "3.13.1", - "@lerna/output": "3.13.0", - "@lerna/prompt": "3.13.0", - "@lerna/run-lifecycle": "3.13.0", - "@lerna/validation-error": "3.13.0", - "chalk": "^2.3.1", - "dedent": "^0.7.0", - "minimatch": "^3.0.4", - "npmlog": "^4.1.2", - "p-map": "^1.2.0", - "p-pipe": "^1.2.0", - "p-reduce": "^1.0.0", - "p-waterfall": "^1.0.0", - "semver": "^5.5.0", - "slash": "^1.0.0", - "temp-write": "^3.4.0" - } - }, - "@lerna/write-log-file": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/write-log-file/-/write-log-file-3.13.0.tgz", - "integrity": "sha512-RibeMnDPvlL8bFYW5C8cs4mbI3AHfQef73tnJCQ/SgrXZHehmHnsyWUiE7qDQCAo+B1RfTapvSyFF69iPj326A==", - "requires": { - "npmlog": "^4.1.2", - "write-file-atomic": "^2.3.0" - } - }, - "@mrmlnc/readdir-enhanced": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", - "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", - "requires": { - "call-me-maybe": "^1.0.1", - "glob-to-regexp": "^0.3.0" - } - }, - "@nodelib/fs.stat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", - "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" - }, - "@octokit/endpoint": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-3.2.3.tgz", - "integrity": "sha512-yUPCt4vMIOclox13CUxzuKiPJIFo46b/6GhUnUTw5QySczN1L0DtSxgmIZrZV4SAb9EyAqrceoyrWoYVnfF2AA==", - "requires": { - "deepmerge": "3.2.0", - "is-plain-object": "^2.0.4", - "universal-user-agent": "^2.0.1", - "url-template": "^2.0.8" - } - }, - "@octokit/plugin-enterprise-rest": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-2.2.2.tgz", - "integrity": "sha512-CTZr64jZYhGWNTDGlSJ2mvIlFsm9OEO3LqWn9I/gmoHI4jRBp4kpHoFYNemG4oA75zUAcmbuWblb7jjP877YZw==" - }, - "@octokit/request": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-2.4.2.tgz", - "integrity": "sha512-lxVlYYvwGbKSHXfbPk5vxEA8w4zHOH1wobado4a9EfsyD3Cbhuhus1w0Ye9Ro0eMubGO8kNy5d+xNFisM3Tvaw==", - "requires": { - "@octokit/endpoint": "^3.2.0", - "deprecation": "^1.0.1", - "is-plain-object": "^2.0.4", - "node-fetch": "^2.3.0", - "once": "^1.4.0", - "universal-user-agent": "^2.0.1" - } - }, - "@octokit/rest": { - "version": "16.23.2", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.23.2.tgz", - "integrity": "sha512-ZxiZMaCuqBG/IsbgNRVfGwYsvBb5DjHuMGjJgOrinT+/b+1j1U7PiGyRkHDJdjTGA6N/PsMC2lP2ZybX9579iA==", - "requires": { - "@octokit/request": "2.4.2", - "atob-lite": "^2.0.0", - "before-after-hook": "^1.4.0", - "btoa-lite": "^1.0.0", - "deprecation": "^1.0.1", - "lodash.get": "^4.4.2", - "lodash.set": "^4.3.2", - "lodash.uniq": "^4.5.0", - "octokit-pagination-methods": "^1.1.0", - "once": "^1.4.0", - "universal-user-agent": "^2.0.0", - "url-template": "^2.0.8" - } - }, - "@sinonjs/commons": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", - "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/formatio": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", - "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", - "requires": { - "@sinonjs/commons": "^1", - "@sinonjs/samsam": "^3.1.0" - } - }, - "@sinonjs/samsam": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.2.tgz", - "integrity": "sha512-ILO/rR8LfAb60Y1Yfp9vxfYAASK43NFC2mLzpvLUbCQY/Qu8YwReboseu8aheCEkyElZF2L2T9mHcR2bgdvZyA==", - "requires": { - "@sinonjs/commons": "^1.0.2", - "array-from": "^2.1.1", - "lodash": "^4.17.11" - } - }, - "@sinonjs/text-encoding": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", - "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==" - }, - "@types/eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==" - }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" - }, - "@types/fs-extra": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.1.0.tgz", - "integrity": "sha512-AInn5+UBFIK9FK5xc9yP5e3TQSPNNgjHByqYcj9g5elVBnDQcQL7PlO1CIRy2gWlbwK7UPYqi7vRvFA44dCmYQ==", - "requires": { - "@types/node": "*" - } - }, - "@types/glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", - "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", - "requires": { - "@types/events": "*", - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "@types/handlebars": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@types/handlebars/-/handlebars-4.1.0.tgz", - "integrity": "sha512-gq9YweFKNNB1uFK71eRqsd4niVkXrxHugqWFQkeLRJvGjnxsLr16bYtcsG4tOFwmYi0Bax+wCkbf1reUfdl4kA==", - "requires": { - "handlebars": "*" - } - }, - "@types/highlight.js": { - "version": "9.12.3", - "resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.12.3.tgz", - "integrity": "sha512-pGF/zvYOACZ/gLGWdQH8zSwteQS1epp68yRcVLJMgUck/MjEn/FBYmPub9pXT8C1e4a8YZfHo1CKyV8q1vKUnQ==" - }, - "@types/jsonwebtoken": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.3.2.tgz", - "integrity": "sha512-Mkjljd9DTpkPlrmGfTJvcP4aBU7yO2QmW7wNVhV4/6AEUxYoacqU7FJU/N0yFEHTsIrE4da3rUrjrR5ejicFmA==", - "requires": { - "@types/node": "*" - } - }, - "@types/lodash": { - "version": "4.14.136", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.136.tgz", - "integrity": "sha512-0GJhzBdvsW2RUccNHOBkabI8HZVdOXmXbXhuKlDEd5Vv12P7oAVGfomGp3Ne21o5D/qu1WmthlNKFaoZJJeErA==" - }, - "@types/marked": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.4.2.tgz", - "integrity": "sha512-cDB930/7MbzaGF6U3IwSQp6XBru8xWajF5PV2YZZeV8DyiliTuld11afVztGI9+yJZ29il5E+NpGA6ooV/Cjkg==" - }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" - }, - "@types/node": { - "version": "10.12.26", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.26.tgz", - "integrity": "sha512-nMRqS+mL1TOnIJrL6LKJcNZPB8V3eTfRo9FQA2b5gDvrHurC8XbSA86KNe0dShlEL7ReWJv/OU9NL7Z0dnqWTg==" - }, - "@types/shelljs": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.8.5.tgz", - "integrity": "sha512-bZgjwIWu9gHCjirKJoOlLzGi5N0QgZ5t7EXEuoqyWCHTuSddURXo3FOBYDyRPNOWzZ6NbkLvZnVkn483Y/tvcQ==", - "requires": { - "@types/glob": "*", - "@types/node": "*" - } - }, - "@types/tunnel": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.0.tgz", - "integrity": "sha512-FGDp0iBRiBdPjOgjJmn1NH0KDLN+Z8fRmo+9J7XGBhubq1DPrGrbmG4UTlGzrpbCpesMqD0sWkzi27EYkOMHyg==", - "requires": { - "@types/node": "*" - } - }, - "@typescript-eslint/eslint-plugin": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.12.0.tgz", - "integrity": "sha512-J/ZTZF+pLNqjXBGNfq5fahsoJ4vJOkYbitWPavA05IrZ7BXUaf4XWlhUB/ic1lpOGTRpLWF+PLAePjiHp6dz8g==", - "requires": { - "@typescript-eslint/experimental-utils": "1.12.0", - "eslint-utils": "^1.3.1", - "functional-red-black-tree": "^1.0.1", - "regexpp": "^2.0.1", - "tsutils": "^3.7.0" - } - }, - "@typescript-eslint/experimental-utils": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.12.0.tgz", - "integrity": "sha512-s0soOTMJloytr9GbPteMLNiO2HvJ+qgQkRNplABXiVw6vq7uQRvidkby64Gqt/nA7pys74HksHwRULaB/QRVyw==", - "requires": { - "@typescript-eslint/typescript-estree": "1.12.0", - "eslint-scope": "^4.0.0" - } - }, - "@typescript-eslint/parser": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.12.0.tgz", - "integrity": "sha512-0uzbaa9ZLCA5yMWJywnJJ7YVENKGWVUhJDV5UrMoldC5HoI54W5kkdPhTfmtFKpPFp93MIwmJj0/61ztvmz5Dw==", - "requires": { - "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "1.12.0", - "@typescript-eslint/typescript-estree": "1.12.0", - "eslint-visitor-keys": "^1.0.0" - } - }, - "@typescript-eslint/typescript-estree": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.12.0.tgz", - "integrity": "sha512-nwN6yy//XcVhFs0ZyU+teJHB8tbCm7AIA8mu6E2r5hu6MajwYBY3Uwop7+rPZWUN/IUOHpL8C+iUPMDVYUU3og==", - "requires": { - "lodash.unescape": "4.0.1", - "semver": "5.5.0" - }, - "dependencies": { - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" - } - } - }, - "JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "acorn": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.0.tgz", - "integrity": "sha512-8oe72N3WPMjA+2zVG71Ia0nXZ8DpQH+QyyHO+p06jT8eg8FGG3FbcUIi8KziHlAfheJQZeoqbvq1mQSQHXKYLw==" - }, - "acorn-jsx": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz", - "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==" - }, - "adal-node": { - "version": "0.1.28", - "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz", - "integrity": "sha1-RoxLs+u9lrEnBmn0ucuk4AZepIU=", - "requires": { - "@types/node": "^8.0.47", - "async": ">=0.6.0", - "date-utils": "*", - "jws": "3.x.x", - "request": ">= 2.52.0", - "underscore": ">= 1.3.1", - "uuid": "^3.1.0", - "xmldom": ">= 0.1.x", - "xpath.js": "~1.1.0" - }, - "dependencies": { - "@types/node": { - "version": "8.10.40", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.40.tgz", - "integrity": "sha512-RRSjdwz63kS4u7edIwJUn8NqKLLQ6LyqF/X4+4jp38MBT3Vwetewi2N4dgJEshLbDwNgOJXNYoOwzVZUSSLhkQ==" - } - } - }, - "agentkeepalive": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.2.tgz", - "integrity": "sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==", - "requires": { - "humanize-ms": "^1.2.1" - } - }, - "ajv": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.1.tgz", - "integrity": "sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==", - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==" - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "http://bbnpm.azurewebsites.net/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - }, - "dependencies": { - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - } - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" - }, - "array-differ": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", - "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=" - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" - }, - "array-from": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", - "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=" - }, - "array-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=" - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" - }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "http://bbnpm.azurewebsites.net/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==" - }, - "async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", - "requires": { - "lodash": "^4.14.0" - } - }, - "asynckit": { - "version": "0.4.0", - "resolved": "http://bbnpm.azurewebsites.net/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" - }, - "atob-lite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz", - "integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "http://bbnpm.azurewebsites.net/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" - }, - "axios": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", - "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", - "requires": { - "follow-redirects": "1.5.10", - "is-buffer": "^2.0.2" - }, - "dependencies": { - "is-buffer": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", - "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" - } - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "http://bbnpm.azurewebsites.net/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "before-after-hook": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-1.4.0.tgz", - "integrity": "sha512-l5r9ir56nda3qu14nAXIlyq1MmUSs0meCIaFAh8HwkFwP1F8eToOuS3ah2VAHHcY04jaYD7FpJC5JTXHYRbkzg==" - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "requires": { - "inherits": "~2.0.0" - } - }, - "bluebird": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz", - "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "http://bbnpm.azurewebsites.net/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha1-uqVZ7hTO1zRSIputcyZGfGH6vWA=" - }, - "btoa-lite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", - "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=" - }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "http://bbnpm.azurewebsites.net/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" - }, - "builtins": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", - "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=" - }, - "byline": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", - "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=" - }, - "byte-size": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-4.0.4.tgz", - "integrity": "sha512-82RPeneC6nqCdSwCX2hZUz3JPOvN5at/nTEw/CMf05Smu3Hrpo9Psb7LjN+k+XndNArG1EY8L4+BM3aTM4BCvw==" - }, - "cacache": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.2.tgz", - "integrity": "sha512-E0zP4EPGDOaT2chM08Als91eYnf8Z+eH1awwwVsngUmgppfM5jjJ8l3z5vO5p5w/I3LsiXawb1sW0VY65pQABg==", - "requires": { - "bluebird": "^3.5.3", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.3", - "graceful-fs": "^4.1.15", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.2", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "requires": { - "yallist": "^3.0.2" - } - }, - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" - } - } - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" - }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", - "requires": { - "callsites": "^2.0.0" - } - }, - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", - "requires": { - "caller-callsite": "^2.0.0" - } - }, - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=" - }, - "camelcase": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", - "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==" - }, - "camelcase-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", - "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", - "requires": { - "camelcase": "^4.1.0", - "map-obj": "^2.0.0", - "quick-lru": "^1.0.0" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" - } - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "http://bbnpm.azurewebsites.net/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" - }, - "chownr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" - }, - "ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==" - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" - }, - "cmd-shim": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-2.0.2.tgz", - "integrity": "sha1-b8vamUg6j9FdfTChlspp1oii79s=", - "requires": { - "graceful-fs": "^4.1.2", - "mkdirp": "~0.5.0" - } - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "http://bbnpm.azurewebsites.net/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "http://bbnpm.azurewebsites.net/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "columnify": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.5.4.tgz", - "integrity": "sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs=", - "requires": { - "strip-ansi": "^3.0.0", - "wcwidth": "^1.0.0" - } - }, - "combined-stream": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", - "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" - }, - "compare-func": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-1.3.2.tgz", - "integrity": "sha1-md0LpFfh+bxyKxLAjsM+6rMfpkg=", - "requires": { - "array-ify": "^1.0.0", - "dot-prop": "^3.0.0" - }, - "dependencies": { - "dot-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz", - "integrity": "sha1-G3CK8JSknJoOfbyteQq6U52sEXc=", - "requires": { - "is-obj": "^1.0.0" - } - } - } - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "http://bbnpm.azurewebsites.net/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "config-chain": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", - "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", - "requires": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "conventional-changelog-angular": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.3.tgz", - "integrity": "sha512-YD1xzH7r9yXQte/HF9JBuEDfvjxxwDGGwZU1+ndanbY0oFgA+Po1T9JDSpPLdP0pZT6MhCAsdvFKC4TJ4MTJTA==", - "requires": { - "compare-func": "^1.3.1", - "q": "^1.5.1" - } - }, - "conventional-changelog-core": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-3.1.6.tgz", - "integrity": "sha512-5teTAZOtJ4HLR6384h50nPAaKdDr+IaU0rnD2Gg2C3MS7hKsEPH8pZxrDNqam9eOSPQg9tET6uZY79zzgSz+ig==", - "requires": { - "conventional-changelog-writer": "^4.0.3", - "conventional-commits-parser": "^3.0.1", - "dateformat": "^3.0.0", - "get-pkg-repo": "^1.0.0", - "git-raw-commits": "2.0.0", - "git-remote-origin-url": "^2.0.0", - "git-semver-tags": "^2.0.2", - "lodash": "^4.2.1", - "normalize-package-data": "^2.3.5", - "q": "^1.5.1", - "read-pkg": "^3.0.0", - "read-pkg-up": "^3.0.0", - "through2": "^2.0.0" - } - }, - "conventional-changelog-preset-loader": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.0.2.tgz", - "integrity": "sha512-pBY+qnUoJPXAXXqVGwQaVmcye05xi6z231QM98wHWamGAmu/ghkBprQAwmF5bdmyobdVxiLhPY3PrCfSeUNzRQ==" - }, - "conventional-changelog-writer": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.0.3.tgz", - "integrity": "sha512-bIlpSiQtQZ1+nDVHEEh798Erj2jhN/wEjyw9sfxY9es6h7pREE5BNJjfv0hXGH/FTrAsEpHUq4xzK99eePpwuA==", - "requires": { - "compare-func": "^1.3.1", - "conventional-commits-filter": "^2.0.1", - "dateformat": "^3.0.0", - "handlebars": "^4.1.0", - "json-stringify-safe": "^5.0.1", - "lodash": "^4.2.1", - "meow": "^4.0.0", - "semver": "^5.5.0", - "split": "^1.0.0", - "through2": "^2.0.0" - } - }, - "conventional-commits-filter": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.1.tgz", - "integrity": "sha512-92OU8pz/977udhBjgPEbg3sbYzIxMDFTlQT97w7KdhR9igNqdJvy8smmedAAgn4tPiqseFloKkrVfbXCVd+E7A==", - "requires": { - "is-subset": "^0.1.1", - "modify-values": "^1.0.0" - } - }, - "conventional-commits-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.0.1.tgz", - "integrity": "sha512-P6U5UOvDeidUJ8ebHVDIoXzI7gMlQ1OF/id6oUvp8cnZvOXMt1n8nYl74Ey9YMn0uVQtxmCtjPQawpsssBWtGg==", - "requires": { - "JSONStream": "^1.0.4", - "is-text-path": "^1.0.0", - "lodash": "^4.2.1", - "meow": "^4.0.0", - "split2": "^2.0.0", - "through2": "^2.0.0", - "trim-off-newlines": "^1.0.0" - } - }, - "conventional-recommended-bump": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-4.0.4.tgz", - "integrity": "sha512-9mY5Yoblq+ZMqJpBzgS+RpSq+SUfP2miOR3H/NR9drGf08WCrY9B6HAGJZEm6+ThsVP917VHAahSOjM6k1vhPg==", - "requires": { - "concat-stream": "^1.6.0", - "conventional-changelog-preset-loader": "^2.0.2", - "conventional-commits-filter": "^2.0.1", - "conventional-commits-parser": "^3.0.1", - "git-raw-commits": "2.0.0", - "git-semver-tags": "^2.0.2", - "meow": "^4.0.0", - "q": "^1.5.1" - } - }, - "copy-concurrently": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", - "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", - "requires": { - "aproba": "^1.1.1", - "fs-write-stream-atomic": "^1.0.8", - "iferr": "^0.1.5", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.0" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "http://bbnpm.azurewebsites.net/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cosmiconfig": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.0.tgz", - "integrity": "sha512-nxt+Nfc3JAqf4WIWd0jXLjTJZmsPLrA9DDc4nRw2KFJQJK7DNooqSXrNI7tzLG50CF8axczly5UV929tBmh/7g==", - "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.0", - "parse-json": "^4.0.0" - }, - "dependencies": { - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "js-yaml": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.0.tgz", - "integrity": "sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - } - } - }, - "coveralls": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.5.tgz", - "integrity": "sha512-/KD7PGfZv/tjKB6LoW97jzIgFqem0Tu9tZL9/iwBnBd8zkIZp7vT1ZSHNvnr0GSQMV/LTMxUstWg8WcDDUVQKg==", - "requires": { - "growl": "~> 1.10.0", - "js-yaml": "^3.13.1", - "lcov-parse": "^0.0.10", - "log-driver": "^1.2.7", - "minimist": "^1.2.0", - "request": "^2.86.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "http://bbnpm.azurewebsites.net/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "requires": { - "array-find-index": "^1.0.1" - } - }, - "cyclist": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", - "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=" - }, - "dargs": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", - "integrity": "sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "http://bbnpm.azurewebsites.net/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "date-utils": { - "version": "1.2.21", - "resolved": "https://registry.npmjs.org/date-utils/-/date-utils-1.2.21.tgz", - "integrity": "sha1-YfsWzcEnSzyayq/+n8ad+HIKK2Q=" - }, - "dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==" - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "debuglog": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", - "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=" - }, - "decamelize": { - "version": "1.2.0", - "resolved": "http://bbnpm.azurewebsites.net/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "decamelize-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", - "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", - "requires": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "dependencies": { - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" - } - } - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" - }, - "dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=" - }, - "deep-is": { - "version": "0.1.3", - "resolved": "http://bbnpm.azurewebsites.net/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" - }, - "deepmerge": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.2.0.tgz", - "integrity": "sha512-6+LuZGU7QCNUnAJyX8cIrlzoEgggTM6B7mm+znKOX4t5ltluT9KLjN6g61ECMS0LTsLW7yDpNoxhix5FZcrIow==" - }, - "defaults": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", - "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", - "requires": { - "clone": "^1.0.2" - } - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "http://bbnpm.azurewebsites.net/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "deprecation": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-1.0.1.tgz", - "integrity": "sha512-ccVHpE72+tcIKaGMql33x5MAjKQIZrk+3x2GbJ7TeraUCZWHoT+KSZpoC+JQFsUBlSTXUrBaGiF0j6zVTepPLg==" - }, - "dezalgo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", - "requires": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "diff": { - "version": "3.5.0", - "resolved": "http://bbnpm.azurewebsites.net/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" - }, - "dir-glob": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", - "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==", - "requires": { - "arrify": "^1.0.1", - "path-type": "^3.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "requires": { - "esutils": "^2.0.2" - } - }, - "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", - "requires": { - "is-obj": "^1.0.0" - } - }, - "duplexer": { - "version": "0.1.1", - "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" - }, - "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "ecdsa-sig-formatter": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz", - "integrity": "sha1-HFlQAPBKiJffuFAAiSoPTDOvhsM=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" - }, - "encoding": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", - "requires": { - "iconv-lite": "~0.4.13" - } - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "^1.4.0" - } - }, - "err-code": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz", - "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=" - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", - "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", - "requires": { - "es-to-primitive": "^1.2.0", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-keys": "^1.0.12" - } - }, - "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es6-promise": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.6.tgz", - "integrity": "sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==" - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "requires": { - "es6-promise": "^4.0.3" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "http://bbnpm.azurewebsites.net/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "eslint": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", - "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", - "requires": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.9.1", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^4.0.3", - "eslint-utils": "^1.3.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^5.0.1", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob": "^7.1.2", - "globals": "^11.7.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^6.2.2", - "js-yaml": "^3.13.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.11", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "path-is-inside": "^1.0.2", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^5.5.1", - "strip-ansi": "^4.0.0", - "strip-json-comments": "^2.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" - }, - "import-fresh": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", - "integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==", - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "eslint-plugin-only-warn": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-only-warn/-/eslint-plugin-only-warn-1.0.1.tgz", - "integrity": "sha512-ckQiP40oGxrWjxY+UN6qr1DSWgsJwibICzsjBSyv6EPujZ/lta9zDe67ja7a3Wq27LiZ7B1nuBOamuigTxrbtQ==" - }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - }, - "dependencies": { - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" - } - } - }, - "eslint-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.0.tgz", - "integrity": "sha512-7ehnzPaP5IIEh1r1tkjuIrxqhNkzUJa9z3R92tLJdZIVdWaczEhr3EbhGtsMrVxi1KeR8qA7Off6SWc5WNQqyQ==", - "requires": { - "eslint-visitor-keys": "^1.0.0" - } - }, - "eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==" - }, - "espree": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", - "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", - "requires": { - "acorn": "^6.0.7", - "acorn-jsx": "^5.0.0", - "eslint-visitor-keys": "^1.0.0" - } - }, - "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", - "requires": { - "estraverse": "^4.0.0" - }, - "dependencies": { - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" - } - } - }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", - "requires": { - "estraverse": "^4.1.0" - }, - "dependencies": { - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" - } - } - }, - "esutils": { - "version": "2.0.2", - "resolved": "http://bbnpm.azurewebsites.net/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "dependencies": { - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { - "pump": "^3.0.0" - } - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "external-editor": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", - "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "http://bbnpm.azurewebsites.net/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" - }, - "fast-glob": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.6.tgz", - "integrity": "sha512-0BvMaZc1k9F+MeWWMe8pL6YltFzZYcJsYU7D4JyDA6PAczaXvxqQQ/z+mDF7/4Mw01DeUc+i3CTKajnkANkV4w==", - "requires": { - "@mrmlnc/readdir-enhanced": "^2.2.1", - "@nodelib/fs.stat": "^1.1.2", - "glob-parent": "^3.1.0", - "is-glob": "^4.0.0", - "merge2": "^1.2.3", - "micromatch": "^3.1.10" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "http://bbnpm.azurewebsites.net/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "http://bbnpm.azurewebsites.net/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" - }, - "figgy-pudding": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", - "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==" - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "requires": { - "flat-cache": "^2.0.1" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "flat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", - "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", - "requires": { - "is-buffer": "~2.0.3" - }, - "dependencies": { - "is-buffer": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", - "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" - } - } - }, - "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - } - }, - "flatted": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", - "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==" - }, - "flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", - "requires": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - } - }, - "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "requires": { - "debug": "=3.1.0" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "http://bbnpm.azurewebsites.net/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "requires": { - "map-cache": "^0.2.2" - } - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs-minipass": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", - "requires": { - "minipass": "^2.2.1" - } - }, - "fs-write-stream-atomic": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", - "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", - "requires": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "http://bbnpm.azurewebsites.net/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, - "genfun": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/genfun/-/genfun-5.0.0.tgz", - "integrity": "sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA==" - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" - }, - "get-pkg-repo": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz", - "integrity": "sha1-xztInAbYDMVTbCyFP54FIyBWly0=", - "requires": { - "hosted-git-info": "^2.1.4", - "meow": "^3.3.0", - "normalize-package-data": "^2.3.0", - "parse-github-repo-url": "^1.3.0", - "through2": "^2.0.0" - }, - "dependencies": { - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - } - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "requires": { - "repeating": "^2.0.0" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" - }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "requires": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "requires": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "requires": { - "is-utf8": "^0.2.0" - } - }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "requires": { - "get-stdin": "^4.0.1" - } - }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" - } - } - }, - "get-port": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", - "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=" - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" - }, - "get-stream": { - "version": "3.0.0", - "resolved": "http://bbnpm.azurewebsites.net/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" - }, - "getpass": { - "version": "0.1.7", - "resolved": "http://bbnpm.azurewebsites.net/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "git-raw-commits": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.0.tgz", - "integrity": "sha512-w4jFEJFgKXMQJ0H0ikBk2S+4KP2VEjhCvLCNqbNRQC8BgGWgLKNCO7a9K9LI+TVT7Gfoloje502sEnctibffgg==", - "requires": { - "dargs": "^4.0.1", - "lodash.template": "^4.0.2", - "meow": "^4.0.0", - "split2": "^2.0.0", - "through2": "^2.0.0" - } - }, - "git-remote-origin-url": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", - "integrity": "sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=", - "requires": { - "gitconfiglocal": "^1.0.0", - "pify": "^2.3.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - } - } - }, - "git-semver-tags": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-2.0.2.tgz", - "integrity": "sha512-34lMF7Yo1xEmsK2EkbArdoU79umpvm0MfzaDkSNYSJqtM5QLAVTPWgpiXSVI5o/O9EvZPSrP4Zvnec/CqhSd5w==", - "requires": { - "meow": "^4.0.0", - "semver": "^5.5.0" - } - }, - "git-up": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/git-up/-/git-up-4.0.1.tgz", - "integrity": "sha512-LFTZZrBlrCrGCG07/dm1aCjjpL1z9L3+5aEeI9SBhAqSc+kiA9Or1bgZhQFNppJX6h/f5McrvJt1mQXTFm6Qrw==", - "requires": { - "is-ssh": "^1.3.0", - "parse-url": "^5.0.0" - } - }, - "git-url-parse": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.1.2.tgz", - "integrity": "sha512-gZeLVGY8QVKMIkckncX+iCq2/L8PlwncvDFKiWkBn9EtCfYDbliRTTp6qzyQ1VMdITUfq7293zDzfpjdiGASSQ==", - "requires": { - "git-up": "^4.0.0" - } - }, - "gitconfiglocal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", - "integrity": "sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=", - "requires": { - "ini": "^1.3.2" - } - }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-to-regexp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", - "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=" - }, - "globby": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.2.tgz", - "integrity": "sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w==", - "requires": { - "array-union": "^1.0.1", - "dir-glob": "2.0.0", - "fast-glob": "^2.0.2", - "glob": "^7.1.2", - "ignore": "^3.3.5", - "pify": "^3.0.0", - "slash": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" - }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==" - }, - "handlebars": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.0.tgz", - "integrity": "sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w==", - "requires": { - "async": "^2.5.0", - "optimist": "^0.6.1", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "http://bbnpm.azurewebsites.net/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "http://bbnpm.azurewebsites.net/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" - }, - "highlight.js": { - "version": "9.15.8", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.8.tgz", - "integrity": "sha512-RrapkKQWwE+wKdF73VsOa2RQdIoO3mxwJ4P8mhbI6KYJUraUHRKM5w5zQQKXNk0xNL4UVRdulV9SBJcmzJNzVA==" - }, - "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==" - }, - "http-cache-semantics": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", - "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==" - }, - "http-signature": { - "version": "1.2.0", - "resolved": "http://bbnpm.azurewebsites.net/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", - "requires": { - "ms": "^2.0.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "iferr": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" - }, - "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==" - }, - "ignore-walk": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", - "requires": { - "minimatch": "^3.0.4" - } - }, - "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", - "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" - } - } - }, - "import-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", - "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==", - "requires": { - "pkg-dir": "^2.0.0", - "resolve-cwd": "^2.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" - }, - "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=" - }, - "inflight": { - "version": "1.0.6", - "resolved": "http://bbnpm.azurewebsites.net/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "http://bbnpm.azurewebsites.net/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, - "init-package-json": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/init-package-json/-/init-package-json-1.10.3.tgz", - "integrity": "sha512-zKSiXKhQveNteyhcj1CoOP8tqp1QuxPIPBl8Bid99DGLFqA1p87M6lNgfjJHSBoWJJlidGOv5rWjyYKEB3g2Jw==", - "requires": { - "glob": "^7.1.1", - "npm-package-arg": "^4.0.0 || ^5.0.0 || ^6.0.0", - "promzard": "^0.3.0", - "read": "~1.0.1", - "read-package-json": "1 || 2", - "semver": "2.x || 3.x || 4 || 5", - "validate-npm-package-license": "^3.0.1", - "validate-npm-package-name": "^3.0.0" - } - }, - "inquirer": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.2.tgz", - "integrity": "sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==", - "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.11", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" - }, - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "interpret": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", - "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==" - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" - }, - "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=" - }, - "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" - }, - "is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", - "requires": { - "ci-info": "^1.5.0" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=" - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "http://bbnpm.azurewebsites.net/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" - }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "requires": { - "has": "^1.0.1" - } - }, - "is-ssh": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.1.tgz", - "integrity": "sha512-0eRIASHZt1E68/ixClI8bp2YK2wmBPVWEismTs6M+M099jKgrzl/3E976zIbImSIob48N2/XGe9y7ZiYdImSlg==", - "requires": { - "protocols": "^1.1.0" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "http://bbnpm.azurewebsites.net/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "is-subset": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", - "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=" - }, - "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", - "requires": { - "has-symbols": "^1.0.0" - } - }, - "is-text-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", - "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=", - "requires": { - "text-extensions": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "http://bbnpm.azurewebsites.net/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "http://bbnpm.azurewebsites.net/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "http://bbnpm.azurewebsites.net/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "dependencies": { - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - } - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "http://bbnpm.azurewebsites.net/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, - "jschardet": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-1.6.0.tgz", - "integrity": "sha512-xYuhvQ7I9PDJIGBWev9xm0+SMSed3ZDBAmvVjbFR1ZRLAF+vlXcQu6cRI9uAlj81rzikElRVteehwV7DuX2ZmQ==" - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" - }, - "json-schema": { - "version": "0.2.3", - "resolved": "http://bbnpm.azurewebsites.net/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "http://bbnpm.azurewebsites.net/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "http://bbnpm.azurewebsites.net/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsonparse": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.2.0.tgz", - "integrity": "sha1-XAxWhRBxYOcv50ib3eoLRMK8Z70=" - }, - "jsprim": { - "version": "1.4.1", - "resolved": "http://bbnpm.azurewebsites.net/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "just-extend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", - "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==" - }, - "jwa": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.2.0.tgz", - "integrity": "sha512-Grku9ZST5NNQ3hqNUodSkDfEBqAmGA1R8yiyPHOnLzEKI0GaCQC/XhFmsheXYuXzFQJdILbh+lYBiliqG5R/Vg==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.10", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.1.tgz", - "integrity": "sha512-bGA2omSrFUkd72dhh05bIAN832znP4wOU3lfuXtRBuGTbsmNmDXMQg28f0Vsxaxgk4myF5YkKQpz6qeRpMgX9g==", - "requires": { - "jwa": "^1.2.0", - "safe-buffer": "^5.0.1" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "requires": { - "invert-kv": "^2.0.0" - } - }, - "lcov-parse": { - "version": "0.0.10", - "resolved": "http://bbnpm.azurewebsites.net/lcov-parse/-/lcov-parse-0.0.10.tgz", - "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=" - }, - "lerna": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/lerna/-/lerna-3.13.1.tgz", - "integrity": "sha512-7kSz8LLozVsoUNTJzJzy+b8TnV9YdviR2Ee2PwGZSlVw3T1Rn7kOAPZjEi+3IWnOPC96zMPHVmjCmzQ4uubalw==", - "requires": { - "@lerna/add": "3.13.1", - "@lerna/bootstrap": "3.13.1", - "@lerna/changed": "3.13.1", - "@lerna/clean": "3.13.1", - "@lerna/cli": "3.13.0", - "@lerna/create": "3.13.1", - "@lerna/diff": "3.13.1", - "@lerna/exec": "3.13.1", - "@lerna/import": "3.13.1", - "@lerna/init": "3.13.1", - "@lerna/link": "3.13.1", - "@lerna/list": "3.13.1", - "@lerna/publish": "3.13.1", - "@lerna/run": "3.13.1", - "@lerna/version": "3.13.1", - "import-local": "^1.0.0", - "npmlog": "^4.1.2" - } - }, - "levn": { - "version": "0.3.0", - "resolved": "http://bbnpm.azurewebsites.net/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "libnpmaccess": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/libnpmaccess/-/libnpmaccess-3.0.1.tgz", - "integrity": "sha512-RlZ7PNarCBt+XbnP7R6PoVgOq9t+kou5rvhaInoNibhPO7eMlRfS0B8yjatgn2yaHIwWNyoJDolC/6Lc5L/IQA==", - "requires": { - "aproba": "^2.0.0", - "get-stream": "^4.0.0", - "npm-package-arg": "^6.1.0", - "npm-registry-fetch": "^3.8.0" - }, - "dependencies": { - "aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { - "pump": "^3.0.0" - } - } - } - }, - "libnpmpublish": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/libnpmpublish/-/libnpmpublish-1.1.1.tgz", - "integrity": "sha512-nefbvJd/wY38zdt+b9SHL6171vqBrMtZ56Gsgfd0duEKb/pB8rDT4/ObUQLrHz1tOfht1flt2zM+UGaemzAG5g==", - "requires": { - "aproba": "^2.0.0", - "figgy-pudding": "^3.5.1", - "get-stream": "^4.0.0", - "lodash.clonedeep": "^4.5.0", - "normalize-package-data": "^2.4.0", - "npm-package-arg": "^6.1.0", - "npm-registry-fetch": "^3.8.0", - "semver": "^5.5.1", - "ssri": "^6.0.1" - }, - "dependencies": { - "aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { - "pump": "^3.0.0" - } - } - } - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - } - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" - }, - "lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" - }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" - }, - "lodash.get": { - "version": "4.4.2", - "resolved": "http://bbnpm.azurewebsites.net/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" - }, - "lodash.set": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", - "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=" - }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://botbuilder.myget.org/F/botbuilder-tools-daily/npm/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" - }, - "lodash.template": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", - "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=", - "requires": { - "lodash._reinterpolate": "~3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "lodash.templatesettings": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz", - "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=", - "requires": { - "lodash._reinterpolate": "~3.0.0" - } - }, - "lodash.unescape": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", - "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=" - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" - }, - "log-driver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", - "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==" - }, - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "requires": { - "chalk": "^2.0.1" - } - }, - "lolex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.1.0.tgz", - "integrity": "sha512-BYxIEXiVq5lGIXeVHnsFzqa1TxN5acnKnPCdlZSpzm8viNEOhiigupA4vTQ9HEFQ6nLTQ9wQOgBknJgzUYQ9Aw==" - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - }, - "macos-release": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.2.0.tgz", - "integrity": "sha512-iV2IDxZaX8dIcM7fG6cI46uNmHUxHE4yN+Z8tKHAW1TBPMZDIKHf/3L+YnOuj/FK9il14UaVdHmiQ1tsi90ltA==" - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "requires": { - "pify": "^3.0.0" - } - }, - "make-fetch-happen": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-4.0.1.tgz", - "integrity": "sha512-7R5ivfy9ilRJ1EMKIOziwrns9fGeAD4bAha8EB7BIiBBLHm2KeTUGCrICFt2rbHfzheTLynv50GnNTK1zDTrcQ==", - "requires": { - "agentkeepalive": "^3.4.1", - "cacache": "^11.0.1", - "http-cache-semantics": "^3.8.1", - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^2.2.1", - "lru-cache": "^4.1.2", - "mississippi": "^3.0.0", - "node-fetch-npm": "^2.0.2", - "promise-retry": "^1.1.1", - "socks-proxy-agent": "^4.0.0", - "ssri": "^6.0.0" - }, - "dependencies": { - "agent-base": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", - "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", - "requires": { - "es6-promisify": "^5.0.0" - } - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "http-proxy-agent": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", - "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", - "requires": { - "agent-base": "4", - "debug": "3.1.0" - } - }, - "https-proxy-agent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", - "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", - "requires": { - "agent-base": "^4.1.0", - "debug": "^3.1.0" - } - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - } - } - }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "requires": { - "p-defer": "^1.0.0" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" - }, - "map-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", - "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=" - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "requires": { - "object-visit": "^1.0.0" - } - }, - "marked": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.4.0.tgz", - "integrity": "sha512-tMsdNBgOsrUophCAFQl0XPe6Zqk/uy9gnue+jIIKhykO51hxyu6uNx7zBPy0+y/WKYVZZMspV9YeXLNdKk+iYw==" - }, - "mem": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.1.0.tgz", - "integrity": "sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg==", - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^1.0.0", - "p-is-promise": "^2.0.0" - }, - "dependencies": { - "p-is-promise": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.0.0.tgz", - "integrity": "sha512-pzQPhYMCAgLAKPWD2jC3Se9fEfrD9npNos0y150EeqZll7akhEgGhTW/slB6lHku8AvYGiJ+YJ5hfHKePPgFWg==" - } - } - }, - "meow": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", - "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", - "requires": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist": "^1.1.3", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "merge2": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.3.tgz", - "integrity": "sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA==" - }, - "mime-db": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", - "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" - }, - "mime-types": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", - "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", - "requires": { - "mime-db": "~1.38.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha1-ggyGo5M0ZA6ZUWkovQP8qIBX0CI=" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "http://bbnpm.azurewebsites.net/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "minimist-options": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", - "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", - "requires": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0" - } - }, - "minipass": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", - "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - }, - "dependencies": { - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" - } - } - }, - "minizlib": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", - "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", - "requires": { - "minipass": "^2.2.1" - } - }, - "mississippi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", - "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", - "requires": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^3.0.0", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - } - }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "http://bbnpm.azurewebsites.net/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, - "mocha": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz", - "integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==", - "requires": { - "ansi-colors": "3.2.3", - "browser-stdout": "1.3.1", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "2.2.0", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "ms": "2.1.1", - "node-environment-flags": "1.0.5", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", - "wide-align": "1.1.3", - "yargs": "13.2.2", - "yargs-parser": "13.0.0", - "yargs-unparser": "1.5.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "yargs": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", - "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", - "requires": { - "cliui": "^4.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.0.0" - } - }, - "yargs-parser": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", - "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "modify-values": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", - "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==" - }, - "moment": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", - "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" - }, - "move-concurrently": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", - "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", - "requires": { - "aproba": "^1.1.1", - "copy-concurrently": "^1.0.0", - "fs-write-stream-atomic": "^1.0.8", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.3" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - }, - "ms-rest": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ms-rest/-/ms-rest-2.5.0.tgz", - "integrity": "sha512-QUTg9CsmWpofDO0MR37z8B28/T9ObpQ+FM23GGDMKXw8KYDJ3cEBdK6dJTDDrtSoZG3U+S/vdmSEwJ7FNj6Kog==", - "requires": { - "duplexer": "^0.1.1", - "is-buffer": "^1.1.6", - "is-stream": "^1.1.0", - "moment": "^2.21.0", - "request": "^2.88.0", - "through": "^2.3.8", - "tunnel": "0.0.5", - "uuid": "^3.2.1" - } - }, - "ms-rest-azure": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/ms-rest-azure/-/ms-rest-azure-2.6.0.tgz", - "integrity": "sha512-J6386a9krZ4VtU7CRt+Ypgo9RGf8+d3gjMBkH7zbkM4zzkhbbMOYiPRaZ+bHZcfihkKLlktTgA6rjshTjF329A==", - "requires": { - "adal-node": "^0.1.28", - "async": "2.6.0", - "moment": "^2.22.2", - "ms-rest": "^2.3.2", - "request": "^2.88.0", - "uuid": "^3.2.1" - } - }, - "multimatch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", - "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=", - "requires": { - "array-differ": "^1.0.0", - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "minimatch": "^3.0.0" - } - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" - }, - "nise": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.0.tgz", - "integrity": "sha512-Z3sfYEkLFzFmL8KY6xnSJLRxwQwYBjOXi/24lb62ZnZiGA0JUzGGTI6TBIgfCSMIDl9Jlu8SRmHNACLTemDHww==", - "requires": { - "@sinonjs/formatio": "^3.1.0", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "lolex": "^4.1.0", - "path-to-regexp": "^1.7.0" - } - }, - "node-environment-flags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", - "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", - "requires": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - }, - "dependencies": { - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" - } - } - }, - "node-fetch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz", - "integrity": "sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA==" - }, - "node-fetch-npm": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz", - "integrity": "sha512-nJIxm1QmAj4v3nfCvEeCrYSoVwXyxLnaPBK5W1W5DGEJwjlKuC2VEUycGw5oxk+4zZahRrB84PUJJgEmhFTDFw==", - "requires": { - "encoding": "^0.1.11", - "json-parse-better-errors": "^1.0.0", - "safe-buffer": "^5.1.1" - } - }, - "node-gyp": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", - "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", - "requires": { - "fstream": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "osenv": "0", - "request": "^2.87.0", - "rimraf": "2", - "semver": "~5.3.0", - "tar": "^2.0.0", - "which": "1" - }, - "dependencies": { - "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "requires": { - "abbrev": "1" - } - }, - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" - } - } - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "npm-bundled": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", - "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==" - }, - "npm-lifecycle": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/npm-lifecycle/-/npm-lifecycle-2.1.0.tgz", - "integrity": "sha512-QbBfLlGBKsktwBZLj6AviHC6Q9Y3R/AY4a2PYSIRhSKSS0/CxRyD/PfxEX6tPeOCXQgMSNdwGeECacstgptc+g==", - "requires": { - "byline": "^5.0.0", - "graceful-fs": "^4.1.11", - "node-gyp": "^3.8.0", - "resolve-from": "^4.0.0", - "slide": "^1.1.6", - "uid-number": "0.0.6", - "umask": "^1.1.0", - "which": "^1.3.1" - } - }, - "npm-package-arg": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.0.tgz", - "integrity": "sha512-zYbhP2k9DbJhA0Z3HKUePUgdB1x7MfIfKssC+WLPFMKTBZKpZh5m13PgexJjCq6KW7j17r0jHWcCpxEqnnncSA==", - "requires": { - "hosted-git-info": "^2.6.0", - "osenv": "^0.1.5", - "semver": "^5.5.0", - "validate-npm-package-name": "^3.0.0" - } - }, - "npm-packlist": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz", - "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npm-pick-manifest": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-2.2.3.tgz", - "integrity": "sha512-+IluBC5K201+gRU85vFlUwX3PFShZAbAgDNp2ewJdWMVSppdo/Zih0ul2Ecky/X7b51J7LrrUAP+XOmOCvYZqA==", - "requires": { - "figgy-pudding": "^3.5.1", - "npm-package-arg": "^6.0.0", - "semver": "^5.4.1" - } - }, - "npm-registry-fetch": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-3.9.0.tgz", - "integrity": "sha512-srwmt8YhNajAoSAaDWndmZgx89lJwIZ1GWxOuckH4Coek4uHv5S+o/l9FLQe/awA+JwTnj4FJHldxhlXdZEBmw==", - "requires": { - "JSONStream": "^1.3.4", - "bluebird": "^3.5.1", - "figgy-pudding": "^3.4.1", - "lru-cache": "^4.1.3", - "make-fetch-happen": "^4.0.1", - "npm-package-arg": "^6.1.0" - }, - "dependencies": { - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - } - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "http://bbnpm.azurewebsites.net/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "requires": { - "path-key": "^2.0.0" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "http://bbnpm.azurewebsites.net/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "requires": { - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "octokit-pagination-methods": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz", - "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==" - }, - "once": { - "version": "1.4.0", - "resolved": "http://bbnpm.azurewebsites.net/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "http://bbnpm.azurewebsites.net/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" - } - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "http://bbnpm.azurewebsites.net/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "os-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.0.0.tgz", - "integrity": "sha512-7c74tib2FsdFbQ3W+qj8Tyd1R3Z6tuVRNNxXjJcZ4NgjIEQU9N/prVMqcW29XZPXGACqaXN3jq58/6hoaoXH6g==", - "requires": { - "macos-release": "^2.0.0", - "windows-release": "^3.1.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" - }, - "p-finally": { - "version": "1.0.0", - "resolved": "http://bbnpm.azurewebsites.net/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" - }, - "p-limit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", - "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-map": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", - "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==" - }, - "p-map-series": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz", - "integrity": "sha1-v5j+V1cFZYqeE1G++4WuTB8Hvco=", - "requires": { - "p-reduce": "^1.0.0" - } - }, - "p-pipe": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz", - "integrity": "sha1-SxoROZoRUgpneQ7loMHViB1r7+k=" - }, - "p-reduce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", - "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=" - }, - "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==" - }, - "p-waterfall": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-waterfall/-/p-waterfall-1.0.0.tgz", - "integrity": "sha1-ftlLPOszMngjU69qrhGqn8I1uwA=", - "requires": { - "p-reduce": "^1.0.0" - } - }, - "pacote": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-9.5.0.tgz", - "integrity": "sha512-aUplXozRbzhaJO48FaaeClmN+2Mwt741MC6M3bevIGZwdCaP7frXzbUOfOWa91FPHoLITzG0hYaKY363lxO3bg==", - "requires": { - "bluebird": "^3.5.3", - "cacache": "^11.3.2", - "figgy-pudding": "^3.5.1", - "get-stream": "^4.1.0", - "glob": "^7.1.3", - "lru-cache": "^5.1.1", - "make-fetch-happen": "^4.0.1", - "minimatch": "^3.0.4", - "minipass": "^2.3.5", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "normalize-package-data": "^2.4.0", - "npm-package-arg": "^6.1.0", - "npm-packlist": "^1.1.12", - "npm-pick-manifest": "^2.2.3", - "npm-registry-fetch": "^3.8.0", - "osenv": "^0.1.5", - "promise-inflight": "^1.0.1", - "promise-retry": "^1.1.1", - "protoduck": "^5.0.1", - "rimraf": "^2.6.2", - "safe-buffer": "^5.1.2", - "semver": "^5.6.0", - "ssri": "^6.0.1", - "tar": "^4.4.8", - "unique-filename": "^1.1.1", - "which": "^1.3.1" - }, - "dependencies": { - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { - "pump": "^3.0.0" - } - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "requires": { - "yallist": "^3.0.2" - } - }, - "tar": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", - "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" - } - } - }, - "parallel-transform": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", - "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", - "requires": { - "cyclist": "~0.2.2", - "inherits": "^2.0.3", - "readable-stream": "^2.1.5" - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "requires": { - "callsites": "^3.0.0" - }, - "dependencies": { - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" - } - } - }, - "parse-github-repo-url": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz", - "integrity": "sha1-nn2LslKmy2ukJZUGC3v23z28H1A=" - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "requires": { - "error-ex": "^1.2.0" - } - }, - "parse-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-4.0.1.tgz", - "integrity": "sha512-d7yhga0Oc+PwNXDvQ0Jv1BuWkLVPXcAoQ/WREgd6vNNoKYaW52KI+RdOFjI63wjkmps9yUE8VS4veP+AgpQ/hA==", - "requires": { - "is-ssh": "^1.3.0", - "protocols": "^1.4.0" - } - }, - "parse-url": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-5.0.1.tgz", - "integrity": "sha512-flNUPP27r3vJpROi0/R3/2efgKkyXqnXwyP1KQ2U0SfFRgdizOdWfvrrvJg1LuOoxs7GQhmxJlq23IpQ/BkByg==", - "requires": { - "is-ssh": "^1.3.0", - "normalize-url": "^3.3.0", - "parse-path": "^4.0.0", - "protocols": "^1.4.0" - }, - "dependencies": { - "normalize-url": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", - "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==" - } - } - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" - }, - "path-exists": { - "version": "3.0.0", - "resolved": "http://bbnpm.azurewebsites.net/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "http://bbnpm.azurewebsites.net/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" - }, - "path-key": { - "version": "2.0.1", - "resolved": "http://bbnpm.azurewebsites.net/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, - "path-to-regexp": { - "version": "1.7.0", - "resolved": "http://bbnpm.azurewebsites.net/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", - "requires": { - "isarray": "0.0.1" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - } - } - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "requires": { - "pify": "^3.0.0" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "http://bbnpm.azurewebsites.net/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "requires": { - "pinkie": "^2.0.0" - } - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "requires": { - "find-up": "^2.1.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" - } - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "http://bbnpm.azurewebsites.net/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" - }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" - }, - "promise-retry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz", - "integrity": "sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=", - "requires": { - "err-code": "^1.0.0", - "retry": "^0.10.0" - } - }, - "promzard": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/promzard/-/promzard-0.3.0.tgz", - "integrity": "sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=", - "requires": { - "read": "1" - } - }, - "proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" - }, - "protocols": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.7.tgz", - "integrity": "sha512-Fx65lf9/YDn3hUX08XUc0J8rSux36rEsyiv21ZGUC1mOyeM3lTRpZLcrm8aAolzS4itwVfm7TAPyxC2E5zd6xg==" - }, - "protoduck": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz", - "integrity": "sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg==", - "requires": { - "genfun": "^5.0.0" - } - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "psl": { - "version": "1.1.31", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", - "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - }, - "dependencies": { - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "quick-lru": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", - "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=" - }, - "read": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", - "requires": { - "mute-stream": "~0.0.4" - } - }, - "read-cmd-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-1.0.1.tgz", - "integrity": "sha1-LV0Vd4ajfAVdIgd8MsU/gynpHHs=", - "requires": { - "graceful-fs": "^4.1.2" - } - }, - "read-package-json": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.0.13.tgz", - "integrity": "sha512-/1dZ7TRZvGrYqE0UAfN6qQb5GYBsNcqS1C0tNK601CFOJmtHI7NIGXwetEPU/OtoFHZL3hDxm4rolFFVE9Bnmg==", - "requires": { - "glob": "^7.1.1", - "graceful-fs": "^4.1.2", - "json-parse-better-errors": "^1.0.1", - "normalize-package-data": "^2.0.0", - "slash": "^1.0.0" - } - }, - "read-package-tree": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.2.2.tgz", - "integrity": "sha512-rW3XWUUkhdKmN2JKB4FL563YAgtINifso5KShykufR03nJ5loGFlkUMe1g/yxmqX073SoYYTsgXu7XdDinKZuA==", - "requires": { - "debuglog": "^1.0.1", - "dezalgo": "^1.0.0", - "once": "^1.3.0", - "read-package-json": "^2.0.0", - "readdir-scoped-modules": "^1.0.0" - } - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" - } - } - }, - "read-text-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-text-file/-/read-text-file-1.1.0.tgz", - "integrity": "sha1-0MPxh2iCj5EH1huws2jue5D3GJM=", - "requires": { - "iconv-lite": "^0.4.17", - "jschardet": "^1.4.2" - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdir-scoped-modules": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz", - "integrity": "sha1-n6+jfShr5dksuuve4DDcm19AZ0c=", - "requires": { - "debuglog": "^1.0.1", - "dezalgo": "^1.0.0", - "graceful-fs": "^4.1.2", - "once": "^1.3.0" - } - }, - "rechoir": { - "version": "0.6.2", - "resolved": "http://bbnpm.azurewebsites.net/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "requires": { - "resolve": "^1.1.6" - } - }, - "redent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", - "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", - "requires": { - "indent-string": "^3.0.0", - "strip-indent": "^2.0.0" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==" - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "requires": { - "is-finite": "^1.0.0" - } - }, - "replace-in-file": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-4.1.1.tgz", - "integrity": "sha512-0Va403DpFFRpm6oIsEf2U9fH9mVuDgRmSbXwrzpC3tmGduah9FhJJmu424rlogJo+0t7ho9f1HOpR+0qcXtzWQ==", - "requires": { - "chalk": "^2.4.2", - "glob": "^7.1.3", - "yargs": "^13.2.2" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "yargs": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", - "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.0" - } - }, - "yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - } - } - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "http://bbnpm.azurewebsites.net/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "http://bbnpm.azurewebsites.net/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" - }, - "resolve": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", - "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "requires": { - "resolve-from": "^3.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" - } - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" - }, - "retry": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", - "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=" - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "requires": { - "glob": "^7.1.3" - } - }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "requires": { - "is-promise": "^2.1.0" - } - }, - "run-queue": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", - "requires": { - "aproba": "^1.1.1" - } - }, - "rxjs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", - "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", - "requires": { - "tslib": "^1.9.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "http://bbnpm.azurewebsites.net/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "http://bbnpm.azurewebsites.net/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "http://bbnpm.azurewebsites.net/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" - }, - "shelljs": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", - "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==", - "requires": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - } - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "http://bbnpm.azurewebsites.net/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "sinon": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.3.2.tgz", - "integrity": "sha512-thErC1z64BeyGiPvF8aoSg0LEnptSaWE7YhdWWbWXgelOyThent7uKOnnEh9zBxDbKixtr5dEko+ws1sZMuFMA==", - "requires": { - "@sinonjs/commons": "^1.4.0", - "@sinonjs/formatio": "^3.2.1", - "@sinonjs/samsam": "^3.3.1", - "diff": "^3.5.0", - "lolex": "^4.0.1", - "nise": "^1.4.10", - "supports-color": "^5.5.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" - }, - "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - } - } - }, - "slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=" - }, - "smart-buffer": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.0.2.tgz", - "integrity": "sha512-JDhEpTKzXusOqXZ0BUIdH+CjFdO/CR3tLlf5CN34IypI+xMmXW1uB16OOY8z3cICbJlDAVJzNbwBhNO0wt9OAw==" - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "requires": { - "kind-of": "^3.2.0" - } - }, - "socks": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.3.2.tgz", - "integrity": "sha512-pCpjxQgOByDHLlNqlnh/mNSAxIUkyBBuwwhTcV+enZGbDaClPvHdvm6uvOwZfFJkam7cGhBNbb4JxiP8UZkRvQ==", - "requires": { - "ip": "^1.1.5", - "smart-buffer": "4.0.2" - } - }, - "socks-proxy-agent": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz", - "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", - "requires": { - "agent-base": "~4.2.1", - "socks": "~2.3.2" - }, - "dependencies": { - "agent-base": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", - "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", - "requires": { - "es6-promisify": "^5.0.0" - } - } - } - }, - "sort-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", - "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", - "requires": { - "is-plain-obj": "^1.0.0" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" - }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==" - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz", - "integrity": "sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==" - }, - "split": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", - "requires": { - "through": "2" - } - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "split2": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", - "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", - "requires": { - "through2": "^2.0.2" - } - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", - "requires": { - "figgy-pudding": "^3.5.1" - } - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "stream-each": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", - "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", - "requires": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" - } - }, - "stream-shift": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "http://bbnpm.azurewebsites.net/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "http://bbnpm.azurewebsites.net/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" - }, - "strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=" - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "strong-log-transformer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz", - "integrity": "sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA==", - "requires": { - "duplexer": "^0.1.1", - "minimist": "^1.2.0", - "through": "^2.3.4" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "table": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.1.tgz", - "integrity": "sha512-E6CK1/pZe2N75rGZQotFOdmzWQ1AILtgYbMAbAjvms0S1l5IDB47zG3nCnFGB/w+7nB3vKofbLXCH7HPBo864w==", - "requires": { - "ajv": "^6.9.1", - "lodash": "^4.17.11", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "requires": { - "block-stream": "*", - "fstream": "^1.0.2", - "inherits": "2" - }, - "dependencies": { - "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - } - } - }, - "temp-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", - "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=" - }, - "temp-write": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-write/-/temp-write-3.4.0.tgz", - "integrity": "sha1-jP9jD7fp2gXwR8dM5M5NaFRX1JI=", - "requires": { - "graceful-fs": "^4.1.2", - "is-stream": "^1.1.0", - "make-dir": "^1.0.0", - "pify": "^3.0.0", - "temp-dir": "^1.0.0", - "uuid": "^3.0.1" - } - }, - "text-extensions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", - "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==" - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" - }, - "through": { - "version": "2.3.8", - "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - }, - "dependencies": { - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - } - } - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "requires": { - "kind-of": "^3.0.2" - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - } - } - } - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "requires": { - "punycode": "^2.1.0" - } - }, - "trim-newlines": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", - "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=" - }, - "trim-off-newlines": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", - "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=" - }, - "tslib": { - "version": "1.9.3", - "resolved": "http://bbnpm.azurewebsites.net/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" - }, - "tslint": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.18.0.tgz", - "integrity": "sha512-Q3kXkuDEijQ37nXZZLKErssQVnwCV/+23gFEMROi8IlbaBG6tXqLPQJ5Wjcyt/yHPKBC+hD5SzuGaMora+ZS6w==", - "requires": { - "@babel/code-frame": "^7.0.0", - "builtin-modules": "^1.1.1", - "chalk": "^2.3.0", - "commander": "^2.12.1", - "diff": "^3.2.0", - "glob": "^7.1.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "tslib": "^1.8.0", - "tsutils": "^2.29.0" - }, - "dependencies": { - "tsutils": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", - "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", - "requires": { - "tslib": "^1.8.1" - } - } - } - }, - "tslint-microsoft-contrib": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tslint-microsoft-contrib/-/tslint-microsoft-contrib-6.2.0.tgz", - "integrity": "sha512-6tfi/2tHqV/3CL77pULBcK+foty11Rr0idRDxKnteTaKm6gWF9qmaCNU17HVssOuwlYNyOmd9Jsmjd+1t3a3qw==", - "requires": { - "tsutils": "^2.27.2 <2.29.0" - }, - "dependencies": { - "tsutils": { - "version": "2.28.0", - "resolved": "http://bbnpm.azurewebsites.net/tsutils/-/tsutils-2.28.0.tgz", - "integrity": "sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA==", - "requires": { - "tslib": "^1.8.1" - } - } - } - }, - "tsutils": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.14.0.tgz", - "integrity": "sha512-SmzGbB0l+8I0QwsPgjooFRaRvHLBLNYM8SeQ0k6rtNDru5sCGeLJcZdwilNndN+GysuFjF5EIYgN8GfFG6UeUw==", - "requires": { - "tslib": "^1.8.1" - } - }, - "tunnel": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.5.tgz", - "integrity": "sha512-gj5sdqherx4VZKMcBA4vewER7zdK25Td+z1npBqpbDys4eJrLx+SlYjJvq1bDXs2irkuJM5pf8ktaEQVipkrbA==" - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "http://bbnpm.azurewebsites.net/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "http://bbnpm.azurewebsites.net/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "type-check": { - "version": "0.3.2", - "resolved": "http://bbnpm.azurewebsites.net/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "http://bbnpm.azurewebsites.net/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "typedoc": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.14.2.tgz", - "integrity": "sha512-aEbgJXV8/KqaVhcedT7xG6d2r+mOvB5ep3eIz1KuB5sc4fDYXcepEEMdU7XSqLFO5hVPu0nllHi1QxX2h/QlpQ==", - "requires": { - "@types/fs-extra": "^5.0.3", - "@types/handlebars": "^4.0.38", - "@types/highlight.js": "^9.12.3", - "@types/lodash": "^4.14.110", - "@types/marked": "^0.4.0", - "@types/minimatch": "3.0.3", - "@types/shelljs": "^0.8.0", - "fs-extra": "^7.0.0", - "handlebars": "^4.0.6", - "highlight.js": "^9.13.1", - "lodash": "^4.17.10", - "marked": "^0.4.0", - "minimatch": "^3.0.0", - "progress": "^2.0.0", - "shelljs": "^0.8.2", - "typedoc-default-themes": "^0.5.0", - "typescript": "3.2.x" - }, - "dependencies": { - "typescript": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.4.tgz", - "integrity": "sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg==" - } - } - }, - "typedoc-default-themes": { - "version": "0.5.0", - "resolved": "http://bbnpm.azurewebsites.net/typedoc-default-themes/-/typedoc-default-themes-0.5.0.tgz", - "integrity": "sha1-bcJDPnjti+qOiHo6zeLzF4W9Yic=" - }, - "typedoc-plugin-external-module-name": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/typedoc-plugin-external-module-name/-/typedoc-plugin-external-module-name-2.1.0.tgz", - "integrity": "sha512-uYYe1yj6COwgyhl3Of71lkkhbEw7LVBEqAlXVvd7b9INGhJq59M1q9wdQdIWfpqe/9JegWSIR9ZDfY6mkPedJg==" - }, - "typedoc-plugin-markdown": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-2.0.9.tgz", - "integrity": "sha512-EKuJ7TaNliHno+vGW3GHjOaO/zNdnsK7wZlTbF+jSFJ/HGSNHysnnT/MO20TnDtmQfRgMAz7ltag5Ko6r/hr/Q==" - }, - "typescript": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", - "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==" - }, - "uglify-js": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", - "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", - "optional": true, - "requires": { - "commander": "~2.17.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", - "optional": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true - } - } - }, - "uid-number": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", - "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" - }, - "umask": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz", - "integrity": "sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=" - }, - "underscore": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", - "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" - }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } - } - }, - "unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "requires": { - "unique-slug": "^2.0.0" - } - }, - "unique-slug": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.1.tgz", - "integrity": "sha512-n9cU6+gITaVu7VGj1Z8feKMmfAjEAQGhwD9fE3zvpRRa0wEIx8ODYkVGfSc94M2OX00tUFV8wH3zYbm1I8mxFg==", - "requires": { - "imurmurhash": "^0.1.4" - } - }, - "universal-user-agent": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-2.0.3.tgz", - "integrity": "sha512-eRHEHhChCBHrZsA4WEhdgiOKgdvgrMIHwnwnqD0r5C6AO8kwKcG7qSku3iXdhvHL3YvsS9ZkSGN8h/hIpoFC8g==", - "requires": { - "os-name": "^3.0.0" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" - }, - "url-template": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", - "integrity": "sha1-/FZaPMy/93MMd19WQflVV5FDnyE=" - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "uuid": { - "version": "3.3.2", - "resolved": "http://bbnpm.azurewebsites.net/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "validate-npm-package-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", - "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", - "requires": { - "builtins": "^1.0.3" - } - }, - "verror": { - "version": "1.10.0", - "resolved": "http://bbnpm.azurewebsites.net/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", - "requires": { - "defaults": "^1.0.3" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha1-qFWYCx8LazWbodXZ+zmulB+qY60=" - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "http://bbnpm.azurewebsites.net/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "windows-release": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.1.0.tgz", - "integrity": "sha512-hBb7m7acFgQPQc222uEQTmdcGLeBmQLNLFIh0rDk3CwFOBrfjefLzEfEfmpMq8Af/n/GnFf3eYf203FY1PmudA==", - "requires": { - "execa": "^0.10.0" - }, - "dependencies": { - "execa": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", - "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - } - } - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "http://bbnpm.azurewebsites.net/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "http://bbnpm.azurewebsites.net/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "http://bbnpm.azurewebsites.net/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "http://bbnpm.azurewebsites.net/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "requires": { - "mkdirp": "^0.5.1" - } - }, - "write-file-atomic": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.2.tgz", - "integrity": "sha512-s0b6vB3xIVRLWywa6X9TOMA7k9zio0TMOsl9ZnDkliA/cfJlpHXAscj0gbHVJiTdIuAYpIyqS5GW91fqm6gG5g==", - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "write-json-file": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/write-json-file/-/write-json-file-2.3.0.tgz", - "integrity": "sha1-K2TIozAE1UuGmMdtWFp3zrYdoy8=", - "requires": { - "detect-indent": "^5.0.0", - "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "pify": "^3.0.0", - "sort-keys": "^2.0.0", - "write-file-atomic": "^2.0.0" - }, - "dependencies": { - "detect-indent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", - "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=" - } - } - }, - "write-pkg": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/write-pkg/-/write-pkg-3.2.0.tgz", - "integrity": "sha512-tX2ifZ0YqEFOF1wjRW2Pk93NLsj02+n1UP5RvO6rCs0K6R2g1padvf006cY74PQJKMGS2r42NK7FD0dG6Y6paw==", - "requires": { - "sort-keys": "^2.0.0", - "write-json-file": "^2.2.0" - } - }, - "xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" - } - }, - "xmlbuilder": { - "version": "9.0.7", - "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" - }, - "xmldom": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", - "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=" - }, - "xpath.js": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz", - "integrity": "sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ==" - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "yargs-unparser": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", - "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", - "requires": { - "flat": "^4.1.0", - "lodash": "^4.17.11", - "yargs": "^12.0.5" - } - } - } + "name": "botbuilder-packages", + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@azure/cognitiveservices-luis-runtime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/cognitiveservices-luis-runtime/-/cognitiveservices-luis-runtime-2.0.0.tgz", + "integrity": "sha512-NZuqxiwpn8iYM76/QDIBDGq1jJ+YHiwS0S/yprAMeaaQgu1S5VtVhWDbTrZl+AfaqCn6iDpRewI7EKRv1GJx0g==", + "requires": { + "@azure/ms-rest-js": "^1.6.0", + "tslib": "^1.9.3" + } + }, + "@azure/cosmos": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.4.1.tgz", + "integrity": "sha512-nK8N6Y0dviRIIFX+mdfxW6IcTA02RH1RIhBk5XLZwDvIGUN3zAzlxaRxio31Pt0bAv1Plz5LcbZAh0mEhuLcsw==", + "requires": { + "@types/debug": "^4.1.4", + "debug": "^4.1.1", + "fast-json-stable-stringify": "^2.0.0", + "node-abort-controller": "^1.0.4", + "node-fetch": "^2.6.0", + "os-name": "^3.1.0", + "priorityqueuejs": "^1.0.0", + "semaphore": "^1.0.5", + "tslib": "^1.9.3", + "uuid": "^3.3.2" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "@azure/ms-rest-js": { + "version": "1.8.13", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@azure/ms-rest-js/-/@azure/ms-rest-js-1.8.13.tgz", + "integrity": "sha1-7QzYZGlpc3jNOdedVYnod6O8h6Y=", + "requires": { + "@types/tunnel": "0.0.0", + "axios": "^0.19.0", + "form-data": "^2.3.2", + "tough-cookie": "^2.4.3", + "tslib": "^1.9.2", + "tunnel": "0.0.6", + "uuid": "^3.2.1", + "xml2js": "^0.4.19" + } + }, + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@babel/code-frame/-/@babel/code-frame-7.5.5.tgz", + "integrity": "sha1-vAeC9tafe31JUxIZaZuYj2aaj50=", + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/highlight": { + "version": "7.5.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@babel/highlight/-/@babel/highlight-7.5.0.tgz", + "integrity": "sha1-VtETEr2SSPphlZHQJHK+boyzJUA=", + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@evocateur/libnpmaccess": { + "version": "3.1.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@evocateur/libnpmaccess/-/@evocateur/libnpmaccess-3.1.2.tgz", + "integrity": "sha1-7Pf2zmsATp+UKwmNkiAL5KSxyEU=", + "requires": { + "@evocateur/npm-registry-fetch": "^4.0.0", + "aproba": "^2.0.0", + "figgy-pudding": "^3.5.1", + "get-stream": "^4.0.0", + "npm-package-arg": "^6.1.0" + }, + "dependencies": { + "aproba": { + "version": "2.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha1-UlILiuW1aSFbNU78DKo/4eRaitw=" + } + } + }, + "@evocateur/libnpmpublish": { + "version": "1.2.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@evocateur/libnpmpublish/-/@evocateur/libnpmpublish-1.2.2.tgz", + "integrity": "sha1-Vd8J0tyhNq+6nIjHWconIZjbnxo=", + "requires": { + "@evocateur/npm-registry-fetch": "^4.0.0", + "aproba": "^2.0.0", + "figgy-pudding": "^3.5.1", + "get-stream": "^4.0.0", + "lodash.clonedeep": "^4.5.0", + "normalize-package-data": "^2.4.0", + "npm-package-arg": "^6.1.0", + "semver": "^5.5.1", + "ssri": "^6.0.1" + }, + "dependencies": { + "aproba": { + "version": "2.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha1-UlILiuW1aSFbNU78DKo/4eRaitw=" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/semver/-/semver-5.7.1.tgz", + "integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=" + } + } + }, + "@evocateur/npm-registry-fetch": { + "version": "4.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@evocateur/npm-registry-fetch/-/@evocateur/npm-registry-fetch-4.0.0.tgz", + "integrity": "sha1-jEw4dm2NMtMgD8sKg/BktXNl7WY=", + "requires": { + "JSONStream": "^1.3.4", + "bluebird": "^3.5.1", + "figgy-pudding": "^3.4.1", + "lru-cache": "^5.1.1", + "make-fetch-happen": "^5.0.0", + "npm-package-arg": "^6.1.0", + "safe-buffer": "^5.1.2" + } + }, + "@evocateur/pacote": { + "version": "9.6.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@evocateur/pacote/-/@evocateur/pacote-9.6.5.tgz", + "integrity": "sha1-M94yuiELbxfCDrq01JfvxnVfSuU=", + "requires": { + "@evocateur/npm-registry-fetch": "^4.0.0", + "bluebird": "^3.5.3", + "cacache": "^12.0.3", + "chownr": "^1.1.2", + "figgy-pudding": "^3.5.1", + "get-stream": "^4.1.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^5.1.1", + "make-fetch-happen": "^5.0.0", + "minimatch": "^3.0.4", + "minipass": "^2.3.5", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "normalize-package-data": "^2.5.0", + "npm-package-arg": "^6.1.0", + "npm-packlist": "^1.4.4", + "npm-pick-manifest": "^3.0.0", + "osenv": "^0.1.5", + "promise-inflight": "^1.0.1", + "promise-retry": "^1.1.1", + "protoduck": "^5.0.1", + "rimraf": "^2.6.3", + "safe-buffer": "^5.2.0", + "semver": "^5.7.0", + "ssri": "^6.0.1", + "tar": "^4.4.10", + "unique-filename": "^1.1.1", + "which": "^1.3.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/semver/-/semver-5.7.1.tgz", + "integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=" + } + } + }, + "@lerna/add": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/add/-/@lerna/add-3.18.0.tgz", + "integrity": "sha1-huOPFNegp8YTFdzLQCN3/rHJ24M=", + "requires": { + "@evocateur/pacote": "^9.6.3", + "@lerna/bootstrap": "3.18.0", + "@lerna/command": "3.18.0", + "@lerna/filter-options": "3.18.0", + "@lerna/npm-conf": "3.16.0", + "@lerna/validation-error": "3.13.0", + "dedent": "^0.7.0", + "npm-package-arg": "^6.1.0", + "p-map": "^2.1.0", + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/semver/-/semver-6.3.0.tgz", + "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=" + } + } + }, + "@lerna/bootstrap": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/bootstrap/-/@lerna/bootstrap-3.18.0.tgz", + "integrity": "sha1-cF2etRok1UlRh5agnyTSRSbtl1s=", + "requires": { + "@lerna/command": "3.18.0", + "@lerna/filter-options": "3.18.0", + "@lerna/has-npm-version": "3.16.5", + "@lerna/npm-install": "3.16.5", + "@lerna/package-graph": "3.18.0", + "@lerna/pulse-till-done": "3.13.0", + "@lerna/rimraf-dir": "3.16.5", + "@lerna/run-lifecycle": "3.16.2", + "@lerna/run-topologically": "3.18.0", + "@lerna/symlink-binary": "3.17.0", + "@lerna/symlink-dependencies": "3.17.0", + "@lerna/validation-error": "3.13.0", + "dedent": "^0.7.0", + "get-port": "^4.2.0", + "multimatch": "^3.0.0", + "npm-package-arg": "^6.1.0", + "npmlog": "^4.1.2", + "p-finally": "^1.0.0", + "p-map": "^2.1.0", + "p-map-series": "^1.0.0", + "p-waterfall": "^1.0.0", + "read-package-tree": "^5.1.6", + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/semver/-/semver-6.3.0.tgz", + "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=" + } + } + }, + "@lerna/changed": { + "version": "3.18.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/changed/-/@lerna/changed-3.18.3.tgz", + "integrity": "sha1-UFKei9XX/i0KzgRqbidNPeZSpJM=", + "requires": { + "@lerna/collect-updates": "3.18.0", + "@lerna/command": "3.18.0", + "@lerna/listable": "3.18.0", + "@lerna/output": "3.13.0", + "@lerna/version": "3.18.3" + } + }, + "@lerna/check-working-tree": { + "version": "3.16.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/check-working-tree/-/@lerna/check-working-tree-3.16.5.tgz", + "integrity": "sha1-tPiuYbtFI1Yd+5+PjYdN1Gu0S6o=", + "requires": { + "@lerna/collect-uncommitted": "3.16.5", + "@lerna/describe-ref": "3.16.5", + "@lerna/validation-error": "3.13.0" + } + }, + "@lerna/child-process": { + "version": "3.16.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/child-process/-/@lerna/child-process-3.16.5.tgz", + "integrity": "sha1-OPo8GAZKpKwHVK2AEUd2p7NqabI=", + "requires": { + "chalk": "^2.3.1", + "execa": "^1.0.0", + "strong-log-transformer": "^2.0.0" + } + }, + "@lerna/clean": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/clean/-/@lerna/clean-3.18.0.tgz", + "integrity": "sha1-zGfXaX25aacOmJmS/fB3EmMI+y4=", + "requires": { + "@lerna/command": "3.18.0", + "@lerna/filter-options": "3.18.0", + "@lerna/prompt": "3.13.0", + "@lerna/pulse-till-done": "3.13.0", + "@lerna/rimraf-dir": "3.16.5", + "p-map": "^2.1.0", + "p-map-series": "^1.0.0", + "p-waterfall": "^1.0.0" + } + }, + "@lerna/cli": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/cli/-/@lerna/cli-3.18.0.tgz", + "integrity": "sha1-K2+GBb7imcatplvC5LPte/cVrzo=", + "requires": { + "@lerna/global-options": "3.13.0", + "dedent": "^0.7.0", + "npmlog": "^4.1.2", + "yargs": "^14.2.0" + } + }, + "@lerna/collect-uncommitted": { + "version": "3.16.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/collect-uncommitted/-/@lerna/collect-uncommitted-3.16.5.tgz", + "integrity": "sha1-pJTWGqwxzceuxLvlLJZVAnQTLmM=", + "requires": { + "@lerna/child-process": "3.16.5", + "chalk": "^2.3.1", + "figgy-pudding": "^3.5.1", + "npmlog": "^4.1.2" + } + }, + "@lerna/collect-updates": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/collect-updates/-/@lerna/collect-updates-3.18.0.tgz", + "integrity": "sha1-YIbGTfMkSZPMCn+PwN3WoBAwCKY=", + "requires": { + "@lerna/child-process": "3.16.5", + "@lerna/describe-ref": "3.16.5", + "minimatch": "^3.0.4", + "npmlog": "^4.1.2", + "slash": "^2.0.0" + } + }, + "@lerna/command": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/command/-/@lerna/command-3.18.0.tgz", + "integrity": "sha1-HkA5kySmnSaniWnVnPYOGbLxP8M=", + "requires": { + "@lerna/child-process": "3.16.5", + "@lerna/package-graph": "3.18.0", + "@lerna/project": "3.18.0", + "@lerna/validation-error": "3.13.0", + "@lerna/write-log-file": "3.13.0", + "dedent": "^0.7.0", + "execa": "^1.0.0", + "is-ci": "^2.0.0", + "lodash": "^4.17.14", + "npmlog": "^4.1.2" + } + }, + "@lerna/conventional-commits": { + "version": "3.16.4", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/conventional-commits/-/@lerna/conventional-commits-3.16.4.tgz", + "integrity": "sha1-v0ZPEbL2U02tIE2wBDDhZRs0agQ=", + "requires": { + "@lerna/validation-error": "3.13.0", + "conventional-changelog-angular": "^5.0.3", + "conventional-changelog-core": "^3.1.6", + "conventional-recommended-bump": "^5.0.0", + "fs-extra": "^8.1.0", + "get-stream": "^4.0.0", + "lodash.template": "^4.5.0", + "npm-package-arg": "^6.1.0", + "npmlog": "^4.1.2", + "pify": "^4.0.1", + "semver": "^6.2.0" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha1-SdQ8RaiM2Wd2aMt74bRu/bjS4cA=", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/semver/-/semver-6.3.0.tgz", + "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=" + } + } + }, + "@lerna/create": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/create/-/@lerna/create-3.18.0.tgz", + "integrity": "sha1-eLpK9eztZhlEoSudfahVPAlsOQ0=", + "requires": { + "@evocateur/pacote": "^9.6.3", + "@lerna/child-process": "3.16.5", + "@lerna/command": "3.18.0", + "@lerna/npm-conf": "3.16.0", + "@lerna/validation-error": "3.13.0", + "camelcase": "^5.0.0", + "dedent": "^0.7.0", + "fs-extra": "^8.1.0", + "globby": "^9.2.0", + "init-package-json": "^1.10.3", + "npm-package-arg": "^6.1.0", + "p-reduce": "^1.0.0", + "pify": "^4.0.1", + "semver": "^6.2.0", + "slash": "^2.0.0", + "validate-npm-package-license": "^3.0.3", + "validate-npm-package-name": "^3.0.0", + "whatwg-url": "^7.0.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha1-48mzFWnhBoEd8kL3FXJaH0xJQyA=" + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha1-SdQ8RaiM2Wd2aMt74bRu/bjS4cA=", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/semver/-/semver-6.3.0.tgz", + "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=" + } + } + }, + "@lerna/create-symlink": { + "version": "3.16.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/create-symlink/-/@lerna/create-symlink-3.16.2.tgz", + "integrity": "sha1-QSy45Zpy9afZRj5ORyGtIHAUmWc=", + "requires": { + "@zkochan/cmd-shim": "^3.1.0", + "fs-extra": "^8.1.0", + "npmlog": "^4.1.2" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha1-SdQ8RaiM2Wd2aMt74bRu/bjS4cA=", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } + } + }, + "@lerna/describe-ref": { + "version": "3.16.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/describe-ref/-/@lerna/describe-ref-3.16.5.tgz", + "integrity": "sha1-ozjCWq7YN9PccLinLER8XGY0asA=", + "requires": { + "@lerna/child-process": "3.16.5", + "npmlog": "^4.1.2" + } + }, + "@lerna/diff": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/diff/-/@lerna/diff-3.18.0.tgz", + "integrity": "sha1-ljj/S0biqLDU6/VM8vJnrC+P2yk=", + "requires": { + "@lerna/child-process": "3.16.5", + "@lerna/command": "3.18.0", + "@lerna/validation-error": "3.13.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/exec": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/exec/-/@lerna/exec-3.18.0.tgz", + "integrity": "sha1-2ewLfKBrdSHwufFKFk4tTKXhs7k=", + "requires": { + "@lerna/child-process": "3.16.5", + "@lerna/command": "3.18.0", + "@lerna/filter-options": "3.18.0", + "@lerna/run-topologically": "3.18.0", + "@lerna/validation-error": "3.13.0", + "p-map": "^2.1.0" + } + }, + "@lerna/filter-options": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/filter-options/-/@lerna/filter-options-3.18.0.tgz", + "integrity": "sha1-QGZn3HWo/IE8Jqkb3nVLanPhqGg=", + "requires": { + "@lerna/collect-updates": "3.18.0", + "@lerna/filter-packages": "3.18.0", + "dedent": "^0.7.0", + "figgy-pudding": "^3.5.1", + "npmlog": "^4.1.2" + } + }, + "@lerna/filter-packages": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/filter-packages/-/@lerna/filter-packages-3.18.0.tgz", + "integrity": "sha1-ano3bShSCNsDqClYz7gXLhebTnA=", + "requires": { + "@lerna/validation-error": "3.13.0", + "multimatch": "^3.0.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/get-npm-exec-opts": { + "version": "3.13.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/get-npm-exec-opts/-/@lerna/get-npm-exec-opts-3.13.0.tgz", + "integrity": "sha1-0bVSywCIGZ/D5+Em+RTjmgjfnqU=", + "requires": { + "npmlog": "^4.1.2" + } + }, + "@lerna/get-packed": { + "version": "3.16.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/get-packed/-/@lerna/get-packed-3.16.0.tgz", + "integrity": "sha1-GzFrcG3O6Gx7qlXlCwh5WUR4Uv8=", + "requires": { + "fs-extra": "^8.1.0", + "ssri": "^6.0.1", + "tar": "^4.4.8" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha1-SdQ8RaiM2Wd2aMt74bRu/bjS4cA=", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } + } + }, + "@lerna/github-client": { + "version": "3.16.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/github-client/-/@lerna/github-client-3.16.5.tgz", + "integrity": "sha1-LrAjXDv3p+XZLXPgmzdhqyHzXC4=", + "requires": { + "@lerna/child-process": "3.16.5", + "@octokit/plugin-enterprise-rest": "^3.6.1", + "@octokit/rest": "^16.28.4", + "git-url-parse": "^11.1.2", + "npmlog": "^4.1.2" + } + }, + "@lerna/gitlab-client": { + "version": "3.15.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/gitlab-client/-/@lerna/gitlab-client-3.15.0.tgz", + "integrity": "sha1-kfTsjGl7WsV/fyW9UP5lnSSqlqY=", + "requires": { + "node-fetch": "^2.5.0", + "npmlog": "^4.1.2", + "whatwg-url": "^7.0.0" + } + }, + "@lerna/global-options": { + "version": "3.13.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/global-options/-/@lerna/global-options-3.13.0.tgz", + "integrity": "sha1-IXZiKQ2watnPLEnY4xAO4o6uuuE=" + }, + "@lerna/has-npm-version": { + "version": "3.16.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/has-npm-version/-/@lerna/has-npm-version-3.16.5.tgz", + "integrity": "sha1-q4OVbyEdiSPqav6bl5s4zHOxUyY=", + "requires": { + "@lerna/child-process": "3.16.5", + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/semver/-/semver-6.3.0.tgz", + "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=" + } + } + }, + "@lerna/import": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/import/-/@lerna/import-3.18.0.tgz", + "integrity": "sha1-xrEks0agl+bA8/HtSSGieNGLyAs=", + "requires": { + "@lerna/child-process": "3.16.5", + "@lerna/command": "3.18.0", + "@lerna/prompt": "3.13.0", + "@lerna/pulse-till-done": "3.13.0", + "@lerna/validation-error": "3.13.0", + "dedent": "^0.7.0", + "fs-extra": "^8.1.0", + "p-map-series": "^1.0.0" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha1-SdQ8RaiM2Wd2aMt74bRu/bjS4cA=", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } + } + }, + "@lerna/init": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/init/-/@lerna/init-3.18.0.tgz", + "integrity": "sha1-sjuRcMzh9GMBcN10To7nV4XqiY0=", + "requires": { + "@lerna/child-process": "3.16.5", + "@lerna/command": "3.18.0", + "fs-extra": "^8.1.0", + "p-map": "^2.1.0", + "write-json-file": "^3.2.0" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha1-SdQ8RaiM2Wd2aMt74bRu/bjS4cA=", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } + } + }, + "@lerna/link": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/link/-/@lerna/link-3.18.0.tgz", + "integrity": "sha1-vHLcYu9Nj7hCsyhoh5gPmLdkeB0=", + "requires": { + "@lerna/command": "3.18.0", + "@lerna/package-graph": "3.18.0", + "@lerna/symlink-dependencies": "3.17.0", + "p-map": "^2.1.0", + "slash": "^2.0.0" + } + }, + "@lerna/list": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/list/-/@lerna/list-3.18.0.tgz", + "integrity": "sha1-bl/lRc5Lp8HuttbPaSQNBsAr1JY=", + "requires": { + "@lerna/command": "3.18.0", + "@lerna/filter-options": "3.18.0", + "@lerna/listable": "3.18.0", + "@lerna/output": "3.13.0" + } + }, + "@lerna/listable": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/listable/-/@lerna/listable-3.18.0.tgz", + "integrity": "sha1-dSsBRAapoBJIZibSLpQO24IFlzo=", + "requires": { + "@lerna/query-graph": "3.18.0", + "chalk": "^2.3.1", + "columnify": "^1.5.4" + } + }, + "@lerna/log-packed": { + "version": "3.16.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/log-packed/-/@lerna/log-packed-3.16.0.tgz", + "integrity": "sha1-+DmRBB7neySVY04URwtCJZ/SvBY=", + "requires": { + "byte-size": "^5.0.1", + "columnify": "^1.5.4", + "has-unicode": "^2.0.1", + "npmlog": "^4.1.2" + } + }, + "@lerna/npm-conf": { + "version": "3.16.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/npm-conf/-/@lerna/npm-conf-3.16.0.tgz", + "integrity": "sha1-HBComuL2wu6WliVXc4aFMA03aCc=", + "requires": { + "config-chain": "^1.1.11", + "pify": "^4.0.1" + } + }, + "@lerna/npm-dist-tag": { + "version": "3.18.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/npm-dist-tag/-/@lerna/npm-dist-tag-3.18.1.tgz", + "integrity": "sha1-1N2C6pLkHpYLcRf4MQLrzXoj5RE=", + "requires": { + "@evocateur/npm-registry-fetch": "^4.0.0", + "@lerna/otplease": "3.16.0", + "figgy-pudding": "^3.5.1", + "npm-package-arg": "^6.1.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/npm-install": { + "version": "3.16.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/npm-install/-/@lerna/npm-install-3.16.5.tgz", + "integrity": "sha1-1r/cFvgShdpmUVrkeSTW4njWN9M=", + "requires": { + "@lerna/child-process": "3.16.5", + "@lerna/get-npm-exec-opts": "3.13.0", + "fs-extra": "^8.1.0", + "npm-package-arg": "^6.1.0", + "npmlog": "^4.1.2", + "signal-exit": "^3.0.2", + "write-pkg": "^3.1.0" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha1-SdQ8RaiM2Wd2aMt74bRu/bjS4cA=", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } + } + }, + "@lerna/npm-publish": { + "version": "3.16.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/npm-publish/-/@lerna/npm-publish-3.16.2.tgz", + "integrity": "sha1-qFC1RzlEbEqnZqDOq/qSg7sL5nY=", + "requires": { + "@evocateur/libnpmpublish": "^1.2.2", + "@lerna/otplease": "3.16.0", + "@lerna/run-lifecycle": "3.16.2", + "figgy-pudding": "^3.5.1", + "fs-extra": "^8.1.0", + "npm-package-arg": "^6.1.0", + "npmlog": "^4.1.2", + "pify": "^4.0.1", + "read-package-json": "^2.0.13" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha1-SdQ8RaiM2Wd2aMt74bRu/bjS4cA=", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } + } + }, + "@lerna/npm-run-script": { + "version": "3.16.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/npm-run-script/-/@lerna/npm-run-script-3.16.5.tgz", + "integrity": "sha1-nC7IJFOibAtG7cC7fBWBbIIfXBU=", + "requires": { + "@lerna/child-process": "3.16.5", + "@lerna/get-npm-exec-opts": "3.13.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/otplease": { + "version": "3.16.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/otplease/-/@lerna/otplease-3.16.0.tgz", + "integrity": "sha1-3mauxPPoNaRl176oS1ikq2WQoPo=", + "requires": { + "@lerna/prompt": "3.13.0", + "figgy-pudding": "^3.5.1" + } + }, + "@lerna/output": { + "version": "3.13.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/output/-/@lerna/output-3.13.0.tgz", + "integrity": "sha1-Pe18yQiyephyIopjDZUK7a56SYk=", + "requires": { + "npmlog": "^4.1.2" + } + }, + "@lerna/pack-directory": { + "version": "3.16.4", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/pack-directory/-/@lerna/pack-directory-3.16.4.tgz", + "integrity": "sha1-Pq5fkb31rP4DhFEO1T+t3EwHRpM=", + "requires": { + "@lerna/get-packed": "3.16.0", + "@lerna/package": "3.16.0", + "@lerna/run-lifecycle": "3.16.2", + "figgy-pudding": "^3.5.1", + "npm-packlist": "^1.4.4", + "npmlog": "^4.1.2", + "tar": "^4.4.10", + "temp-write": "^3.4.0" + } + }, + "@lerna/package": { + "version": "3.16.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/package/-/@lerna/package-3.16.0.tgz", + "integrity": "sha1-fgpG5Gl+2LipwU1Zx/iQ4NOLoTw=", + "requires": { + "load-json-file": "^5.3.0", + "npm-package-arg": "^6.1.0", + "write-pkg": "^3.1.0" + } + }, + "@lerna/package-graph": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/package-graph/-/@lerna/package-graph-3.18.0.tgz", + "integrity": "sha1-60LRRASlWyayRyCBYV4msIF82Ro=", + "requires": { + "@lerna/prerelease-id-from-version": "3.16.0", + "@lerna/validation-error": "3.13.0", + "npm-package-arg": "^6.1.0", + "npmlog": "^4.1.2", + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/semver/-/semver-6.3.0.tgz", + "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=" + } + } + }, + "@lerna/prerelease-id-from-version": { + "version": "3.16.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/prerelease-id-from-version/-/@lerna/prerelease-id-from-version-3.16.0.tgz", + "integrity": "sha1-skv6eJ9eG6q5FNewi6rpt719g6E=", + "requires": { + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/semver/-/semver-6.3.0.tgz", + "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=" + } + } + }, + "@lerna/project": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/project/-/@lerna/project-3.18.0.tgz", + "integrity": "sha1-Vv7uAdrrQsA8vfDtiioQy84y9nA=", + "requires": { + "@lerna/package": "3.16.0", + "@lerna/validation-error": "3.13.0", + "cosmiconfig": "^5.1.0", + "dedent": "^0.7.0", + "dot-prop": "^4.2.0", + "glob-parent": "^5.0.0", + "globby": "^9.2.0", + "load-json-file": "^5.3.0", + "npmlog": "^4.1.2", + "p-map": "^2.1.0", + "resolve-from": "^4.0.0", + "write-json-file": "^3.2.0" + } + }, + "@lerna/prompt": { + "version": "3.13.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/prompt/-/@lerna/prompt-3.13.0.tgz", + "integrity": "sha1-U1cUYrs/U5nMHKbTNaQR/gk0JqU=", + "requires": { + "inquirer": "^6.2.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/publish": { + "version": "3.18.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/publish/-/@lerna/publish-3.18.3.tgz", + "integrity": "sha1-R4u5TucSpAtyNBPkN7y54wfTcJw=", + "requires": { + "@evocateur/libnpmaccess": "^3.1.2", + "@evocateur/npm-registry-fetch": "^4.0.0", + "@evocateur/pacote": "^9.6.3", + "@lerna/check-working-tree": "3.16.5", + "@lerna/child-process": "3.16.5", + "@lerna/collect-updates": "3.18.0", + "@lerna/command": "3.18.0", + "@lerna/describe-ref": "3.16.5", + "@lerna/log-packed": "3.16.0", + "@lerna/npm-conf": "3.16.0", + "@lerna/npm-dist-tag": "3.18.1", + "@lerna/npm-publish": "3.16.2", + "@lerna/otplease": "3.16.0", + "@lerna/output": "3.13.0", + "@lerna/pack-directory": "3.16.4", + "@lerna/prerelease-id-from-version": "3.16.0", + "@lerna/prompt": "3.13.0", + "@lerna/pulse-till-done": "3.13.0", + "@lerna/run-lifecycle": "3.16.2", + "@lerna/run-topologically": "3.18.0", + "@lerna/validation-error": "3.13.0", + "@lerna/version": "3.18.3", + "figgy-pudding": "^3.5.1", + "fs-extra": "^8.1.0", + "npm-package-arg": "^6.1.0", + "npmlog": "^4.1.2", + "p-finally": "^1.0.0", + "p-map": "^2.1.0", + "p-pipe": "^1.2.0", + "semver": "^6.2.0" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha1-SdQ8RaiM2Wd2aMt74bRu/bjS4cA=", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/semver/-/semver-6.3.0.tgz", + "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=" + } + } + }, + "@lerna/pulse-till-done": { + "version": "3.13.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/pulse-till-done/-/@lerna/pulse-till-done-3.13.0.tgz", + "integrity": "sha1-yOnOW6+vENkwpn1+0My12Vj+ARA=", + "requires": { + "npmlog": "^4.1.2" + } + }, + "@lerna/query-graph": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/query-graph/-/@lerna/query-graph-3.18.0.tgz", + "integrity": "sha1-Q4AaLxuAoOoL/Z1C1HBgUyajA10=", + "requires": { + "@lerna/package-graph": "3.18.0", + "figgy-pudding": "^3.5.1" + } + }, + "@lerna/resolve-symlink": { + "version": "3.16.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/resolve-symlink/-/@lerna/resolve-symlink-3.16.0.tgz", + "integrity": "sha1-N/xwlfq9vPMXwm63Tg0L3o79I4Y=", + "requires": { + "fs-extra": "^8.1.0", + "npmlog": "^4.1.2", + "read-cmd-shim": "^1.0.1" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha1-SdQ8RaiM2Wd2aMt74bRu/bjS4cA=", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } + } + }, + "@lerna/rimraf-dir": { + "version": "3.16.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/rimraf-dir/-/@lerna/rimraf-dir-3.16.5.tgz", + "integrity": "sha1-BDFqtf/SkJZXqvOI6lAsuMLyCgk=", + "requires": { + "@lerna/child-process": "3.16.5", + "npmlog": "^4.1.2", + "path-exists": "^3.0.0", + "rimraf": "^2.6.2" + } + }, + "@lerna/run": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/run/-/@lerna/run-3.18.0.tgz", + "integrity": "sha1-twaYgPYxPkxgJrVkt7duXQ8wpSE=", + "requires": { + "@lerna/command": "3.18.0", + "@lerna/filter-options": "3.18.0", + "@lerna/npm-run-script": "3.16.5", + "@lerna/output": "3.13.0", + "@lerna/run-topologically": "3.18.0", + "@lerna/timer": "3.13.0", + "@lerna/validation-error": "3.13.0", + "p-map": "^2.1.0" + } + }, + "@lerna/run-lifecycle": { + "version": "3.16.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/run-lifecycle/-/@lerna/run-lifecycle-3.16.2.tgz", + "integrity": "sha1-Z7KI+OqWTbnqT7H7x3FdW7sLzgA=", + "requires": { + "@lerna/npm-conf": "3.16.0", + "figgy-pudding": "^3.5.1", + "npm-lifecycle": "^3.1.2", + "npmlog": "^4.1.2" + } + }, + "@lerna/run-topologically": { + "version": "3.18.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/run-topologically/-/@lerna/run-topologically-3.18.0.tgz", + "integrity": "sha1-lQhgRVPPvroQbNhLcR+t4XlH+Uo=", + "requires": { + "@lerna/query-graph": "3.18.0", + "figgy-pudding": "^3.5.1", + "p-queue": "^4.0.0" + } + }, + "@lerna/symlink-binary": { + "version": "3.17.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/symlink-binary/-/@lerna/symlink-binary-3.17.0.tgz", + "integrity": "sha1-j4AxswmGOBSIPT8AmHf4Ljiu9Fo=", + "requires": { + "@lerna/create-symlink": "3.16.2", + "@lerna/package": "3.16.0", + "fs-extra": "^8.1.0", + "p-map": "^2.1.0" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha1-SdQ8RaiM2Wd2aMt74bRu/bjS4cA=", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } + } + }, + "@lerna/symlink-dependencies": { + "version": "3.17.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/symlink-dependencies/-/@lerna/symlink-dependencies-3.17.0.tgz", + "integrity": "sha1-SNY2DphYZaDlbNi1GzCKUmMIeEo=", + "requires": { + "@lerna/create-symlink": "3.16.2", + "@lerna/resolve-symlink": "3.16.0", + "@lerna/symlink-binary": "3.17.0", + "fs-extra": "^8.1.0", + "p-finally": "^1.0.0", + "p-map": "^2.1.0", + "p-map-series": "^1.0.0" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha1-SdQ8RaiM2Wd2aMt74bRu/bjS4cA=", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } + } + }, + "@lerna/timer": { + "version": "3.13.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/timer/-/@lerna/timer-3.13.0.tgz", + "integrity": "sha1-vNCQRVHbFuCDZNbBjl4hYPyHB4E=" + }, + "@lerna/validation-error": { + "version": "3.13.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/validation-error/-/@lerna/validation-error-3.13.0.tgz", + "integrity": "sha1-yGuPB8WrlTn3db2KVJdukm83WcM=", + "requires": { + "npmlog": "^4.1.2" + } + }, + "@lerna/version": { + "version": "3.18.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/version/-/@lerna/version-3.18.3.tgz", + "integrity": "sha1-ATRLOcB0n962wXhxRzO6y95NYC8=", + "requires": { + "@lerna/check-working-tree": "3.16.5", + "@lerna/child-process": "3.16.5", + "@lerna/collect-updates": "3.18.0", + "@lerna/command": "3.18.0", + "@lerna/conventional-commits": "3.16.4", + "@lerna/github-client": "3.16.5", + "@lerna/gitlab-client": "3.15.0", + "@lerna/output": "3.13.0", + "@lerna/prerelease-id-from-version": "3.16.0", + "@lerna/prompt": "3.13.0", + "@lerna/run-lifecycle": "3.16.2", + "@lerna/run-topologically": "3.18.0", + "@lerna/validation-error": "3.13.0", + "chalk": "^2.3.1", + "dedent": "^0.7.0", + "load-json-file": "^5.3.0", + "minimatch": "^3.0.4", + "npmlog": "^4.1.2", + "p-map": "^2.1.0", + "p-pipe": "^1.2.0", + "p-reduce": "^1.0.0", + "p-waterfall": "^1.0.0", + "semver": "^6.2.0", + "slash": "^2.0.0", + "temp-write": "^3.4.0", + "write-json-file": "^3.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/semver/-/semver-6.3.0.tgz", + "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=" + } + } + }, + "@lerna/write-log-file": { + "version": "3.13.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@lerna/write-log-file/-/@lerna/write-log-file-3.13.0.tgz", + "integrity": "sha1-t42eTPwTSai+ZNkTJMTIGZ6CKiY=", + "requires": { + "npmlog": "^4.1.2", + "write-file-atomic": "^2.3.0" + } + }, + "@microsoft/recognizers-text": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text/-/recognizers-text-1.1.4.tgz", + "integrity": "sha512-hlSVXcaX5i8JcjuUJpVxmy2Z/GxvFXarF0KVySCFop57wNEnrLWMHe4I4DjP866G19VyIKRw+vPA32pkGhZgTg==" + }, + "@microsoft/recognizers-text-choice": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-choice/-/recognizers-text-choice-1.1.2.tgz", + "integrity": "sha512-4hFdqxusM0YrOXYM2RVYPl2rLjItSh6VkRiACjWB95GKC/DBGjJRYQpTxhzuZAsJSkDMinu/aLf8DvQtwUaLtA==", + "requires": { + "@microsoft/recognizers-text": "~1.1.2", + "grapheme-splitter": "^1.0.2" + } + }, + "@microsoft/recognizers-text-data-types-timex-expression": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-data-types-timex-expression/-/recognizers-text-data-types-timex-expression-1.1.4.tgz", + "integrity": "sha512-2vICaEJfV9EpaDKs5P1PLAEs+WpNqrtpkl7CLsmc5gKmxgpQtsojG4tk6km5JRKg1mYuLV5ZzJ/65oOEeyTMvQ==" + }, + "@microsoft/recognizers-text-date-time": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-date-time/-/recognizers-text-date-time-1.1.2.tgz", + "integrity": "sha512-tsdsl2Z3OMFhfWicFQSS4sbk/c0pVTIXhCglDXm/cfSNuQgNgy2NFFRquiQ6MkVwiIk2I7k76TnkoDZt7PD4Dg==", + "requires": { + "@microsoft/recognizers-text": "~1.1.2", + "@microsoft/recognizers-text-number": "~1.1.2", + "@microsoft/recognizers-text-number-with-unit": "~1.1.2", + "lodash.isequal": "^4.5.0", + "lodash.tonumber": "^4.0.3" + } + }, + "@microsoft/recognizers-text-number": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-number/-/recognizers-text-number-1.1.2.tgz", + "integrity": "sha512-GESjSF42dllym83diyd6pmlzFwdzidewoq/qSQz89lSoTx9HdJQHjbXxwdBp7w4Ax/Jroo2lcAedM3B7alZhYQ==", + "requires": { + "@microsoft/recognizers-text": "~1.1.2", + "bignumber.js": "^7.2.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.sortby": "^4.7.0", + "lodash.trimend": "^4.5.1" + } + }, + "@microsoft/recognizers-text-number-with-unit": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-number-with-unit/-/recognizers-text-number-with-unit-1.1.4.tgz", + "integrity": "sha512-zl+CfmfWK0x/x+iSgaBAevKTYO0F4+z7SYHAHztaaaGuX8FERw2jmUjSgVetm5KA3EveyCx0XYGU1mRNY8p7Eg==", + "requires": { + "@microsoft/recognizers-text": "~1.1.4", + "@microsoft/recognizers-text-number": "~1.1.4", + "lodash.escaperegexp": "^4.1.2", + "lodash.last": "^3.0.0", + "lodash.max": "^4.0.1" + }, + "dependencies": { + "@microsoft/recognizers-text-number": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-number/-/recognizers-text-number-1.1.4.tgz", + "integrity": "sha512-6EmlR+HR+eJBIX7sQby1vs6LJB64wxLowHaGpIU9OCXFvZ5Nb0QT8qh10rC40v3Mtrz4DpScXfSXr9tWkIO5MQ==", + "requires": { + "@microsoft/recognizers-text": "~1.1.4", + "bignumber.js": "^7.2.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.sortby": "^4.7.0", + "lodash.trimend": "^4.5.1" + } + } + } + }, + "@microsoft/recognizers-text-sequence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-sequence/-/recognizers-text-sequence-1.1.4.tgz", + "integrity": "sha512-rb5j8/aE7HSOdIxaVfCGFrj0wWPpSq0CuykFg/A/iJNPP+FnAU71bgP5HexrwQcpCsDinauisX7u0DKIChrHRA==", + "requires": { + "@microsoft/recognizers-text": "~1.1.4", + "grapheme-splitter": "^1.0.2" + } + }, + "@microsoft/recognizers-text-suite": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-suite/-/recognizers-text-suite-1.1.2.tgz", + "integrity": "sha512-w3WCsKa//64jE1fGPFlV02rRg9+b3oDp+K5/skPAn4KDr80LjXxD1ulIgiJ2Ll/2OoBl8ociCiCjYA7zS3LpdQ==", + "requires": { + "@microsoft/recognizers-text": "~1.1.2", + "@microsoft/recognizers-text-choice": "~1.1.2", + "@microsoft/recognizers-text-date-time": "~1.1.2", + "@microsoft/recognizers-text-number": "~1.1.2", + "@microsoft/recognizers-text-number-with-unit": "~1.1.2", + "@microsoft/recognizers-text-sequence": "~1.1.2" + } + }, + "@mrmlnc/readdir-enhanced": { + "version": "2.2.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@mrmlnc/readdir-enhanced/-/@mrmlnc/readdir-enhanced-2.2.1.tgz", + "integrity": "sha1-UkryQNGjYFJ7cwR17PoTRKpUDd4=", + "requires": { + "call-me-maybe": "^1.0.1", + "glob-to-regexp": "^0.3.0" + } + }, + "@nodelib/fs.stat": { + "version": "1.1.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@nodelib/fs.stat/-/@nodelib/fs.stat-1.1.3.tgz", + "integrity": "sha1-K1o6s/kYzKSKjHVMCBaOPwPrphs=" + }, + "@octokit/endpoint": { + "version": "5.5.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@octokit/endpoint/-/@octokit/endpoint-5.5.1.tgz", + "integrity": "sha1-LuqB4RDKdU/y3hHHkVTMq0rhaz8=", + "requires": { + "@octokit/types": "^2.0.0", + "is-plain-object": "^3.0.0", + "universal-user-agent": "^4.0.0" + }, + "dependencies": { + "is-plain-object": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-plain-object/-/is-plain-object-3.0.0.tgz", + "integrity": "sha1-R7/F2htdUNZBEIBsGZNZSC51qSg=", + "requires": { + "isobject": "^4.0.0" + } + }, + "isobject": { + "version": "4.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/isobject/-/isobject-4.0.0.tgz", + "integrity": "sha1-PxyRVec7GSAiqAgZus0DQ3EWl7A=" + } + } + }, + "@octokit/plugin-enterprise-rest": { + "version": "3.6.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@octokit/plugin-enterprise-rest/-/@octokit/plugin-enterprise-rest-3.6.2.tgz", + "integrity": "sha1-dN4lvvIeAYK0+gOoZ4zQCk5n5WE=" + }, + "@octokit/request": { + "version": "5.3.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@octokit/request/-/@octokit/request-5.3.1.tgz", + "integrity": "sha1-OhrOReb4ixvkdJxdqWOzo7Si8SA=", + "requires": { + "@octokit/endpoint": "^5.5.0", + "@octokit/request-error": "^1.0.1", + "@octokit/types": "^2.0.0", + "deprecation": "^2.0.0", + "is-plain-object": "^3.0.0", + "node-fetch": "^2.3.0", + "once": "^1.4.0", + "universal-user-agent": "^4.0.0" + }, + "dependencies": { + "is-plain-object": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-plain-object/-/is-plain-object-3.0.0.tgz", + "integrity": "sha1-R7/F2htdUNZBEIBsGZNZSC51qSg=", + "requires": { + "isobject": "^4.0.0" + } + }, + "isobject": { + "version": "4.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/isobject/-/isobject-4.0.0.tgz", + "integrity": "sha1-PxyRVec7GSAiqAgZus0DQ3EWl7A=" + } + } + }, + "@octokit/request-error": { + "version": "1.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@octokit/request-error/-/@octokit/request-error-1.2.0.tgz", + "integrity": "sha1-pk0qnXoTVVVwzXlyLeSk12Nxuqo=", + "requires": { + "@octokit/types": "^2.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "@octokit/rest": { + "version": "16.34.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@octokit/rest/-/@octokit/rest-16.34.1.tgz", + "integrity": "sha1-4otaVzyi8VuwAvkLwBACCEXtnAE=", + "requires": { + "@octokit/request": "^5.2.0", + "@octokit/request-error": "^1.0.2", + "atob-lite": "^2.0.0", + "before-after-hook": "^2.0.0", + "btoa-lite": "^1.0.0", + "deprecation": "^2.0.0", + "lodash.get": "^4.4.2", + "lodash.set": "^4.3.2", + "lodash.uniq": "^4.5.0", + "octokit-pagination-methods": "^1.1.0", + "once": "^1.4.0", + "universal-user-agent": "^4.0.0" + } + }, + "@octokit/types": { + "version": "2.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@octokit/types/-/@octokit/types-2.0.1.tgz", + "integrity": "sha1-DK8DZOAQKWJlYhWTrJo39A73Xa0=", + "requires": { + "@types/node": ">= 8" + } + }, + "@sindresorhus/is": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", + "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==" + }, + "@sinonjs/commons": { + "version": "1.6.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@sinonjs/commons/-/@sinonjs/commons-1.6.0.tgz", + "integrity": "sha1-7HZwQyrpyOtxBADREsIBo2LYM5M=", + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.2.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@sinonjs/formatio/-/@sinonjs/formatio-3.2.2.tgz", + "integrity": "sha1-dxxg36dep/LWjjuUx+iIp4eBNyw=", + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@sinonjs/samsam/-/@sinonjs/samsam-3.3.3.tgz", + "integrity": "sha1-Rmgu/Zlnslm4ETa58SD9VFhf60o=", + "requires": { + "@sinonjs/commons": "^1.3.0", + "array-from": "^2.1.1", + "lodash": "^4.17.15" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@sinonjs/text-encoding/-/@sinonjs/text-encoding-0.7.1.tgz", + "integrity": "sha1-jaXGUwkVZT86Hzj9XxAdjD+AecU=" + }, + "@types/bunyan": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.6.tgz", + "integrity": "sha512-YiozPOOsS6bIuz31ilYqR5SlLif4TBWsousN2aCWLi5233nZSX19tFbcQUPdR7xJ8ypPyxkCGNxg0CIV5n9qxQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/chai": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.4.tgz", + "integrity": "sha512-7qvf9F9tMTzo0akeswHPGqgUx/gIaJqrOEET/FCD8CFRkSUHlygQiM5yB6OvjrtdxBVLSyw7COJubsFYs0683g==" + }, + "@types/debug": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", + "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==" + }, + "@types/documentdb": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/@types/documentdb/-/documentdb-1.10.5.tgz", + "integrity": "sha512-FHQV9Nc1ffrLkQxO0zFlDCRPyHZtuKmAAuJIi278COhtkKBuBRuKOzoO3JlT0yfUrivPjAzNae+gh9fS++r0Ag==", + "requires": { + "@types/node": "*" + } + }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@types/eslint-visitor-keys/-/@types/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha1-HuMNeVRMqE1o1LPNsK9PIFZj3S0=" + }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@types/events/-/@types/events-3.0.0.tgz", + "integrity": "sha1-KGLz9Yqaf3w+eNefEw3U1xwlwqc=" + }, + "@types/glob": { + "version": "7.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@types/glob/-/@types/glob-7.1.1.tgz", + "integrity": "sha1-qlmhxuP7xCHgfM0xqUTDDrpSFXU=", + "requires": { + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/json-schema": { + "version": "7.0.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@types/json-schema/-/@types/json-schema-7.0.3.tgz", + "integrity": "sha1-vf1p1h5GTcyBslFZwnDXWnPBpjY=" + }, + "@types/jsonpath": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@types/jsonpath/-/jsonpath-0.2.0.tgz", + "integrity": "sha512-v7qlPA0VpKUlEdhghbDqRoKMxFB3h3Ch688TApBJ6v+XLDdvWCGLJIYiPKGZnS6MAOie+IorCfNYVHOPIHSWwQ==" + }, + "@types/jsonwebtoken": { + "version": "8.3.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@types/jsonwebtoken/-/@types/jsonwebtoken-8.3.5.tgz", + "integrity": "sha1-/5vhFRqEQJXfH/X3I2USmMLAdlk=", + "requires": { + "@types/node": "*" + } + }, + "@types/jspath": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@types/jspath/-/jspath-0.4.0.tgz", + "integrity": "sha512-ns6qcoS1OZwRCRezNIS6gWzNormot82WcaQ/6njVey8P2cGDjOjbsbytX35hfGdHqfbEQe3GUoRcmqPltuNHKA==" + }, + "@types/lodash": { + "version": "4.14.144", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@types/lodash/-/@types/lodash-4.14.144.tgz", + "integrity": "sha1-EuV/yZBkvOReWrPIvEeD/rdeq44=" + }, + "@types/lru-cache": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.0.tgz", + "integrity": "sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w==" + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@types/minimatch/-/@types/minimatch-3.0.3.tgz", + "integrity": "sha1-PcoOPzOyAPx9ETnAzZbBJoyt/Z0=" + }, + "@types/mocha": { + "version": "2.2.48", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.48.tgz", + "integrity": "sha512-nlK/iyETgafGli8Zh9zJVCTicvU3iajSkRwOh3Hhiva598CMqNJ4NcVCGMTGKpGpTYj/9R8RLzS9NAykSSCqGw==" + }, + "@types/node": { + "version": "10.17.4", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@types/node/-/@types/node-10.17.4.tgz", + "integrity": "sha1-iZOk/jxAIv2ma/TqZg1hX8VlnG8=" + }, + "@types/node-fetch": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.3.tgz", + "integrity": "sha512-X3TNlzZ7SuSwZsMkb5fV7GrPbVKvHc2iwHmslb8bIxRKWg2iqkfm3F/Wd79RhDpOXR7wCtKAwc5Y2JE6n/ibyw==", + "requires": { + "@types/node": "*" + } + }, + "@types/restify": { + "version": "7.2.12", + "resolved": "https://registry.npmjs.org/@types/restify/-/restify-7.2.12.tgz", + "integrity": "sha512-1AcxgYz3WnAx/NvpxROSiNCJSGmbl2ho8CNyAg+fhuo+oopXKEitQwXTR7/ibhKAzrBaerxRF6nn2YXXaTkNrg==", + "requires": { + "@types/bunyan": "*", + "@types/node": "*", + "@types/spdy": "*" + } + }, + "@types/semaphore": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/semaphore/-/semaphore-1.1.0.tgz", + "integrity": "sha512-YD+lyrPhrsJdSOaxmA9K1lzsCoN0J29IsQGMKd67SbkPDXxJPdwdqpok1sytD19NEozUaFpjIsKOWnJDOYO/GA==" + }, + "@types/spdy": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@types/spdy/-/spdy-3.4.4.tgz", + "integrity": "sha512-N9LBlbVRRYq6HgYpPkqQc3a9HJ/iEtVZToW6xlTtJiMhmRJ7jJdV7TaZQJw/Ve/1ePUsQiCTDc4JMuzzag94GA==", + "requires": { + "@types/node": "*" + } + }, + "@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-FKjsOVbC6B7bdSB5CuzyHCkK69I=" + }, + "@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==" + }, + "@types/tunnel": { + "version": "0.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@types/tunnel/-/@types/tunnel-0.0.0.tgz", + "integrity": "sha1-wqQpQ+5jyQZSpVV7jE5Wzad/lE4=", + "requires": { + "@types/node": "*" + } + }, + "@types/uuid": { + "version": "3.4.6", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.6.tgz", + "integrity": "sha512-cCdlC/1kGEZdEglzOieLDYBxHsvEOIg7kp/2FYyVR9Pxakq+Qf/inL3RKQ+PA8gOlI/NnL+fXmQH12nwcGzsHw==", + "requires": { + "@types/node": "*" + } + }, + "@types/ws": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.3.tgz", + "integrity": "sha512-yBTM0P05Tx9iXGq00BbJPo37ox68R5vaGTXivs6RGh/BQ6QP5zqZDGWdAO6JbRE/iR1l80xeGAwCQS2nMV9S/w==", + "requires": { + "@types/node": "*" + } + }, + "@types/xml2js": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.5.tgz", + "integrity": "sha512-yohU3zMn0fkhlape1nxXG2bLEGZRc1FeqF80RoHaYXJN7uibaauXfhzhOJr1Xh36sn+/tx21QAOf07b/xYVk1w==", + "requires": { + "@types/node": "*" + } + }, + "@typescript-eslint/eslint-plugin": { + "version": "1.13.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@typescript-eslint/eslint-plugin/-/@typescript-eslint/eslint-plugin-1.13.0.tgz", + "integrity": "sha1-Iv7ZsW3f60Av17zeVjB4IPbrxJ8=", + "requires": { + "@typescript-eslint/experimental-utils": "1.13.0", + "eslint-utils": "^1.3.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^2.0.1", + "tsutils": "^3.7.0" + } + }, + "@typescript-eslint/experimental-utils": { + "version": "1.13.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@typescript-eslint/experimental-utils/-/@typescript-eslint/experimental-utils-1.13.0.tgz", + "integrity": "sha1-sIxg14DABn3i+0SwS0MvVAE4MB4=", + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "1.13.0", + "eslint-scope": "^4.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "1.13.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@typescript-eslint/parser/-/@typescript-eslint/parser-1.13.0.tgz", + "integrity": "sha1-Yax4EepSeRxH3J/U3UoYT66aw1U=", + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "1.13.0", + "@typescript-eslint/typescript-estree": "1.13.0", + "eslint-visitor-keys": "^1.0.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "1.13.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@typescript-eslint/typescript-estree/-/@typescript-eslint/typescript-estree-1.13.0.tgz", + "integrity": "sha1-gUDxfQ9gwDYZeY8dYouENJE9wy4=", + "requires": { + "lodash.unescape": "4.0.1", + "semver": "5.5.0" + } + }, + "@zkochan/cmd-shim": { + "version": "3.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@zkochan/cmd-shim/-/@zkochan/cmd-shim-3.1.0.tgz", + "integrity": "sha1-KrjtgfW7VFKoXyV1jrm4aBmC/S4=", + "requires": { + "is-windows": "^1.0.0", + "mkdirp-promise": "^5.0.1", + "mz": "^2.5.0" + } + }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha1-MgjB8I06TZkmGrZPkjArwV4RHKA=", + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg=" + }, + "acorn": { + "version": "6.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/acorn/-/acorn-6.3.0.tgz", + "integrity": "sha1-AIdQkRn/pPwKAEHR6TpBfmjLhW4=" + }, + "acorn-jsx": { + "version": "5.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/acorn-jsx/-/acorn-jsx-5.1.0.tgz", + "integrity": "sha1-KUrbcbVzmLBoABXwo4xWPuHbU4Q=" + }, + "adal-node": { + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz", + "integrity": "sha1-RoxLs+u9lrEnBmn0ucuk4AZepIU=", + "requires": { + "@types/node": "^8.0.47", + "async": ">=0.6.0", + "date-utils": "*", + "jws": "3.x.x", + "request": ">= 2.52.0", + "underscore": ">= 1.3.1", + "uuid": "^3.1.0", + "xmldom": ">= 0.1.x", + "xpath.js": "~1.1.0" + }, + "dependencies": { + "@types/node": { + "version": "8.10.58", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@types/node/-/@types/node-8.10.58.tgz", + "integrity": "sha1-mMFM6VpjRwG9LVnVLfiCwGEN0Os=" + } + } + }, + "adm-zip": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.11.tgz", + "integrity": "sha512-L8vcjDTCOIJk7wFvmlEUN7AsSb8T+2JrdP7KINBjzr24TJ5Mwj590sLu3BC7zNZowvJWa/JtPmD8eJCzdtDWjA==" + }, + "agent-base": { + "version": "4.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha1-gWXwHENgCbzK0LHRIvBe13Dvxu4=", + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "agentkeepalive": { + "version": "3.5.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/agentkeepalive/-/agentkeepalive-3.5.2.tgz", + "integrity": "sha1-oROSTdP6JKC8O3gQjEUMKr7gD2c=", + "requires": { + "humanize-ms": "^1.2.1" + } + }, + "ajv": { + "version": "6.10.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha1-086gTWsBeyiUrWkED+yLYj60vVI=", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha1-V9NbhoboUeLMBMQD8cACA5dqGBM=" + }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha1-h4C5j/nb9WOBUtHx/lwde0RCl2s=" + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=", + "requires": { + "color-convert": "^1.9.0" + } + }, + "antlr4ts": { + "version": "0.5.0-alpha.1", + "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.1.tgz", + "integrity": "sha512-LU5FLWq2fUwg2cTL/DeIL16ucUm5jv6SNVFoMjbYLviXAp6p5g1ZzkTAnWiOKX/muEEy0PY78perPj6WUBSQCw==" + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, + "app-root-path": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.2.1.tgz", + "integrity": "sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA==" + }, + "applicationinsights": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-1.2.0.tgz", + "integrity": "sha512-zb2id/cGdapn7sSH9rotgzic7Cje9k9zb+e9RrrQxG2GuOPPN0kD03FqO8qIAd3HvdtefQY3tTZXbQKo0qtmKw==", + "requires": { + "cls-hooked": "^4.2.2", + "continuation-local-storage": "^3.2.1", + "diagnostic-channel": "0.2.0", + "diagnostic-channel-publishers": "0.3.0" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo=" + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha1-SzXClE8GKov82mZBB2A1D+nd/CE=", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha1-vNZ5HqWuCXJeF+WtmIE0zUCz2RE=", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "array-differ": { + "version": "2.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/array-differ/-/array-differ-2.1.0.tgz", + "integrity": "sha1-S5wcPxS5BnVwgpJXaeirkE9IAbE=" + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" + }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=" + }, + "array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=" + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha1-jSR136tVO7M+d7VOWeiAu4ziMTY=", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "http://bbnpm.azurewebsites.net/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha1-bIw/uCfdQ+45GPJ7gngqt2WKb9k=" + }, + "async": { + "version": "2.6.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/async/-/async-2.6.0.tgz", + "integrity": "sha1-YaKau2/MAm/qd+VtHG7FOnlZUfQ=", + "requires": { + "lodash": "^4.14.0" + } + }, + "async-hook-jl": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz", + "integrity": "sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==", + "requires": { + "stack-chain": "^1.3.7" + } + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, + "async-listener": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz", + "integrity": "sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==", + "requires": { + "semver": "^5.3.0", + "shimmer": "^1.1.0" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "http://bbnpm.azurewebsites.net/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "atob": { + "version": "2.1.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/atob/-/atob-2.1.2.tgz", + "integrity": "sha1-bZUX654DDSQ2ZmZR6GvZ9vE1M8k=" + }, + "atob-lite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz", + "integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "http://bbnpm.azurewebsites.net/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha1-8OAD2cqef1nHpQiUXXsu+aBKVC8=" + }, + "axios": { + "version": "0.19.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/axios/-/axios-0.19.0.tgz", + "integrity": "sha1-jgm/89kSLhM/e4EByPvdAO09Krg=", + "requires": { + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + } + }, + "azure-storage": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/azure-storage/-/azure-storage-2.10.2.tgz", + "integrity": "sha512-pOyGPya9+NDpAfm5YcFfklo57HfjDbYLXxs4lomPwvRxmb0Di/A+a+RkUmEFzaQ8S13CqxK40bRRB0sjj2ZQxA==", + "requires": { + "browserify-mime": "~1.2.9", + "extend": "^3.0.2", + "json-edm-parser": "0.1.2", + "md5.js": "1.3.4", + "readable-stream": "~2.0.0", + "request": "^2.86.0", + "underscore": "~1.8.3", + "uuid": "^3.0.0", + "validator": "~9.4.1", + "xml2js": "0.2.8", + "xmlbuilder": "^9.0.7" + }, + "dependencies": { + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + }, + "sax": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", + "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=" + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + }, + "xml2js": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.8.tgz", + "integrity": "sha1-m4FpCTFjH/CdGVdUn69U9PmAs8I=", + "requires": { + "sax": "0.5.x" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + } + } + }, + "backbone": { + "version": "1.4.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/backbone/-/backbone-1.4.0.tgz", + "integrity": "sha1-VNtN6d98OBHD8DLzR0mkzSfzvRI=", + "requires": { + "underscore": ">=1.8.3" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "http://bbnpm.azurewebsites.net/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/base/-/base-0.11.2.tgz", + "integrity": "sha1-e95c7RRbbVUakNuH+DxVi060io8=", + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "before-after-hook": { + "version": "2.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/before-after-hook/-/before-after-hook-2.1.0.tgz", + "integrity": "sha1-tsA0h/ROJCAN0wyl5qGXnF0vtjU=" + }, + "big-integer": { + "version": "1.6.47", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.47.tgz", + "integrity": "sha512-9t9f7X3as2XGX8b52GqG6ox0GvIdM86LyIXASJnDCFhYNgt+A+MByQZ3W2PyMRZjEvG5f8TEbSPfEotVuMJnQg==" + }, + "bignumber.js": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", + "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==" + }, + "binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", + "requires": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + } + }, + "binary-search-bounds": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.3.tgz", + "integrity": "sha1-X/hhbW3SylOIvIWy1iZuK52lAtw=" + }, + "bluebird": { + "version": "3.7.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/bluebird/-/bluebird-3.7.1.tgz", + "integrity": "sha1-33DjArRx10c0iazyapPWO1P4dN4=" + }, + "botbuilder": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/botbuilder/-/botbuilder-4.6.0.tgz", + "integrity": "sha512-dXfyAjobTyJ5ViOurEt088uOw82cpuzQB+L1J041GbEBZ/DzjAAC/8TMGn++j4++kzE6WsFc+D0hZuBAF2YzgA==", + "requires": { + "@types/node": "^10.12.18", + "botbuilder-core": "^4.6.0", + "botframework-connector": "^4.6.0", + "filenamify": "^4.1.0", + "fs-extra": "^7.0.1" + } + }, + "botbuilder-ai": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/botbuilder-ai/-/botbuilder-ai-4.6.0.tgz", + "integrity": "sha512-Xj40ihYKAhbxzYMwdR+ei1YnTKREFlTGE4mkXIgb5ipdAnPA6cQZV2MV/AYY591/5LKrEzKRpgAXzy1IChwjPQ==", + "requires": { + "@azure/cognitiveservices-luis-runtime": "2.0.0", + "@azure/ms-rest-js": "1.8.13", + "@microsoft/recognizers-text-date-time": "1.1.2", + "@types/node": "^10.12.18", + "botbuilder-core": "^4.6.0", + "moment": "^2.20.1", + "node-fetch": "^2.3.0", + "url-parse": "^1.4.4" + } + }, + "botbuilder-core": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/botbuilder-core/-/botbuilder-core-4.6.0.tgz", + "integrity": "sha512-NrZENrtKDoe6SlFhc+tSdkSbC9qWhEFrk60VOUiT6Q4OMfz0eFZc3HwrcN2obcYc5eQteW+zCEvvi/r41DLP4g==", + "requires": { + "assert": "^1.4.1", + "botframework-schema": "^4.6.0" + } + }, + "botbuilder-dialogs": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/botbuilder-dialogs/-/botbuilder-dialogs-4.6.0.tgz", + "integrity": "sha512-mDpxbh6A9DcXHXHQNkVHqWmyARtdrI3CyIEwa244n2O/KH9LKMQ+71oSwl82Qm8lsQTTAZ93atFQcgtMDtuEmQ==", + "requires": { + "@microsoft/recognizers-text-choice": "1.1.4", + "@microsoft/recognizers-text-date-time": "1.1.4", + "@microsoft/recognizers-text-number": "1.1.4", + "@microsoft/recognizers-text-suite": "1.1.4", + "@types/node": "^10.12.18", + "botbuilder-core": "^4.6.0", + "cldr-data": "^35.1.0", + "globalize": "^1.4.2" + }, + "dependencies": { + "@microsoft/recognizers-text-choice": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-choice/-/recognizers-text-choice-1.1.4.tgz", + "integrity": "sha512-4CddwFe4RVhZeJgW65ocBrEdeukBMghK8pgI0K0Qy2eA5ysPZQpeZ7BGSDz5QMQei5LPY+QaAQ3CHU+ORHoO7A==", + "requires": { + "@microsoft/recognizers-text": "~1.1.4", + "grapheme-splitter": "^1.0.2" + } + }, + "@microsoft/recognizers-text-date-time": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-date-time/-/recognizers-text-date-time-1.1.4.tgz", + "integrity": "sha512-leMnjN+KYNwNvRD5T4G0ORUzkjlek/BBZDvQIjAujtyrd/pkViUnuouWIPkFT/dbSOxXML8et54CSk2KfHiWIA==", + "requires": { + "@microsoft/recognizers-text": "~1.1.4", + "@microsoft/recognizers-text-number": "~1.1.4", + "@microsoft/recognizers-text-number-with-unit": "~1.1.4", + "lodash.isequal": "^4.5.0", + "lodash.tonumber": "^4.0.3" + } + }, + "@microsoft/recognizers-text-number": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-number/-/recognizers-text-number-1.1.4.tgz", + "integrity": "sha512-6EmlR+HR+eJBIX7sQby1vs6LJB64wxLowHaGpIU9OCXFvZ5Nb0QT8qh10rC40v3Mtrz4DpScXfSXr9tWkIO5MQ==", + "requires": { + "@microsoft/recognizers-text": "~1.1.4", + "bignumber.js": "^7.2.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.sortby": "^4.7.0", + "lodash.trimend": "^4.5.1" + } + }, + "@microsoft/recognizers-text-suite": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-suite/-/recognizers-text-suite-1.1.4.tgz", + "integrity": "sha512-hNIaR4M2G0nNeI9WZxt9C0KYh/1vhjeKzX5Ds8XDdT0pxF7zwCSo19WNcPjrVK6aCOeZTw/ULofsAjdu9gSkcA==", + "requires": { + "@microsoft/recognizers-text": "~1.1.4", + "@microsoft/recognizers-text-choice": "~1.1.4", + "@microsoft/recognizers-text-date-time": "~1.1.4", + "@microsoft/recognizers-text-number": "~1.1.4", + "@microsoft/recognizers-text-number-with-unit": "~1.1.4", + "@microsoft/recognizers-text-sequence": "~1.1.4" + } + } + } + }, + "botframework-config": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/botframework-config/-/botframework-config-4.6.0.tgz", + "integrity": "sha512-sWm5Lx1Y0d6/0/YCYzCQ+Y4W0zPgDDbkOq+tURiKSGfOwLNa66DVVP7EwTmadgPox02UOYI7q1l7MqMa6mzHYg==", + "requires": { + "fs-extra": "^7.0.0", + "read-text-file": "^1.1.0", + "uuid": "^3.3.2" + } + }, + "botframework-connector": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/botframework-connector/-/botframework-connector-4.6.0.tgz", + "integrity": "sha512-sbqSWpZBC+0hA7+ExyaSJOy88XeuINTGWSOcAxx5JyJAXosBr0Bdd4y+I4evq5VhSj3GME2g/DYa8d73i/mFMQ==", + "requires": { + "@azure/ms-rest-js": "1.2.6", + "@types/jsonwebtoken": "7.2.8", + "@types/node": "^10.12.18", + "base64url": "^3.0.0", + "botframework-schema": "^4.6.0", + "form-data": "^2.3.3", + "jsonwebtoken": "8.0.1", + "node-fetch": "^2.2.1", + "rsa-pem-from-mod-exp": "^0.8.4" + }, + "dependencies": { + "@azure/ms-rest-js": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-1.2.6.tgz", + "integrity": "sha512-8cmDpxsQjVdveJwYKtNnkJorxEORLYJu9UHaUvLZA6yHExzDeISHAcSVWE0J05+VkJtqheVHF17M+2ro18Cdnw==", + "requires": { + "axios": "^0.18.0", + "form-data": "^2.3.2", + "tough-cookie": "^2.4.3", + "tslib": "^1.9.2", + "uuid": "^3.2.1", + "xml2js": "^0.4.19" + } + }, + "@types/jsonwebtoken": { + "version": "7.2.8", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-7.2.8.tgz", + "integrity": "sha512-XENN3YzEB8D6TiUww0O8SRznzy1v+77lH7UmuN54xq/IHIsyWjWOzZuFFTtoiRuaE782uAoRwBe/wwow+vQXZw==", + "requires": { + "@types/node": "*" + } + }, + "axios": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", + "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==", + "requires": { + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + } + } + } + }, + "botframework-schema": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/botframework-schema/-/botframework-schema-4.6.0.tgz", + "integrity": "sha512-3ndSI2S17Do6RRncYN3Z+N1bkui9HPHIw2lyIGjyX7grukmAcnex/xfpafK++UqmhvF65OdY/Xa4njn7E/tKEQ==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/braces/-/braces-2.3.2.tgz", + "integrity": "sha1-WXn9PxTNUxVl5fot8av/8d+u5yk=", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha1-uqVZ7hTO1zRSIputcyZGfGH6vWA=" + }, + "browserify-mime": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/browserify-mime/-/browserify-mime-1.2.9.tgz", + "integrity": "sha1-rrGvKN5sDXpqLOQK22j/GEIq8x8=" + }, + "btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==" + }, + "btoa-lite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", + "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=" + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8=" + }, + "buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=" + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" + }, + "builtins": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", + "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=" + }, + "bunyan": { + "version": "1.8.12", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz", + "integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=", + "requires": { + "dtrace-provider": "~0.8", + "moment": "^2.10.6", + "mv": "~2", + "safe-json-stringify": "~1" + } + }, + "byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=" + }, + "byte-size": { + "version": "5.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/byte-size/-/byte-size-5.0.1.tgz", + "integrity": "sha1-S2UQOaXs2Wdn5xo9ftOA5IvtQZE=" + }, + "cacache": { + "version": "12.0.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/cacache/-/cacache-12.0.3.tgz", + "integrity": "sha1-vpmruk4b9d9GHNWiwQcfxDJXM5A=", + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha1-Cn9GQWgxyLZi7jb+TnxZ129marI=", + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "cacheable-request": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", + "integrity": "sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=", + "requires": { + "clone-response": "1.0.2", + "get-stream": "3.0.0", + "http-cache-semantics": "3.8.1", + "keyv": "3.0.0", + "lowercase-keys": "1.0.0", + "normalize-url": "2.0.1", + "responselike": "1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "lowercase-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", + "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=" + }, + "normalize-url": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", + "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", + "requires": { + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" + } + } + } + }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "requires": { + "callsites": "^2.0.0" + }, + "dependencies": { + "callsites": { + "version": "2.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=" + } + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha1-s2MKvYlDQy9Us/BRkjjjPNffL3M=" + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + }, + "camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "requires": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "http://bbnpm.azurewebsites.net/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", + "requires": { + "traverse": ">=0.3.0 <0.4" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha1-zUJUFnelQzPPVBpJEIwUMrRMlCQ=", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha1-kAlISfCTfy7twkJdDSip5fDLrZ4=" + }, + "chatdown": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/chatdown/-/chatdown-1.2.3.tgz", + "integrity": "sha512-87ayTdkOyXYxDVZsikMVIMiEUas5kdT/2s0ideCwKjhk/2SvGG3QAVCoCmdfnNpDaWHAP4/5AR9Ql+571C9r4Q==", + "requires": { + "botframework-schema": "^4.0.0-preview1.2", + "chalk": "2.4.1", + "cli-table3": "^0.5.1", + "fs-extra": "^5.0.0", + "glob": "^7.1.3", + "intercept-stdout": "^0.1.2", + "latest-version": "^4.0.0", + "mime-types": "^2.1.18", + "minimist": "^1.2.0", + "please-upgrade-node": "^3.0.1", + "request": "^2.88.0", + "request-promise-native": "^1.0.5", + "semver": "^5.5.1", + "window-size": "^1.1.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "fs-extra": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", + "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" + }, + "chownr": { + "version": "1.1.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/chownr/-/chownr-1.1.3.tgz", + "integrity": "sha1-Qtg31SOWiNVfMDADpQgjD6ZycUI=" + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha1-Z6npZL4xpR4V5QENWObxKDQAL0Y=" + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha1-+TNprouafOAv1B+q0MqDAzGQxGM=", + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "cldr-data": { + "version": "35.1.0", + "resolved": "https://registry.npmjs.org/cldr-data/-/cldr-data-35.1.0.tgz", + "integrity": "sha512-HreWlQ/Yy4AZVGD9aB6cOvpwPYGW3Vss62Bhojcy7r/MgX13PZsr90ujwcAlLATA0o2446H04MD6OqZRfJ84aw==", + "requires": { + "cldr-data-downloader": "0.3.x", + "glob": "5.x.x" + }, + "dependencies": { + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "cldr-data-downloader": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/cldr-data-downloader/-/cldr-data-downloader-0.3.5.tgz", + "integrity": "sha512-uyIMa1K98DAp/PE7dYpq2COIrkWn681Atjng1GgEzeJzYb1jANtugtp9wre6+voE+qzVC8jtWv6E/xZ1GTJdlw==", + "requires": { + "adm-zip": "0.4.11", + "mkdirp": "0.5.0", + "nopt": "3.0.x", + "progress": "1.1.8", + "q": "1.0.1", + "request": "~2.87.0", + "request-progress": "0.3.1" + }, + "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "^5.1.0", + "har-schema": "^2.0.0" + } + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", + "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", + "requires": { + "minimist": "0.0.8" + } + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "q": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.0.1.tgz", + "integrity": "sha1-EYcq7t7okmgRCxCnGESP+xARKhQ=" + }, + "request": { + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.1", + "forever-agent": "~0.6.1", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" + } + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "requires": { + "punycode": "^1.4.1" + } + } + } + }, + "cldrjs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/cldrjs/-/cldrjs-0.5.1.tgz", + "integrity": "sha512-xyiP8uAm8K1IhmpDndZLraloW1yqu0L+HYdQ7O1aGPxx9Cr+BMnPANlNhSt++UKfxytL2hd2NPXgTjiy7k43Ew==" + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-table3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", + "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", + "requires": { + "colors": "^1.1.2", + "object-assign": "^4.1.0", + "string-width": "^2.1.1" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha1-3u/P2y6AB4SqNPRvoI4GhRx7u8U=", + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc=" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha1-InZ74htirxCBV0MG9prFG2IgOWE=", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "cls-hooked": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz", + "integrity": "sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==", + "requires": { + "async-hook-jl": "^1.7.6", + "emitter-listener": "^1.0.1", + "semver": "^5.4.1" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "http://bbnpm.azurewebsites.net/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "codelyzer": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-4.5.0.tgz", + "integrity": "sha512-oO6vCkjqsVrEsmh58oNlnJkRXuA30hF8cdNAQV9DytEalDwyOFRvHMnlKFzmOStNerOmPGZU9GAHnBo4tGvtiQ==", + "requires": { + "app-root-path": "^2.1.0", + "css-selector-tokenizer": "^0.7.0", + "cssauron": "^1.4.0", + "semver-dsl": "^1.0.1", + "source-map": "^0.5.7", + "sprintf-js": "^1.1.1" + }, + "dependencies": { + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" + } + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha1-u3GFBpDh8TZWfeYp0tVHHe2kweg=", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "http://bbnpm.azurewebsites.net/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "optional": true + }, + "columnify": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.5.4.tgz", + "integrity": "sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs=", + "requires": { + "strip-ansi": "^3.0.0", + "wcwidth": "^1.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha1-w9RaizT9cwYxoRCoolIGgrMdWn8=", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/commander/-/commander-2.20.3.tgz", + "integrity": "sha1-/UhehMA+tIgcIHIrpIA16FMa6zM=" + }, + "compare-func": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-1.3.2.tgz", + "integrity": "sha1-md0LpFfh+bxyKxLAjsM+6rMfpkg=", + "requires": { + "array-ify": "^1.0.0", + "dot-prop": "^3.0.0" + }, + "dependencies": { + "dot-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz", + "integrity": "sha1-G3CK8JSknJoOfbyteQq6U52sEXc=", + "requires": { + "is-obj": "^1.0.0" + } + } + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha1-FuQHD7qK4ptnnyIVhT7hgasuq8A=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "http://bbnpm.azurewebsites.net/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha1-kEvfGUzTEi/Gdcd/xKw9T/D9GjQ=", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "config-chain": { + "version": "1.1.12", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/config-chain/-/config-chain-1.1.12.tgz", + "integrity": "sha1-D96NCRIA616AjK8l/mGMAvSOTvo=", + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "continuation-local-storage": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", + "integrity": "sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==", + "requires": { + "async-listener": "^0.6.0", + "emitter-listener": "^1.1.1" + } + }, + "conventional-changelog-angular": { + "version": "5.0.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/conventional-changelog-angular/-/conventional-changelog-angular-5.0.5.tgz", + "integrity": "sha1-abVBvPPlOKhXix5fuqvpvY9XK1c=", + "requires": { + "compare-func": "^1.3.1", + "q": "^1.5.1" + } + }, + "conventional-changelog-core": { + "version": "3.2.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/conventional-changelog-core/-/conventional-changelog-core-3.2.3.tgz", + "integrity": "sha1-sxQQhW9DHIRwhqfctNLKGEp9iPs=", + "requires": { + "conventional-changelog-writer": "^4.0.6", + "conventional-commits-parser": "^3.0.3", + "dateformat": "^3.0.0", + "get-pkg-repo": "^1.0.0", + "git-raw-commits": "2.0.0", + "git-remote-origin-url": "^2.0.0", + "git-semver-tags": "^2.0.3", + "lodash": "^4.2.1", + "normalize-package-data": "^2.3.5", + "q": "^1.5.1", + "read-pkg": "^3.0.0", + "read-pkg-up": "^3.0.0", + "through2": "^3.0.0" + }, + "dependencies": { + "through2": { + "version": "3.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/through2/-/through2-3.0.1.tgz", + "integrity": "sha1-OSducTwzAu3544jdnIEt07glvVo=", + "requires": { + "readable-stream": "2 || 3" + } + } + } + }, + "conventional-changelog-preset-loader": { + "version": "2.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.2.0.tgz", + "integrity": "sha1-Vx4rPXtT1lWHvqnu3243+qXbT8w=" + }, + "conventional-changelog-writer": { + "version": "4.0.9", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/conventional-changelog-writer/-/conventional-changelog-writer-4.0.9.tgz", + "integrity": "sha1-RKxMSBIbyQ5xyylH4eoabCIszX8=", + "requires": { + "compare-func": "^1.3.1", + "conventional-commits-filter": "^2.0.2", + "dateformat": "^3.0.0", + "handlebars": "^4.4.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.2.1", + "meow": "^4.0.0", + "semver": "^6.0.0", + "split": "^1.0.0", + "through2": "^3.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/semver/-/semver-6.3.0.tgz", + "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=" + }, + "through2": { + "version": "3.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/through2/-/through2-3.0.1.tgz", + "integrity": "sha1-OSducTwzAu3544jdnIEt07glvVo=", + "requires": { + "readable-stream": "2 || 3" + } + } + } + }, + "conventional-commits-filter": { + "version": "2.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/conventional-commits-filter/-/conventional-commits-filter-2.0.2.tgz", + "integrity": "sha1-8SL4n7zVu4Hiry/KwCVNBi0QOcE=", + "requires": { + "lodash.ismatch": "^4.4.0", + "modify-values": "^1.0.0" + } + }, + "conventional-commits-parser": { + "version": "3.0.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/conventional-commits-parser/-/conventional-commits-parser-3.0.5.tgz", + "integrity": "sha1-30cdbLP2/s/RNWrHLgtXfb2uCpw=", + "requires": { + "JSONStream": "^1.0.4", + "is-text-path": "^2.0.0", + "lodash": "^4.2.1", + "meow": "^4.0.0", + "split2": "^2.0.0", + "through2": "^3.0.0", + "trim-off-newlines": "^1.0.0" + }, + "dependencies": { + "through2": { + "version": "3.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/through2/-/through2-3.0.1.tgz", + "integrity": "sha1-OSducTwzAu3544jdnIEt07glvVo=", + "requires": { + "readable-stream": "2 || 3" + } + } + } + }, + "conventional-recommended-bump": { + "version": "5.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/conventional-recommended-bump/-/conventional-recommended-bump-5.0.1.tgz", + "integrity": "sha1-WvY5A5R7bgied3Z2ActZLKuxBro=", + "requires": { + "concat-stream": "^2.0.0", + "conventional-changelog-preset-loader": "^2.1.1", + "conventional-commits-filter": "^2.0.2", + "conventional-commits-parser": "^3.0.3", + "git-raw-commits": "2.0.0", + "git-semver-tags": "^2.0.3", + "meow": "^4.0.0", + "q": "^1.5.1" + }, + "dependencies": { + "concat-stream": { + "version": "2.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha1-QUz1r3kKSMYKub5FJ9VtXkETPLE=", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "readable-stream": { + "version": "3.4.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha1-pRwmdUZY4KPCHb9ZFjvUW6b0R/w=", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==" + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha1-kilzmMrjSTf8r9bsgTnBgFHwteA=", + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "http://bbnpm.azurewebsites.net/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha1-BA9yaAnFked6F8CjYmykW08Wixo=", + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "dependencies": { + "import-fresh": { + "version": "2.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" + } + } + }, + "coveralls": { + "version": "3.0.7", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/coveralls/-/coveralls-3.0.7.tgz", + "integrity": "sha1-HspI5Hl26Vc9ai8YuXwv6kAm80o=", + "requires": { + "growl": "~> 1.10.0", + "js-yaml": "^3.13.1", + "lcov-parse": "^0.0.10", + "log-driver": "^1.2.7", + "minimist": "^1.2.0", + "request": "^2.86.0" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha1-Sl7Hxk364iw6FBJNus3uhG2Ay8Q=", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "css-selector-tokenizer": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz", + "integrity": "sha512-xYL0AMZJ4gFzJQsHUKa5jiWWi2vH77WVNg7JYRyewwj6oPh4yb/y6Y9ZCw9dsj/9UauMhtuxR+ogQd//EdEVNA==", + "requires": { + "cssesc": "^0.1.0", + "fastparse": "^1.1.1", + "regexpu-core": "^1.0.0" + } + }, + "cssauron": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz", + "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=", + "requires": { + "through": "X.X.X" + } + }, + "cssesc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", + "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=" + }, + "csv": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/csv/-/csv-1.2.1.tgz", + "integrity": "sha1-UjHt/BxxUlEuxFeBB2p6l/9SXAw=", + "requires": { + "csv-generate": "^1.1.2", + "csv-parse": "^1.3.3", + "csv-stringify": "^1.1.2", + "stream-transform": "^0.2.2" + } + }, + "csv-generate": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-1.1.2.tgz", + "integrity": "sha1-7GsA7a7W5ZrZwgWC9MNk4osUYkA=" + }, + "csv-parse": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-1.3.3.tgz", + "integrity": "sha1-0c/YdDwvhJoKuy/VRNtWaV0ZpJA=" + }, + "csv-stringify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-1.1.2.tgz", + "integrity": "sha1-d6QVJlgbzjOA8SsA18W7rHDIK1g=", + "requires": { + "lodash.get": "~4.4.2" + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "requires": { + "array-find-index": "^1.0.1" + } + }, + "cyclist": { + "version": "1.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=" + }, + "dargs": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", + "integrity": "sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "http://bbnpm.azurewebsites.net/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "date-utils": { + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/date-utils/-/date-utils-1.2.21.tgz", + "integrity": "sha1-YfsWzcEnSzyayq/+n8ad+HIKK2Q=" + }, + "dateformat": { + "version": "3.0.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha1-puN0maTZqc+F71hyBE1ikByYia4=" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/debug/-/debug-3.1.0.tgz", + "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "requires": { + "ms": "2.0.0" + } + }, + "debuglog": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", + "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=" + }, + "decamelize": { + "version": "1.2.0", + "resolved": "http://bbnpm.azurewebsites.net/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" + } + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=" + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.0.tgz", + "integrity": "sha512-ZbfWJq/wN1Z273o7mUSjILYqehAktR2NVoSrOukDkU9kg2v/Uv89yU4Cvz8seJeAmtN5oqiefKq8FPuXOboqLw==", + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "http://bbnpm.azurewebsites.net/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "requires": { + "clone": "^1.0.2" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha1-z4jabL7ib+bbcJT2HYcMvYTO6fE=", + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha1-1Flono1lS6d+AqgX+HENcCyxbp0=", + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "http://bbnpm.azurewebsites.net/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "deprecation": { + "version": "2.3.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha1-Y2jL20Cr8zc7UlrIfkomDDpwCRk=" + }, + "detect-indent": { + "version": "5.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/detect-indent/-/detect-indent-5.0.0.tgz", + "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=" + }, + "detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==" + }, + "dezalgo": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", + "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "diagnostic-channel": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz", + "integrity": "sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc=", + "requires": { + "semver": "^5.3.0" + } + }, + "diagnostic-channel-publishers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.3.0.tgz", + "integrity": "sha512-tylBZM/ZJ+ismlyop3g9ejI/0+bR/3BTo06fcE4wxq6cJZOe6XMABgRUZ+QUs+0WSnuglxmJ8Wwamnl01tV+Gw==" + }, + "diff": { + "version": "3.5.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/diff/-/diff-3.5.0.tgz", + "integrity": "sha1-gAwN0eCov7yVg1wgKtIg/jF+WhI=" + }, + "dir-glob": { + "version": "2.2.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/dir-glob/-/dir-glob-2.2.2.tgz", + "integrity": "sha1-+gnwaUFTyJGLGLoN6vrpR2n8UMQ=", + "requires": { + "path-type": "^3.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha1-rd6+rXKmV023g2OdyHoSF3OXOWE=", + "requires": { + "esutils": "^2.0.2" + } + }, + "documentdb": { + "version": "1.14.5", + "resolved": "https://registry.npmjs.org/documentdb/-/documentdb-1.14.5.tgz", + "integrity": "sha512-0nDoQQiq5jzGIxOQF2y2bUOrFYehvk9pIrXy0dscXc3JsepNYhNVmjIsug5sgYPbt+XUYtMXpsfjzGCnYgNXgw==", + "requires": { + "big-integer": "^1.6.25", + "binary-search-bounds": "2.0.3", + "int64-buffer": "^0.1.9", + "priorityqueuejs": "1.0.0", + "semaphore": "1.0.5", + "tunnel": "0.0.5", + "underscore": "1.8.3" + }, + "dependencies": { + "semaphore": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.0.5.tgz", + "integrity": "sha1-tJJXbmavGT25XWXiXsU/Xxl5jWA=" + }, + "tunnel": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.5.tgz", + "integrity": "sha512-gj5sdqherx4VZKMcBA4vewER7zdK25Td+z1npBqpbDys4eJrLx+SlYjJvq1bDXs2irkuJM5pf8ktaEQVipkrbA==" + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + } + } + }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha1-HxngwuGqDjJ5fEl5nyg3rGr2nFc=", + "requires": { + "is-obj": "^1.0.0" + } + }, + "dotenv": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz", + "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==" + }, + "dtrace-provider": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", + "requires": { + "nan": "^2.14.0" + } + }, + "duplexer": { + "version": "0.1.1", + "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha1-Kk31MX9sz9kfhtb9JdjYoQO4gwk=", + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha1-rg8PothQRe8UqBfao86azQSJ5b8=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "emitter-listener": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", + "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==", + "requires": { + "shimmer": "^1.2.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha1-kzoEBShgyF6DwSJHnEdIqOTHIVY=" + }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "~0.4.13" + } + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha1-WuZKX0UFe682JuwU2gyl5LJDHrA=", + "requires": { + "once": "^1.4.0" + } + }, + "env-paths": { + "version": "1.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/env-paths/-/env-paths-1.0.0.tgz", + "integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=" + }, + "err-code": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz", + "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=" + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha1-tKxAZIEH/c3PriQvQovqihTU8b8=", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.16.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/es-abstract/-/es-abstract-1.16.0.tgz", + "integrity": "sha1-06JtycMoOsl1DcpWlYbpdtncwG0=", + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.0", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-inspect": "^1.6.0", + "object-keys": "^1.1.1", + "string.prototype.trimleft": "^2.1.0", + "string.prototype.trimright": "^2.1.0" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha1-7fckeAM0VujdqO8J4ArZZQcH83c=", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha1-TrIVlMlyvEBVPSduUQU5FD21Pgo=" + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "requires": { + "es6-promise": "^4.0.3" + } + }, + "escape-regexp-component": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/escape-regexp-component/-/escape-regexp-component-1.0.2.tgz", + "integrity": "sha1-nGO20LJf8qiMOtvRjFthrMO5+qI=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "http://bbnpm.azurewebsites.net/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escodegen": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.12.0.tgz", + "integrity": "sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg==", + "requires": { + "esprima": "^3.1.3", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + } + } + }, + "eslint": { + "version": "5.16.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/eslint/-/eslint-5.16.0.tgz", + "integrity": "sha1-oeOsGq5KP72Clvz496tzFMu2q+o=", + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.9.1", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^4.0.3", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^5.0.1", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^6.2.2", + "js-yaml": "^3.13.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.11", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^5.5.1", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/debug/-/debug-4.1.1.tgz", + "integrity": "sha1-O3ImAlUQnGtYnO4FDx1RYTlmR5E=", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ms/-/ms-2.1.2.tgz", + "integrity": "sha1-0J0fNXtEP0kzgqjrPM0YOHKuYAk=" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/semver/-/semver-5.7.1.tgz", + "integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=" + } + } + }, + "eslint-plugin-only-warn": { + "version": "1.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/eslint-plugin-only-warn/-/eslint-plugin-only-warn-1.0.1.tgz", + "integrity": "sha1-oB5hXDbzC6+Qn/VqsGBf/k6hijc=" + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha1-ygODMxD2iJoyZHgaqC5j65z+eEg=", + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha1-dP7HxU0Hdrb2fgJRBAtYBlZOmB8=", + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha1-4qgs6oT/JGrW+1f5veW0ZiFFnsI=" + }, + "espree": { + "version": "5.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/espree/-/espree-5.0.1.tgz", + "integrity": "sha1-XWUm+k/H8HiKXPdbFfMDI+L4H3o=", + "requires": { + "acorn": "^6.0.7", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha1-E7BM2z5sXRnfkatph6hpVhmwqnE=" + }, + "esquery": { + "version": "1.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha1-QGxRZYsfWZGl+bYrHcJbAOPlxwg=", + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha1-AHo7n9vCs7uH5IeeoZyS/b05Qs8=", + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha1-OYrT88WiSUi+dyXoPRGn3ijNvR0=" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha1-dNLrTeC42hKTcRkQ1Qd1ubcQ72Q=" + }, + "eventemitter3": { + "version": "3.1.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha1-LT1I+cNGaY/Og6hdfWZOmFNd9uc=" + }, + "ewma": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ewma/-/ewma-2.0.1.tgz", + "integrity": "sha512-MYYK17A76cuuyvkR7MnqLW4iFYPEi5Isl2qb8rXiWpLiwFS9dxW/rncuNnjjgSENuVqZQkIuR4+DChVL4g1lnw==", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/execa/-/execa-1.0.0.tgz", + "integrity": "sha1-xiNqW7TfbW8V6I5/AXeYIWdJ3dg=", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/debug/-/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/extend/-/extend-3.0.2.tgz", + "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha1-ywP3QL764D6k0oPK7SdBqD8zVJU=", + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha1-rQD+TcYSqSMuhxhxHcXLWrAoVUM=", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "http://bbnpm.azurewebsites.net/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-glob": { + "version": "2.2.7", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/fast-glob/-/fast-glob-2.2.7.tgz", + "integrity": "sha1-aVOFfDr6R1//ku5gFdUtpwpM050=", + "requires": { + "@mrmlnc/readdir-enhanced": "^2.2.1", + "@nodelib/fs.stat": "^1.1.2", + "glob-parent": "^3.1.0", + "is-glob": "^4.0.0", + "merge2": "^1.2.3", + "micromatch": "^3.1.10" + }, + "dependencies": { + "glob-parent": { + "version": "3.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + } + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "http://bbnpm.azurewebsites.net/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "http://bbnpm.azurewebsites.net/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==" + }, + "figgy-pudding": { + "version": "3.5.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/figgy-pudding/-/figgy-pudding-3.5.1.tgz", + "integrity": "sha1-hiRwESkBxyeg5JWoB0S9W6odZ5A=" + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha1-yg9u+m3T1WEzP7FFFQZcL6/fQ5w=", + "requires": { + "flat-cache": "^2.0.1" + } + }, + "filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=" + }, + "filenamify": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.1.0.tgz", + "integrity": "sha512-KQV/uJDI9VQgN7sHH1Zbk6+42cD6mnQ2HONzkXUfPJ+K2FC8GZ1dpewbbHw0Sz8Tf5k3EVdHVayM4DoAwWlmtg==", + "requires": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "find-my-way": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-1.18.1.tgz", + "integrity": "sha512-5M9oQuUPNDxr7w7g65Rv2acToLUIjVUbnMsltXNQaSYWOwjf+2MBp7sMuY+pfO+OPCo2qwcxsr29VQQ09ouVMg==", + "requires": { + "fast-decode-uri-component": "^1.0.0", + "safe-regex": "^1.1.0", + "semver-store": "^0.3.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + }, + "flat": { + "version": "4.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/flat/-/flat-4.1.0.tgz", + "integrity": "sha1-CQvsiwXjnLowl0fx1YjwTbr5jbI=", + "requires": { + "is-buffer": "~2.0.3" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha1-XSltbwS9pEpGMKMBQTvbwuwIXsA=", + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/flatted/-/flatted-2.0.1.tgz", + "integrity": "sha1-aeV8qo8OrLwoHS4stFjUb9tEngg=" + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha1-jdfYc6G6vCB9lOrQwuDkQnbr8ug=", + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha1-e3qfmuov3/NnhqlP9kPtB/T/Xio=", + "requires": { + "debug": "=3.1.0" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "http://bbnpm.azurewebsites.net/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.5.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha1-8svsV7XlniNxbhKP5E1OXdI4lfQ=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==" + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "requires": { + "map-cache": "^0.2.2" + } + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha1-zP+FcIQef+QmVpPaiJNsVa7X98c=", + "requires": { + "minipass": "^2.6.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "http://bbnpm.azurewebsites.net/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fstream": { + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-0.1.31.tgz", + "integrity": "sha1-czfwWPu7vvqMn1YaKMqwhJICyYg=", + "requires": { + "graceful-fs": "~3.0.2", + "inherits": "~2.0.0", + "mkdirp": "0.5", + "rimraf": "2" + }, + "dependencies": { + "graceful-fs": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.12.tgz", + "integrity": "sha512-J55gaCS4iTTJfTXIxSVw3EMQckcqkpdRv3IR7gu6sq0+tbC363Zx6KH/SEwXASK9JRbhyZmVjJEVJIOxYsB3Qg==", + "requires": { + "natives": "^1.1.3" + } + } + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=" + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "genfun": { + "version": "5.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/genfun/-/genfun-5.0.0.tgz", + "integrity": "sha1-ndlxCgaQClxKW/V6yl2k5S/nZTc=" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha1-T5RBKoLbMvNuOwuXQfipf+sDH34=" + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" + }, + "get-pkg-repo": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz", + "integrity": "sha1-xztInAbYDMVTbCyFP54FIyBWly0=", + "requires": { + "hosted-git-info": "^2.1.4", + "meow": "^3.3.0", + "normalize-package-data": "^2.3.0", + "parse-github-repo-url": "^1.3.0", + "through2": "^2.0.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "requires": { + "repeating": "^2.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "requires": { + "get-stdin": "^4.0.1" + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" + } + } + }, + "get-port": { + "version": "4.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/get-port/-/get-port-4.2.0.tgz", + "integrity": "sha1-43Nosehjt2KcQ8WjI2Jflc8ksRk=" + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha1-wbJVV189wh1Zv8ec09K0axw6VLU=", + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + }, + "getpass": { + "version": "0.1.7", + "resolved": "http://bbnpm.azurewebsites.net/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "git-raw-commits": { + "version": "2.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/git-raw-commits/-/git-raw-commits-2.0.0.tgz", + "integrity": "sha1-2Srd90RAwUvMXIPszj+3+KeRGLU=", + "requires": { + "dargs": "^4.0.1", + "lodash.template": "^4.0.2", + "meow": "^4.0.0", + "split2": "^2.0.0", + "through2": "^2.0.0" + } + }, + "git-remote-origin-url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", + "integrity": "sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=", + "requires": { + "gitconfiglocal": "^1.0.0", + "pify": "^2.3.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + } + } + }, + "git-semver-tags": { + "version": "2.0.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/git-semver-tags/-/git-semver-tags-2.0.3.tgz", + "integrity": "sha1-SJiKcYrPWTgA+ZYiqVKnfEBb+jQ=", + "requires": { + "meow": "^4.0.0", + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/semver/-/semver-6.3.0.tgz", + "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=" + } + } + }, + "git-up": { + "version": "4.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/git-up/-/git-up-4.0.1.tgz", + "integrity": "sha1-yy7whmU2QOch0gQv4xBIV9iQB8A=", + "requires": { + "is-ssh": "^1.3.0", + "parse-url": "^5.0.0" + } + }, + "git-url-parse": { + "version": "11.1.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/git-url-parse/-/git-url-parse-11.1.2.tgz", + "integrity": "sha1-r/Gol8NsyTaZJwWHvqPby7uV3mc=", + "requires": { + "git-up": "^4.0.0" + } + }, + "gitconfiglocal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", + "integrity": "sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=", + "requires": { + "ini": "^1.3.2" + } + }, + "glob": { + "version": "7.1.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/glob/-/glob-7.1.5.tgz", + "integrity": "sha1-ZxTGm+4g88PmTE3ZBVU+UytAzcA=", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/glob-parent/-/glob-parent-5.1.0.tgz", + "integrity": "sha1-X0wdHnSNMM1zrSlEs1d6gbCB6MI=", + "requires": { + "is-glob": "^4.0.1" + } + }, + "glob-to-regexp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", + "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=" + }, + "globalize": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/globalize/-/globalize-1.4.2.tgz", + "integrity": "sha512-IfKeYI5mAITBmT5EnH8kSQB5uGson4Fkj2XtTpyEbIS7IHNfLHoeTyLJ6tfjiKC6cJXng3IhVurDk5C7ORqFhQ==", + "requires": { + "cldrjs": "^0.5.0" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/globals/-/globals-11.12.0.tgz", + "integrity": "sha1-q4eVM4hooLq9hSV1gBjCp+uVxC4=" + }, + "globby": { + "version": "9.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/globby/-/globby-9.2.0.tgz", + "integrity": "sha1-/QKacGxwPSm90XD0tts6P3p8tj0=", + "requires": { + "@types/glob": "^7.1.1", + "array-union": "^1.0.2", + "dir-glob": "^2.2.2", + "fast-glob": "^2.2.6", + "glob": "^7.1.3", + "ignore": "^4.0.3", + "pify": "^4.0.1", + "slash": "^2.0.0" + } + }, + "got": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz", + "integrity": "sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==", + "requires": { + "@sindresorhus/is": "^0.7.0", + "cacheable-request": "^2.1.1", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "into-stream": "^3.1.0", + "is-retry-allowed": "^1.1.0", + "isurl": "^1.0.0-alpha5", + "lowercase-keys": "^1.0.0", + "mimic-response": "^1.0.0", + "p-cancelable": "^0.4.0", + "p-timeout": "^2.0.1", + "pify": "^3.0.0", + "safe-buffer": "^5.1.1", + "timed-out": "^4.0.1", + "url-parse-lax": "^3.0.0", + "url-to-options": "^1.0.1" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + } + } + }, + "graceful-fs": { + "version": "4.2.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha1-ShL/G2A3bvCYYsIJPt2Qgyi+hCM=" + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" + }, + "growl": { + "version": "1.10.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/growl/-/growl-1.10.5.tgz", + "integrity": "sha1-8nNdwig2dPpnR4sQGBBZNVw2nl4=" + }, + "handle-thing": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz", + "integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=" + }, + "handlebars": { + "version": "4.5.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/handlebars/-/handlebars-4.5.1.tgz", + "integrity": "sha1-igHDgsGAJyJg0H8tGqOudFcVx7o=", + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=" + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "http://bbnpm.azurewebsites.net/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha1-HvievT5JllV2de7ZiTEQ3DUPoIA=", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/has/-/has-1.0.3.tgz", + "integrity": "sha1-ci18v8H2qoJB8W3YFOAR4fQeh5Y=", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "http://bbnpm.azurewebsites.net/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-symbol-support-x": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", + "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==" + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" + }, + "has-to-string-tag-x": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", + "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", + "requires": { + "has-symbol-support-x": "^1.4.1" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=" + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/he/-/he-1.2.0.tgz", + "integrity": "sha1-hK5l+n6vsWX922FWauFLrwVmTw8=" + }, + "highlight.js": { + "version": "9.16.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/highlight.js/-/highlight.js-9.16.2.tgz", + "integrity": "sha1-aDaNA5/+HGIRvMB+SD2vld4+QD4=" + }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "hosted-git-info": { + "version": "2.8.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/hosted-git-info/-/hosted-git-info-2.8.5.tgz", + "integrity": "sha1-dZz88sTRVq3lmwst+r3cQqa5xww=" + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "http-cache-semantics": { + "version": "3.8.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", + "integrity": "sha1-ObDhat2bYFvwqe89nar0hDtMrNI=" + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=" + }, + "http-proxy-agent": { + "version": "2.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", + "integrity": "sha1-5IIb7vWyFCogJr1zkm/lN2McVAU=", + "requires": { + "agent-base": "4", + "debug": "3.1.0" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "http://bbnpm.azurewebsites.net/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha1-TuenN6vZJniik9mzShr00NCMeHs=", + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "requires": { + "ms": "^2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha1-ICK0sl+93CHS9SSXSkdKr+czkIs=", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha1-dQ49tYYgh7RzfrrIIH/9HvJ7Jfw=" + }, + "ignore-walk": { + "version": "3.0.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha1-AX4kRxhL/q3nwjjkrv3R6PlbHjc=", + "requires": { + "minimatch": "^3.0.4" + } + }, + "import-fresh": { + "version": "3.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/import-fresh/-/import-fresh-3.1.0.tgz", + "integrity": "sha1-bTP6Hc7235MPrgA0RvM0Fa+QURg=", + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha1-VQcL44pZk88Y72236WH1vuXFoJ0=", + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=" + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha1-xM78qo5RBRwqQLos6KPScpWvlGc=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "http://bbnpm.azurewebsites.net/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ini/-/ini-1.3.5.tgz", + "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc=" + }, + "init-package-json": { + "version": "1.10.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/init-package-json/-/init-package-json-1.10.3.tgz", + "integrity": "sha1-Rf/i9hCoyhNPK9HbVjeyNQcPbL4=", + "requires": { + "glob": "^7.1.1", + "npm-package-arg": "^4.0.0 || ^5.0.0 || ^6.0.0", + "promzard": "^0.3.0", + "read": "~1.0.1", + "read-package-json": "1 || 2", + "semver": "2.x || 3.x || 4 || 5", + "validate-npm-package-license": "^3.0.1", + "validate-npm-package-name": "^3.0.0" + } + }, + "inquirer": { + "version": "6.5.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha1-rVCUI3XQNtMn/1KMCL1fqwiZKMo=", + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc=" + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "int64-buffer": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-0.1.10.tgz", + "integrity": "sha1-J3siiofZWtd30HwTgyAiQGpHNCM=" + }, + "intercept-stdout": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/intercept-stdout/-/intercept-stdout-0.1.2.tgz", + "integrity": "sha1-Emq/H65sUJpCipjGGmMVWQQq6f0=", + "requires": { + "lodash.toarray": "^3.0.0" + } + }, + "interpret": { + "version": "1.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/interpret/-/interpret-1.2.0.tgz", + "integrity": "sha1-1QYaYiS+WOgIOYX1AU2EQ1lXYpY=" + }, + "into-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", + "integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=", + "requires": { + "from2": "^2.1.1", + "p-is-promise": "^1.1.0" + } + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=" + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-buffer": { + "version": "2.0.4", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha1-PlcvI8hBGlz9lVfISeNmXgspBiM=" + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha1-HhrfIZ4e62hNaR+dagX/DTCiTXU=" + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha1-a8YzQYGBDgS1wis9WJ/cpVAmQEw=", + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=" + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha1-cpyR4thXt6QZofmqZWhcTDP1hF0=" + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=" + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "http://bbnpm.azurewebsites.net/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha1-dWfb6fL14kZ7x3q4PEopSCQHpdw=", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=" + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + }, + "is-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", + "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=" + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=", + "requires": { + "isobject": "^3.0.1" + } + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "requires": { + "has": "^1.0.1" + } + }, + "is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==" + }, + "is-ssh": { + "version": "1.3.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-ssh/-/is-ssh-1.3.1.tgz", + "integrity": "sha1-80moyt0k5lKYA3pSLPdSDy6BoPM=", + "requires": { + "protocols": "^1.1.0" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "http://bbnpm.azurewebsites.net/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha1-oFX2rlcZLK7jKeeoYBGLSXqVDzg=", + "requires": { + "has-symbols": "^1.0.0" + } + }, + "is-text-path": { + "version": "2.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-text-path/-/is-text-path-2.0.0.tgz", + "integrity": "sha1-skhOK3IKYz/rLoW2fcGT/3LHVjY=", + "requires": { + "text-extensions": "^2.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "http://bbnpm.azurewebsites.net/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha1-0YUOuXkezRjmGCzhKjDzlmNLsZ0=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "http://bbnpm.azurewebsites.net/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "http://bbnpm.azurewebsites.net/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "isurl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", + "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", + "requires": { + "has-to-string-tag-x": "^1.2.0", + "is-object": "^1.0.1" + } + }, + "jquery": { + "version": "3.4.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/jquery/-/jquery-3.4.1.tgz", + "integrity": "sha1-cU8fjZ3eS9+lV2S6N+8hRjDYDvI=" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha1-GSA/tZmR35jjoocFDUZHzerzJJk=" + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha1-r/FRswv9+o5J4F2iLnQV6d+jeEc=", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "http://bbnpm.azurewebsites.net/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "jschardet": { + "version": "1.6.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/jschardet/-/jschardet-1.6.0.tgz", + "integrity": "sha1-x9GnHtz/KDnbL57DD8XV69PBpng=" + }, + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + }, + "json-edm-parser": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/json-edm-parser/-/json-edm-parser-0.1.2.tgz", + "integrity": "sha1-HmCw/vG8CvZ7wNFG393lSGzWFbQ=", + "requires": { + "jsonparse": "~1.2.0" + }, + "dependencies": { + "jsonparse": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.2.0.tgz", + "integrity": "sha1-XAxWhRBxYOcv50ib3eoLRMK8Z70=" + } + } + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha1-u4Z8+zRQ5pEHwTHRxRS6s9yLyqk=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "http://bbnpm.azurewebsites.net/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "http://bbnpm.azurewebsites.net/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "http://bbnpm.azurewebsites.net/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" + }, + "jsonpath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.0.2.tgz", + "integrity": "sha512-rmzlgFZiQPc6q4HDyK8s9Qb4oxBnI5sF61y/Co5PV0lc3q2bIuRsNdueVbhoSHdKM4fxeimphOAtfz47yjCfeA==", + "requires": { + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.7.0" + }, + "dependencies": { + "esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha1-dqD9Zvz+FU/SkmZ9wmQBl1CxZXs=" + }, + "underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=" + } + } + }, + "jsonwebtoken": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.0.1.tgz", + "integrity": "sha1-UNrvjQqMfeLNBrwQE7dbBMzz8M8=", + "requires": { + "jws": "^3.1.4", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.0.0", + "xtend": "^4.0.1" + } + }, + "jspath": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/jspath/-/jspath-0.4.0.tgz", + "integrity": "sha512-2/R8wkot8NCXrppBT/onp+4mcAUAZqtPxsW6aSJU3hrFAVqKqtFYcat2XJZ7inN4RtATUxfv0UQSYOmvJKiIGA==" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "http://bbnpm.azurewebsites.net/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "just-extend": { + "version": "4.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha1-8/R/ffyg+YnFVBCn68iFSwcQivw=" + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha1-dDwymFy56YZVUw1TZBtmyGRbA5o=", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/jws/-/jws-3.2.2.tgz", + "integrity": "sha1-ABCZ82OUaMlBQADpmZX6UvtHgwQ=", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "keyv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", + "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", + "requires": { + "json-buffer": "3.0.0" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE=" + }, + "latest-version": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-4.0.0.tgz", + "integrity": "sha512-b4Myk7aQiQJvgssw2O8yITjELdqKRX4JQJUF1IUplgLaA8unv7s+UsAOwH6Q0/a09czSvlxEm306it2LBXrCzg==", + "requires": { + "package-json": "^5.0.0" + } + }, + "lcov-parse": { + "version": "0.0.10", + "resolved": "http://bbnpm.azurewebsites.net/lcov-parse/-/lcov-parse-0.0.10.tgz", + "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=" + }, + "lerna": { + "version": "3.18.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/lerna/-/lerna-3.18.3.tgz", + "integrity": "sha1-yUVW52+Y35x65O07wBZhF8xCzRM=", + "requires": { + "@lerna/add": "3.18.0", + "@lerna/bootstrap": "3.18.0", + "@lerna/changed": "3.18.3", + "@lerna/clean": "3.18.0", + "@lerna/cli": "3.18.0", + "@lerna/create": "3.18.0", + "@lerna/diff": "3.18.0", + "@lerna/exec": "3.18.0", + "@lerna/import": "3.18.0", + "@lerna/init": "3.18.0", + "@lerna/link": "3.18.0", + "@lerna/list": "3.18.0", + "@lerna/publish": "3.18.3", + "@lerna/run": "3.18.0", + "@lerna/version": "3.18.3", + "import-local": "^2.0.0", + "npmlog": "^4.1.2" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "http://bbnpm.azurewebsites.net/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "load-json-file": { + "version": "5.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/load-json-file/-/load-json-file-5.3.0.tgz", + "integrity": "sha1-TTweAfocA+p4pgrHr5MsnOU0A/M=", + "requires": { + "graceful-fs": "^4.1.15", + "parse-json": "^4.0.0", + "pify": "^4.0.1", + "strip-bom": "^3.0.0", + "type-fest": "^0.3.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha1-tEf2ZwoEVbv+7dETku/zMOoJdUg=" + }, + "lodash-compat": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/lodash-compat/-/lodash-compat-3.10.2.tgz", + "integrity": "sha1-xpQBKKnTD46QLNLPmf0Muk7PwYM=" + }, + "lodash._arraycopy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz", + "integrity": "sha1-due3wfH7klRzdIeKVi7Qaj5Q9uE=" + }, + "lodash._basevalues": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz", + "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=" + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + }, + "lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "http://bbnpm.azurewebsites.net/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=" + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.ismatch": { + "version": "4.4.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", + "integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "requires": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "lodash.last": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash.last/-/lodash.last-3.0.0.tgz", + "integrity": "sha1-JC9mMRLdTG5jcoxgo8kJ0b2tvUw=" + }, + "lodash.max": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.max/-/lodash.max-4.0.1.tgz", + "integrity": "sha1-hzVWbGGLNan3YFILSHrnllivE2o=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, + "lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=" + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-tools-daily/npm/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + }, + "lodash.template": { + "version": "4.5.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha1-+XYZXPPzR9DV9SSDVp/oAxzM6Ks=", + "requires": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha1-5IExDwSdPPbUfpEq0JMTsVTw+zM=", + "requires": { + "lodash._reinterpolate": "^3.0.0" + } + }, + "lodash.toarray": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-3.0.2.tgz", + "integrity": "sha1-KyBPD6T1HChcbwDIHRzqWiMEEXk=", + "requires": { + "lodash._arraycopy": "^3.0.0", + "lodash._basevalues": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "lodash.tonumber": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/lodash.tonumber/-/lodash.tonumber-4.0.3.tgz", + "integrity": "sha1-C5azGzVnJ5Prf1pj7nkfG56QJdk=" + }, + "lodash.trimend": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/lodash.trimend/-/lodash.trimend-4.5.1.tgz", + "integrity": "sha1-EoBENyhrmMrYmWt5QU4RMAEUCC8=" + }, + "lodash.unescape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", + "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=" + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + }, + "log-driver": { + "version": "1.2.7", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha1-Y7lQIfBwL+36LJuwok53l9cYcdg=" + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha1-V0Dhxdbw39pK2TI7UzIQfva0xAo=", + "requires": { + "chalk": "^2.0.1" + } + }, + "lolex": { + "version": "4.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/lolex/-/lolex-4.2.0.tgz", + "integrity": "sha1-3b1/YhPKHqWCaQGrEiK2XXFLPNc=" + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha1-HaJ+ZxAnGUdpXa9oSOhH8B2EuSA=", + "requires": { + "yallist": "^3.0.2" + } + }, + "lunr": { + "version": "2.3.8", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/lunr/-/lunr-2.3.8.tgz", + "integrity": "sha1-qLicMfMLWgRLl9LSji2hkba6IHI=" + }, + "macos-release": { + "version": "2.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/macos-release/-/macos-release-2.3.0.tgz", + "integrity": "sha1-6xkwsDbAgArevM1fF7xMEt6Ltx8=" + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha1-ecEDO4BRW9bSTsmTPoYMp17ifww=", + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + } + } + }, + "make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==" + }, + "make-fetch-happen": { + "version": "5.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/make-fetch-happen/-/make-fetch-happen-5.0.1.tgz", + "integrity": "sha1-+sZUAKtfepwAGGKj6bD0F/CEAXU=", + "requires": { + "agentkeepalive": "^3.4.1", + "cacache": "^12.0.0", + "http-cache-semantics": "^3.8.1", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^2.2.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "node-fetch-npm": "^2.0.2", + "promise-retry": "^1.1.1", + "socks-proxy-agent": "^4.0.0", + "ssri": "^6.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + }, + "map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=" + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "requires": { + "object-visit": "^1.0.0" + } + }, + "marked": { + "version": "0.7.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/marked/-/marked-0.7.0.tgz", + "integrity": "sha1-tkIB8FHScbHtwQoE0a6bdLuOXA4=" + }, + "match-stream": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/match-stream/-/match-stream-0.0.2.tgz", + "integrity": "sha1-mesFAJOzTf+t5CG5rAtBCpz6F88=", + "requires": { + "buffers": "~0.1.1", + "readable-stream": "~1.0.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "meow": { + "version": "4.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/meow/-/meow-4.0.1.tgz", + "integrity": "sha1-1IWY9vSxRy81v2MXqVlFrONH+XU=", + "requires": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist": "^1.1.3", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0" + } + }, + "merge2": { + "version": "1.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/merge2/-/merge2-1.3.0.tgz", + "integrity": "sha1-WzZu6DsvFYLEj4fkfPGpNSEDyoE=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha1-cIWbyVyYQJUvNZoGij/En57PrCM=", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha1-plBX6ZjbCQ9zKmj2wnbTh9QSbDI=" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha1-tvjQs+lR77d97eyhlM/20W9nb4E=", + "requires": { + "mime-db": "1.40.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha1-ggyGo5M0ZA6ZUWkovQP8qIBX0CI=" + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "minimist-options": { + "version": "3.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/minimist-options/-/minimist-options-3.0.2.tgz", + "integrity": "sha1-+6TIGRM54T7PTWG+sD8HAQPz2VQ=", + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0" + } + }, + "minipass": { + "version": "2.9.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha1-5xN2Ln0+Mv7YAxFc+T4EvKn8yaY=", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha1-IpDeloGKNMKVUcio0wEha9Zahh0=", + "requires": { + "minipass": "^2.9.0" + } + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha1-6goykfl+C16HdrNj1fChLZTGcCI=", + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha1-ESC0PcNZp4Xc5ltVuC4lfM9HlWY=", + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://bbnpm.azurewebsites.net/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } + }, + "mkdirp-promise": { + "version": "5.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/mkdirp-promise/-/mkdirp-promise-5.0.1.tgz", + "integrity": "sha1-6bj2jlUsaKnBcTuEiD96HdA5uKE=", + "requires": { + "mkdirp": "*" + } + }, + "mocha": { + "version": "6.2.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/mocha/-/mocha-6.2.2.tgz", + "integrity": "sha1-XYmH4olAyviVen12ZLkQ3Fsv6iA=", + "requires": { + "ansi-colors": "3.2.3", + "browser-stdout": "1.3.1", + "debug": "3.2.6", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "find-up": "3.0.0", + "glob": "7.1.3", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "2.2.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "ms": "2.1.1", + "node-environment-flags": "1.0.5", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.3.0", + "yargs-parser": "13.1.1", + "yargs-unparser": "1.6.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc=" + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha1-48mzFWnhBoEd8kL3FXJaH0xJQyA=" + }, + "debug": { + "version": "3.2.6", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/debug/-/debug-3.2.6.tgz", + "integrity": "sha1-6D0X3hbYp++3cX7b5fsQE17uYps=", + "requires": { + "ms": "^2.1.1" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha1-SRafHXmTQwZG2mHsxa41XCHJe3M=", + "requires": { + "locate-path": "^3.0.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/glob/-/glob-7.1.3.tgz", + "integrity": "sha1-OWCDLT8VdBCDQtr9OmezMsCWnfE=", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha1-2+w7OrdZdYBxtY/ln8QYca8hQA4=", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ms/-/ms-2.1.1.tgz", + "integrity": "sha1-MKWGTrPrsKZvLr5tcnrwagnYbgo=" + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha1-qgeniMwxUck5tRMfY1cPDdIAlTc=", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha1-Mi1poFwCZLJZl9n0DNiokasAZKQ=", + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha1-yyhoVA4xPWHeWPr741zpAE1VQOY=" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha1-InZ74htirxCBV0MG9prFG2IgOWE=", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "6.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha1-ds/nQs8fQbubHCmtAwaMBbTA5Ao=", + "requires": { + "has-flag": "^3.0.0" + } + }, + "yargs": { + "version": "13.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha1-TGV6VeB+Xyz5R/ijZlZ8BKDe3IM=", + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha1-0mBYUyqgbTZf4JH2ofwGsvfl7KA=", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "mocha-logger": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/mocha-logger/-/mocha-logger-1.0.6.tgz", + "integrity": "sha512-D7Z3r1RkyaJOnlgokODdzt9p4ut0m3DVzEKp3r3tgeXIpdxp54z049Vc0EEh5hkhudfRN0dfUD10Fcj2WuOO3w==", + "requires": { + "mocha": "^5.1.1" + }, + "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=" + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "mock-require": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz", + "integrity": "sha512-lLzfLHcyc10MKQnNUCv7dMcoY/2Qxd6wJfbqCcVk3LDb8An4hF6ohk5AztrvgKhJCqj36uyzi/p5se+tvyD+Wg==", + "requires": { + "get-caller-file": "^1.0.2", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" + } + } + }, + "modify-values": { + "version": "1.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/modify-values/-/modify-values-1.0.1.tgz", + "integrity": "sha1-s5OfpgVUZHTj4+PGPWS9Q7TuYCI=" + }, + "moment": { + "version": "2.24.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/moment/-/moment-2.24.0.tgz", + "integrity": "sha1-DQVdU/UFKqZTyfbraLtdEr9cK1s=" + }, + "moment-timezone": { + "version": "0.5.27", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.27.tgz", + "integrity": "sha512-EIKQs7h5sAsjhPCqN6ggx6cEbs94GK050254TIJySD1bzoM5JTYDwAU1IoVOeTOL6Gm27kYJ51/uuvq1kIlrbw==", + "requires": { + "moment": ">= 2.9.0" + } + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "ms-rest": { + "version": "2.5.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ms-rest/-/ms-rest-2.5.3.tgz", + "integrity": "sha1-rjb8RsHGCpBRHS8pJG/g9bVSl7M=", + "requires": { + "duplexer": "^0.1.1", + "is-buffer": "^1.1.6", + "is-stream": "^1.1.0", + "moment": "^2.21.0", + "request": "^2.88.0", + "through": "^2.3.8", + "tunnel": "0.0.5", + "uuid": "^3.2.1" + }, + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=" + }, + "tunnel": { + "version": "0.0.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/tunnel/-/tunnel-0.0.5.tgz", + "integrity": "sha1-0VMiVHSe02Yg/NEBCGVJWh+p0K4=" + } + } + }, + "ms-rest-azure": { + "version": "2.6.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ms-rest-azure/-/ms-rest-azure-2.6.0.tgz", + "integrity": "sha1-IJjv7FKe7PoMbiFbaRQ6vKuhIUA=", + "requires": { + "adal-node": "^0.1.28", + "async": "2.6.0", + "moment": "^2.22.2", + "ms-rest": "^2.3.2", + "request": "^2.88.0", + "uuid": "^3.2.1" + } + }, + "multimatch": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/multimatch/-/multimatch-3.0.0.tgz", + "integrity": "sha1-DiU0zGvCONmrZ+G5zV/Nhabb9ws=", + "requires": { + "array-differ": "^2.0.3", + "array-union": "^1.0.2", + "arrify": "^1.0.1", + "minimatch": "^3.0.4" + } + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" + }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "optional": true, + "requires": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "optional": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "optional": true, + "requires": { + "glob": "^6.0.1" + } + } + } + }, + "mz": { + "version": "2.7.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/mz/-/mz-2.7.0.tgz", + "integrity": "sha1-lQCAV6Vsr63CvGPd5/n/aVWUjjI=", + "requires": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha1-uHqKpPwN6P5r6IiVs4mD/yZb0Rk=", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natives": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.6.tgz", + "integrity": "sha512-6+TDFewD4yxY14ptjKaS63GVdtKiES1pTPyxn9Jb0rBqPMZ7VcCiooEhPNsr+mqHtMGxa/5c/HhcC4uPEUw/nA==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" + }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "optional": true + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha1-rCetpmFn+ohJpq3dg39rGJrSCBw=" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha1-ozeKdpbOfSI+iPybdkvX7xCJ42Y=" + }, + "nise": { + "version": "1.5.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/nise/-/nise-1.5.2.tgz", + "integrity": "sha1-ttKa8Q5IsyGzB+EOBlGZM47rJlI=", + "requires": { + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^4.1.0", + "path-to-regexp": "^1.7.0" + } + }, + "nock": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz", + "integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==", + "requires": { + "chai": "^4.1.2", + "debug": "^4.1.0", + "deep-equal": "^1.0.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.5", + "mkdirp": "^0.5.0", + "propagate": "^1.0.0", + "qs": "^6.5.1", + "semver": "^5.5.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "node-abort-controller": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-1.0.4.tgz", + "integrity": "sha512-7cNtLKTAg0LrW3ViS2C7UfIzbL3rZd8L0++5MidbKqQVJ8yrH6+1VRSHl33P0ZjBTbOJd37d9EYekvHyKkB0QQ==" + }, + "node-environment-flags": { + "version": "1.0.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/node-environment-flags/-/node-environment-flags-1.0.5.tgz", + "integrity": "sha1-+pMCdfW/Xa4YjWGSsktMi7rD12o=", + "requires": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/semver/-/semver-5.7.1.tgz", + "integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=" + } + } + }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha1-5jNFY4bUqlWGP2dqerDaqP3ssP0=" + }, + "node-fetch-npm": { + "version": "2.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz", + "integrity": "sha1-cljJBGGC3KNFtCCO2pGNrzNpf/c=", + "requires": { + "encoding": "^0.1.11", + "json-parse-better-errors": "^1.0.0", + "safe-buffer": "^5.1.1" + } + }, + "node-gyp": { + "version": "5.0.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/node-gyp/-/node-gyp-5.0.5.tgz", + "integrity": "sha1-9s8dokbrjEKwl9fNTWw84jpBY68=", + "requires": { + "env-paths": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^4.4.12", + "which": "1" + }, + "dependencies": { + "semver": { + "version": "5.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" + } + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha1-5m2xg4sgDB38IzIl0SyzZSDiNKg=", + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "normalize-url": { + "version": "3.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/normalize-url/-/normalize-url-3.3.0.tgz", + "integrity": "sha1-suHE3E98bVd0PfczpPWXjRhlBVk=" + }, + "npm-bundled": { + "version": "1.0.6", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/npm-bundled/-/npm-bundled-1.0.6.tgz", + "integrity": "sha1-57qarc75YrthJI+RchzZMrP+a90=" + }, + "npm-lifecycle": { + "version": "3.1.4", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/npm-lifecycle/-/npm-lifecycle-3.1.4.tgz", + "integrity": "sha1-3ml1x9jfZfUVDbEQtXzOSYsLYEw=", + "requires": { + "byline": "^5.0.0", + "graceful-fs": "^4.1.15", + "node-gyp": "^5.0.2", + "resolve-from": "^4.0.0", + "slide": "^1.1.6", + "uid-number": "0.0.6", + "umask": "^1.1.0", + "which": "^1.3.1" + } + }, + "npm-package-arg": { + "version": "6.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/npm-package-arg/-/npm-package-arg-6.1.1.tgz", + "integrity": "sha1-AhaMsKSaK3W/mIooaY3ntSnfXLc=", + "requires": { + "hosted-git-info": "^2.7.1", + "osenv": "^0.1.5", + "semver": "^5.6.0", + "validate-npm-package-name": "^3.0.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/semver/-/semver-5.7.1.tgz", + "integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=" + } + } + }, + "npm-packlist": { + "version": "1.4.6", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/npm-packlist/-/npm-packlist-1.4.6.tgz", + "integrity": "sha1-U7o+0R+FIwefFFc3bdN57k6kL/Q=", + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npm-pick-manifest": { + "version": "3.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/npm-pick-manifest/-/npm-pick-manifest-3.0.2.tgz", + "integrity": "sha1-9Nnl/UviFT5fTl+be+jcQZqZq7c=", + "requires": { + "figgy-pudding": "^3.5.1", + "npm-package-arg": "^6.0.0", + "semver": "^5.4.1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "http://bbnpm.azurewebsites.net/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "http://bbnpm.azurewebsites.net/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "nyc": { + "version": "11.9.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-11.9.0.tgz", + "integrity": "sha512-w8OdJAhXL5izerzZMdqzYKMj/pgHJyY3qEPYBjLLxrhcVoHEY9pU5ENIiZyCgG9OR7x3VcUMoD40o6PtVpfR4g==", + "requires": { + "archy": "^1.0.0", + "arrify": "^1.0.1", + "caching-transform": "^1.0.0", + "convert-source-map": "^1.5.1", + "debug-log": "^1.0.1", + "default-require-extensions": "^1.0.0", + "find-cache-dir": "^0.1.1", + "find-up": "^2.1.0", + "foreground-child": "^1.5.3", + "glob": "^7.0.6", + "istanbul-lib-coverage": "^1.1.2", + "istanbul-lib-hook": "^1.1.0", + "istanbul-lib-instrument": "^1.10.0", + "istanbul-lib-report": "^1.1.3", + "istanbul-lib-source-maps": "^1.2.3", + "istanbul-reports": "^1.4.0", + "md5-hex": "^1.2.0", + "merge-source-map": "^1.1.0", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.0", + "resolve-from": "^2.0.0", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.1", + "spawn-wrap": "^1.4.2", + "test-exclude": "^4.2.0", + "yargs": "11.1.0", + "yargs-parser": "^8.0.0" + }, + "dependencies": { + "align-text": { + "version": "0.1.4", + "bundled": true, + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "amdefine": { + "version": "1.0.1", + "bundled": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "ansi-styles": { + "version": "2.2.1", + "bundled": true + }, + "append-transform": { + "version": "0.4.0", + "bundled": true, + "requires": { + "default-require-extensions": "^1.0.0" + } + }, + "archy": { + "version": "1.0.0", + "bundled": true + }, + "arr-diff": { + "version": "4.0.0", + "bundled": true + }, + "arr-flatten": { + "version": "1.1.0", + "bundled": true + }, + "arr-union": { + "version": "3.1.0", + "bundled": true + }, + "array-unique": { + "version": "0.3.2", + "bundled": true + }, + "arrify": { + "version": "1.0.1", + "bundled": true + }, + "assign-symbols": { + "version": "1.0.0", + "bundled": true + }, + "async": { + "version": "1.5.2", + "bundled": true + }, + "atob": { + "version": "2.1.1", + "bundled": true + }, + "babel-code-frame": { + "version": "6.26.0", + "bundled": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "babel-generator": { + "version": "6.26.1", + "bundled": true, + "requires": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "bundled": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-runtime": { + "version": "6.26.0", + "bundled": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "babel-template": { + "version": "6.26.0", + "bundled": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "bundled": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "bundled": true, + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "bundled": true + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "base": { + "version": "0.11.2", + "bundled": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "bundled": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "bundled": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "builtin-modules": { + "version": "1.1.1", + "bundled": true + }, + "cache-base": { + "version": "1.0.1", + "bundled": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true + } + } + }, + "caching-transform": { + "version": "1.0.1", + "bundled": true, + "requires": { + "md5-hex": "^1.2.0", + "mkdirp": "^0.5.1", + "write-file-atomic": "^1.1.4" + } + }, + "camelcase": { + "version": "1.2.1", + "bundled": true, + "optional": true + }, + "center-align": { + "version": "0.1.3", + "bundled": true, + "optional": true, + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "chalk": { + "version": "1.1.3", + "bundled": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "class-utils": { + "version": "0.3.6", + "bundled": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true + } + } + }, + "cliui": { + "version": "2.1.0", + "bundled": true, + "optional": true, + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "bundled": true, + "optional": true + } + } + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "collection-visit": { + "version": "1.0.0", + "bundled": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "commondir": { + "version": "1.0.1", + "bundled": true + }, + "component-emitter": { + "version": "1.2.1", + "bundled": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "convert-source-map": { + "version": "1.5.1", + "bundled": true + }, + "copy-descriptor": { + "version": "0.1.1", + "bundled": true + }, + "core-js": { + "version": "2.5.6", + "bundled": true + }, + "cross-spawn": { + "version": "4.0.2", + "bundled": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "requires": { + "ms": "2.0.0" + } + }, + "debug-log": { + "version": "1.0.1", + "bundled": true + }, + "decamelize": { + "version": "1.2.0", + "bundled": true + }, + "decode-uri-component": { + "version": "0.2.0", + "bundled": true + }, + "default-require-extensions": { + "version": "1.0.0", + "bundled": true, + "requires": { + "strip-bom": "^2.0.0" + } + }, + "define-property": { + "version": "2.0.2", + "bundled": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true + } + } + }, + "detect-indent": { + "version": "4.0.0", + "bundled": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "error-ex": { + "version": "1.3.1", + "bundled": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "bundled": true + }, + "esutils": { + "version": "2.0.2", + "bundled": true + }, + "execa": { + "version": "0.7.0", + "bundled": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "bundled": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "bundled": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "bundled": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "bundled": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "bundled": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "bundled": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "kind-of": { + "version": "6.0.2", + "bundled": true + } + } + }, + "fill-range": { + "version": "4.0.0", + "bundled": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "find-cache-dir": { + "version": "0.1.1", + "bundled": true, + "requires": { + "commondir": "^1.0.1", + "mkdirp": "^0.5.1", + "pkg-dir": "^1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "bundled": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "for-in": { + "version": "1.0.2", + "bundled": true + }, + "foreground-child": { + "version": "1.5.6", + "bundled": true, + "requires": { + "cross-spawn": "^4", + "signal-exit": "^3.0.0" + } + }, + "fragment-cache": { + "version": "0.2.1", + "bundled": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "get-caller-file": { + "version": "1.0.2", + "bundled": true + }, + "get-stream": { + "version": "3.0.0", + "bundled": true + }, + "get-value": { + "version": "2.0.6", + "bundled": true + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globals": { + "version": "9.18.0", + "bundled": true + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true + }, + "handlebars": { + "version": "4.0.11", + "bundled": true, + "requires": { + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4", + "uglify-js": "^2.6" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "bundled": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "has-ansi": { + "version": "2.0.0", + "bundled": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "1.0.0", + "bundled": true + }, + "has-value": { + "version": "1.0.0", + "bundled": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true + } + } + }, + "has-values": { + "version": "1.0.0", + "bundled": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "bundled": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "bundled": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hosted-git-info": { + "version": "2.6.0", + "bundled": true + }, + "imurmurhash": { + "version": "0.1.4", + "bundled": true + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "invariant": { + "version": "2.2.4", + "bundled": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "invert-kv": { + "version": "1.0.0", + "bundled": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "bundled": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-arrayish": { + "version": "0.2.1", + "bundled": true + }, + "is-buffer": { + "version": "1.1.6", + "bundled": true + }, + "is-builtin-module": { + "version": "1.0.0", + "bundled": true, + "requires": { + "builtin-modules": "^1.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "bundled": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "bundled": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "bundled": true + } + } + }, + "is-extendable": { + "version": "0.1.1", + "bundled": true + }, + "is-finite": { + "version": "1.0.2", + "bundled": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true + }, + "is-number": { + "version": "3.0.0", + "bundled": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-odd": { + "version": "2.0.0", + "bundled": true, + "requires": { + "is-number": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "bundled": true + } + } + }, + "is-plain-object": { + "version": "2.0.4", + "bundled": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true + } + } + }, + "is-stream": { + "version": "1.1.0", + "bundled": true + }, + "is-utf8": { + "version": "0.2.1", + "bundled": true + }, + "is-windows": { + "version": "1.0.2", + "bundled": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true + }, + "isexe": { + "version": "2.0.0", + "bundled": true + }, + "isobject": { + "version": "3.0.1", + "bundled": true + }, + "istanbul-lib-coverage": { + "version": "1.2.0", + "bundled": true + }, + "istanbul-lib-hook": { + "version": "1.1.0", + "bundled": true, + "requires": { + "append-transform": "^0.4.0" + } + }, + "istanbul-lib-instrument": { + "version": "1.10.1", + "bundled": true, + "requires": { + "babel-generator": "^6.18.0", + "babel-template": "^6.16.0", + "babel-traverse": "^6.18.0", + "babel-types": "^6.18.0", + "babylon": "^6.18.0", + "istanbul-lib-coverage": "^1.2.0", + "semver": "^5.3.0" + } + }, + "istanbul-lib-report": { + "version": "1.1.3", + "bundled": true, + "requires": { + "istanbul-lib-coverage": "^1.1.2", + "mkdirp": "^0.5.1", + "path-parse": "^1.0.5", + "supports-color": "^3.1.2" + }, + "dependencies": { + "supports-color": { + "version": "3.2.3", + "bundled": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.3", + "bundled": true, + "requires": { + "debug": "^3.1.0", + "istanbul-lib-coverage": "^1.1.2", + "mkdirp": "^0.5.1", + "rimraf": "^2.6.1", + "source-map": "^0.5.3" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "bundled": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "istanbul-reports": { + "version": "1.4.0", + "bundled": true, + "requires": { + "handlebars": "^4.0.3" + } + }, + "js-tokens": { + "version": "3.0.2", + "bundled": true + }, + "jsesc": { + "version": "1.3.0", + "bundled": true + }, + "kind-of": { + "version": "3.2.2", + "bundled": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "lazy-cache": { + "version": "1.0.4", + "bundled": true, + "optional": true + }, + "lcid": { + "version": "1.0.0", + "bundled": true, + "requires": { + "invert-kv": "^1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "bundled": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "bundled": true + } + } + }, + "lodash": { + "version": "4.17.10", + "bundled": true + }, + "longest": { + "version": "1.0.1", + "bundled": true + }, + "loose-envify": { + "version": "1.3.1", + "bundled": true, + "requires": { + "js-tokens": "^3.0.0" + } + }, + "lru-cache": { + "version": "4.1.3", + "bundled": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "map-cache": { + "version": "0.2.2", + "bundled": true + }, + "map-visit": { + "version": "1.0.0", + "bundled": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "md5-hex": { + "version": "1.3.0", + "bundled": true, + "requires": { + "md5-o-matic": "^0.1.1" + } + }, + "md5-o-matic": { + "version": "0.1.1", + "bundled": true + }, + "mem": { + "version": "1.1.0", + "bundled": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "merge-source-map": { + "version": "1.1.0", + "bundled": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "bundled": true + } + } + }, + "micromatch": { + "version": "3.1.10", + "bundled": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "bundled": true + } + } + }, + "mimic-fn": { + "version": "1.2.0", + "bundled": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true + }, + "mixin-deep": { + "version": "1.3.1", + "bundled": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "bundled": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true + }, + "nanomatch": { + "version": "1.2.9", + "bundled": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-odd": "^2.0.0", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "bundled": true + }, + "array-unique": { + "version": "0.3.2", + "bundled": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true + } + } + }, + "normalize-package-data": { + "version": "2.4.0", + "bundled": true, + "requires": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "bundled": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true + }, + "object-copy": { + "version": "0.1.0", + "bundled": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "bundled": true, + "requires": { + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true + } + } + }, + "object.pick": { + "version": "1.3.0", + "bundled": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true + } + } + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1" + } + }, + "optimist": { + "version": "0.6.1", + "bundled": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true + }, + "os-locale": { + "version": "2.1.0", + "bundled": true, + "requires": { + "execa": "^0.7.0", + "lcid": "^1.0.0", + "mem": "^1.1.0" + } + }, + "p-finally": { + "version": "1.0.0", + "bundled": true + }, + "p-limit": { + "version": "1.2.0", + "bundled": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "bundled": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "bundled": true + }, + "parse-json": { + "version": "2.2.0", + "bundled": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "pascalcase": { + "version": "0.1.1", + "bundled": true + }, + "path-exists": { + "version": "2.1.0", + "bundled": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "path-key": { + "version": "2.0.1", + "bundled": true + }, + "path-parse": { + "version": "1.0.5", + "bundled": true + }, + "path-type": { + "version": "1.1.0", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "bundled": true + }, + "pinkie": { + "version": "2.0.4", + "bundled": true + }, + "pinkie-promise": { + "version": "2.0.1", + "bundled": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "1.0.0", + "bundled": true, + "requires": { + "find-up": "^1.0.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "bundled": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "bundled": true + }, + "pseudomap": { + "version": "1.0.2", + "bundled": true + }, + "read-pkg": { + "version": "1.1.0", + "bundled": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "bundled": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "bundled": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + } + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "bundled": true + }, + "regex-not": { + "version": "1.0.2", + "bundled": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "repeat-element": { + "version": "1.1.2", + "bundled": true + }, + "repeat-string": { + "version": "1.6.1", + "bundled": true + }, + "repeating": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "require-directory": { + "version": "2.1.1", + "bundled": true + }, + "require-main-filename": { + "version": "1.0.1", + "bundled": true + }, + "resolve-from": { + "version": "2.0.0", + "bundled": true + }, + "resolve-url": { + "version": "0.2.1", + "bundled": true + }, + "ret": { + "version": "0.1.15", + "bundled": true + }, + "right-align": { + "version": "0.1.3", + "bundled": true, + "optional": true, + "requires": { + "align-text": "^0.1.1" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "requires": { + "glob": "^7.0.5" + } + }, + "safe-regex": { + "version": "1.1.0", + "bundled": true, + "requires": { + "ret": "~0.1.10" + } + }, + "semver": { + "version": "5.5.0", + "bundled": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true + }, + "set-value": { + "version": "2.0.0", + "bundled": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "shebang-command": { + "version": "1.2.0", + "bundled": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "bundled": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true + }, + "slide": { + "version": "1.1.6", + "bundled": true + }, + "snapdragon": { + "version": "0.8.2", + "bundled": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "bundled": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "bundled": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "bundled": true, + "requires": { + "kind-of": "^3.2.0" + } + }, + "source-map": { + "version": "0.5.7", + "bundled": true + }, + "source-map-resolve": { + "version": "0.5.1", + "bundled": true, + "requires": { + "atob": "^2.0.0", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "bundled": true + }, + "spawn-wrap": { + "version": "1.4.2", + "bundled": true, + "requires": { + "foreground-child": "^1.5.6", + "mkdirp": "^0.5.0", + "os-homedir": "^1.0.1", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.2", + "which": "^1.3.0" + } + }, + "spdx-correct": { + "version": "3.0.0", + "bundled": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "bundled": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "bundled": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "bundled": true + }, + "split-string": { + "version": "3.1.0", + "bundled": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "static-extend": { + "version": "0.1.2", + "bundled": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "string-width": { + "version": "2.1.1", + "bundled": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "bundled": true + }, + "strip-ansi": { + "version": "4.0.0", + "bundled": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "bundled": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "bundled": true + }, + "supports-color": { + "version": "2.0.0", + "bundled": true + }, + "test-exclude": { + "version": "4.2.1", + "bundled": true, + "requires": { + "arrify": "^1.0.1", + "micromatch": "^3.1.8", + "object-assign": "^4.1.0", + "read-pkg-up": "^1.0.1", + "require-main-filename": "^1.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "bundled": true + }, + "array-unique": { + "version": "0.3.2", + "bundled": true + }, + "braces": { + "version": "2.3.2", + "bundled": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "bundled": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "bundled": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "bundled": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "bundled": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "bundled": true + } + } + }, + "extglob": { + "version": "2.0.4", + "bundled": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "bundled": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "bundled": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-number": { + "version": "3.0.0", + "bundled": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true + }, + "micromatch": { + "version": "3.1.10", + "bundled": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } + } + }, + "to-fast-properties": { + "version": "1.0.3", + "bundled": true + }, + "to-object-path": { + "version": "0.3.0", + "bundled": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "to-regex": { + "version": "3.0.2", + "bundled": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "bundled": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "bundled": true, + "requires": { + "kind-of": "^3.0.2" + } + } + } + }, + "trim-right": { + "version": "1.0.1", + "bundled": true + }, + "uglify-js": { + "version": "2.8.29", + "bundled": true, + "optional": true, + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "yargs": { + "version": "3.10.0", + "bundled": true, + "optional": true, + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "union-value": { + "version": "1.0.0", + "bundled": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "set-value": { + "version": "0.4.3", + "bundled": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + } + } + } + }, + "unset-value": { + "version": "1.0.0", + "bundled": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "bundled": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "bundled": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "bundled": true + }, + "isobject": { + "version": "3.0.1", + "bundled": true + } + } + }, + "urix": { + "version": "0.1.0", + "bundled": true + }, + "use": { + "version": "3.1.0", + "bundled": true, + "requires": { + "kind-of": "^6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "bundled": true + } + } + }, + "validate-npm-package-license": { + "version": "3.0.3", + "bundled": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "which": { + "version": "1.3.0", + "bundled": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "bundled": true + }, + "window-size": { + "version": "0.1.0", + "bundled": true, + "optional": true + }, + "wordwrap": { + "version": "0.0.3", + "bundled": true + }, + "wrap-ansi": { + "version": "2.1.0", + "bundled": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + }, + "write-file-atomic": { + "version": "1.3.4", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "slide": "^1.1.5" + } + }, + "y18n": { + "version": "3.2.1", + "bundled": true + }, + "yallist": { + "version": "2.1.2", + "bundled": true + }, + "yargs": { + "version": "11.1.0", + "bundled": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.1.1", + "find-up": "^2.1.0", + "get-caller-file": "^1.0.1", + "os-locale": "^2.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^9.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "bundled": true + }, + "camelcase": { + "version": "4.1.0", + "bundled": true + }, + "cliui": { + "version": "4.1.0", + "bundled": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "bundled": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "yargs-parser": { + "version": "9.0.2", + "bundled": true, + "requires": { + "camelcase": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "8.1.0", + "bundled": true, + "requires": { + "camelcase": "^4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "bundled": true + } + } + } + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha1-R6ewFrqmi1+g7PPe4IqFxnmsZFU=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=" + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-inspect": { + "version": "1.6.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/object-inspect/-/object-inspect-1.6.0.tgz", + "integrity": "sha1-xwtsv3LydKq0w0wMgvUWe/gs8Vs=" + }, + "object-is": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", + "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha1-HEfyct8nfzsdrwYWd9nILiMixg4=" + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha1-lovxEA15Vrs8oIbwBvhGs7xACNo=", + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "^3.0.1" + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" + }, + "octokit-pagination-methods": { + "version": "1.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz", + "integrity": "sha1-z0cu3J1VEFX573P25CtNu0yAvqQ=" + }, + "once": { + "version": "1.4.0", + "resolved": "http://bbnpm.azurewebsites.net/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "http://bbnpm.azurewebsites.net/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "http://bbnpm.azurewebsites.net/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-name": { + "version": "3.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/os-name/-/os-name-3.1.0.tgz", + "integrity": "sha1-3sGdlmKW4c1i1wGlpm7h3ernCAE=", + "requires": { + "macos-release": "^2.2.0", + "windows-release": "^3.1.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha1-hc36+uso6Gd/QW4odZK18/SepBA=", + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "over": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/over/-/over-0.0.5.tgz", + "integrity": "sha1-8phS5w/X4l82DgE6jsRMgq7bVwg=" + }, + "p-cancelable": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", + "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==" + }, + "p-finally": { + "version": "1.0.0", + "resolved": "http://bbnpm.azurewebsites.net/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-is-promise": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", + "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=" + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha1-uGvV8MJWkJEcdZD8v8IBDVSzzLg=", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha1-MQko/u+cnsxltosXaTAYpmXOoXU=" + }, + "p-map-series": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz", + "integrity": "sha1-v5j+V1cFZYqeE1G++4WuTB8Hvco=", + "requires": { + "p-reduce": "^1.0.0" + } + }, + "p-pipe": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz", + "integrity": "sha1-SxoROZoRUgpneQ7loMHViB1r7+k=" + }, + "p-queue": { + "version": "4.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-queue/-/p-queue-4.0.0.tgz", + "integrity": "sha1-7Q7uh5iSftbywvX1t3/bIGGl00Y=", + "requires": { + "eventemitter3": "^3.1.0" + } + }, + "p-reduce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", + "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=" + }, + "p-timeout": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", + "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", + "requires": { + "p-finally": "^1.0.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, + "p-waterfall": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-waterfall/-/p-waterfall-1.0.0.tgz", + "integrity": "sha1-ftlLPOszMngjU69qrhGqn8I1uwA=", + "requires": { + "p-reduce": "^1.0.0" + } + }, + "package-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-5.0.0.tgz", + "integrity": "sha512-EeHQFFTlEmLrkIQoxbE9w0FuAWHoc1XpthDqnZ/i9keOt701cteyXwAxQFLpVqVjj3feh2TodkihjLaRUtIgLg==", + "requires": { + "got": "^8.3.1", + "registry-auth-token": "^3.3.2", + "registry-url": "^3.1.0", + "semver": "^5.5.0" + } + }, + "parallel-transform": { + "version": "1.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha1-kEnKN9bLIYLDsdLHIL6U0UpYFPw=", + "requires": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha1-aR0nCeeMefrjoVZiJFLQB2LKqqI=", + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-github-repo-url": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz", + "integrity": "sha1-nn2LslKmy2ukJZUGC3v23z28H1A=" + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" + }, + "parse-path": { + "version": "4.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/parse-path/-/parse-path-4.0.1.tgz", + "integrity": "sha1-DsdpcElJd4yzuO2l6ZTDIHOhrf8=", + "requires": { + "is-ssh": "^1.3.0", + "protocols": "^1.4.0" + } + }, + "parse-url": { + "version": "5.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/parse-url/-/parse-url-5.0.1.tgz", + "integrity": "sha1-mcQIT8Eb4UFB76QbPRF6lvy5Un8=", + "requires": { + "is-ssh": "^1.3.0", + "normalize-url": "^3.3.0", + "parse-path": "^4.0.0", + "protocols": "^1.4.0" + } + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + }, + "path-exists": { + "version": "3.0.0", + "resolved": "http://bbnpm.azurewebsites.net/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "http://bbnpm.azurewebsites.net/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "http://bbnpm.azurewebsites.net/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha1-1i27VnlAXXLEc37FhgDp3c8G0kw=" + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "http://bbnpm.azurewebsites.net/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + } + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha1-zvMdyOCho7sNEFwM2Xzzv0f0428=", + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + } + } + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "http://bbnpm.azurewebsites.net/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pidusage": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-1.2.0.tgz", + "integrity": "sha512-OGo+iSOk44HRJ8q15AyG570UYxcm5u+R99DI8Khu8P3tKGkVu5EZX4ywHglWSTMNNXQ274oeGpYrvFEhDIFGPg==" + }, + "pify": { + "version": "4.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/pify/-/pify-4.0.1.tgz", + "integrity": "sha1-SyzSXFDVmHNcUCkiJP2MbfQeMjE=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha1-J0kCDyOe2ZCIGx9xIQ1R62UjvqM=", + "requires": { + "find-up": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha1-SRafHXmTQwZG2mHsxa41XCHJe3M=", + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha1-2+w7OrdZdYBxtY/ln8QYca8hQA4=", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha1-qgeniMwxUck5tRMfY1cPDdIAlTc=", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha1-Mi1poFwCZLJZl9n0DNiokasAZKQ=", + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha1-yyhoVA4xPWHeWPr741zpAE1VQOY=" + } + } + }, + "please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "requires": { + "semver-compare": "^1.0.0" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "http://bbnpm.azurewebsites.net/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" + }, + "priorityqueuejs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/priorityqueuejs/-/priorityqueuejs-1.0.0.tgz", + "integrity": "sha1-LuTyPCVgkT4IwHzlzN1t498sWvg=" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha1-eCDZsWEgzFXKmud5JoCufbptf+I=" + }, + "progress": { + "version": "2.0.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/progress/-/progress-2.0.3.tgz", + "integrity": "sha1-foz42PW48jnBvGi+tOt4Vn1XLvg=" + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" + }, + "promise-retry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz", + "integrity": "sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=", + "requires": { + "err-code": "^1.0.0", + "retry": "^0.10.0" + } + }, + "promzard": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/promzard/-/promzard-0.3.0.tgz", + "integrity": "sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=", + "requires": { + "read": "1" + } + }, + "propagate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", + "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=" + }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" + }, + "protocols": { + "version": "1.4.7", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/protocols/-/protocols-1.4.7.tgz", + "integrity": "sha1-lfeIpPDpebKR/+/PVjatET0DfTI=" + }, + "protoduck": { + "version": "5.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/protoduck/-/protoduck-5.0.1.tgz", + "integrity": "sha1-A8NlnKGAB7aaUP2Cp+vMUWJhFR8=", + "requires": { + "genfun": "^5.0.0" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "psl": { + "version": "1.4.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/psl/-/psl-1.4.0.tgz", + "integrity": "sha1-XdJhVs22n6H9uKsZkWZ9P4DO18I=" + }, + "pullstream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pullstream/-/pullstream-0.4.1.tgz", + "integrity": "sha1-1vs79a7Wl+gxFQ6xACwlo/iuExQ=", + "requires": { + "over": ">= 0.0.5 < 1", + "readable-stream": "~1.0.31", + "setimmediate": ">= 1.0.2 < 2", + "slice-stream": ">= 1.0.0 < 2" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/pump/-/pump-3.0.0.tgz", + "integrity": "sha1-tKIRaBW94vTh6mAjVOjHVWUQemQ=", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha1-NlE74karJ1cLGjdKXOJ4v9dDcM4=", + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/pump/-/pump-2.0.1.tgz", + "integrity": "sha1-Ejma3W5M91Jtlzy8i1zi4pCLOQk=", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha1-tYsBCsQMIsVldhbI0sLALHv0eew=" + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/qs/-/qs-6.5.2.tgz", + "integrity": "sha1-yzroBuh0BERYTvFUzo7pjUA/PjY=" + }, + "query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "requires": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "querystringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", + "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==" + }, + "quick-lru": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", + "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=" + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "requires": { + "mute-stream": "~0.0.4" + } + }, + "read-cmd-shim": { + "version": "1.0.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz", + "integrity": "sha1-h+Q+ulAJi6WjLQzrWDq45DuWHBY=", + "requires": { + "graceful-fs": "^4.1.2" + } + }, + "read-package-json": { + "version": "2.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/read-package-json/-/read-package-json-2.1.0.tgz", + "integrity": "sha1-49QubDXqWugg2aA6sMcpEhf8UdU=", + "requires": { + "glob": "^7.1.1", + "graceful-fs": "^4.1.2", + "json-parse-better-errors": "^1.0.1", + "normalize-package-data": "^2.0.0", + "slash": "^1.0.0" + }, + "dependencies": { + "slash": { + "version": "1.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" + } + } + }, + "read-package-tree": { + "version": "5.3.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/read-package-tree/-/read-package-tree-5.3.1.tgz", + "integrity": "sha1-oyy2TH8x64pvMe8G+c7fdAaP5jY=", + "requires": { + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "util-promisify": "^2.1.0" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "dependencies": { + "load-json-file": { + "version": "4.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + } + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + } + }, + "read-text-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-text-file/-/read-text-file-1.1.0.tgz", + "integrity": "sha1-0MPxh2iCj5EH1huws2jue5D3GJM=", + "requires": { + "iconv-lite": "^0.4.17", + "jschardet": "^1.4.2" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=" + } + } + }, + "readdir-scoped-modules": { + "version": "1.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", + "integrity": "sha1-jUVAe0+HCg3K68DihnDRjnRRQwk=", + "requires": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "requires": { + "resolve": "^1.1.6" + } + }, + "redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "requires": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + } + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==" + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha1-H07OJ+ALC2XgJHpoEOaoXYOldSw=", + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexp.prototype.flags": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz", + "integrity": "sha512-ztaw4M1VqgMwl9HlPpOuiYgItcHlunW0He2fE6eNfT6E/CF2FtYi9ofOYe4mKntstYk0Fyh/rDRBdS3AnxjlrA==", + "requires": { + "define-properties": "^1.1.2" + } + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha1-jRnTHPYySCtYkEn4KB+T28uk0H8=" + }, + "regexpu-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", + "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "requires": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + }, + "registry-auth-token": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", + "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", + "requires": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "requires": { + "rc": "^1.0.1" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "requires": { + "jsesc": "~0.5.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha1-eC4NglwMWjuzlzH4Tv7mt0Lmsc4=" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "requires": { + "is-finite": "^1.0.0" + } + }, + "replace-in-file": { + "version": "4.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/replace-in-file/-/replace-in-file-4.2.0.tgz", + "integrity": "sha1-vnefT2Go7hq5+ReZi9NkQWc2gP4=", + "requires": { + "chalk": "^2.4.2", + "glob": "^7.1.4", + "yargs": "^13.3.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc=" + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha1-48mzFWnhBoEd8kL3FXJaH0xJQyA=" + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha1-SRafHXmTQwZG2mHsxa41XCHJe3M=", + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha1-2+w7OrdZdYBxtY/ln8QYca8hQA4=", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha1-qgeniMwxUck5tRMfY1cPDdIAlTc=", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha1-Mi1poFwCZLJZl9n0DNiokasAZKQ=", + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha1-yyhoVA4xPWHeWPr741zpAE1VQOY=" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha1-InZ74htirxCBV0MG9prFG2IgOWE=", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "yargs": { + "version": "13.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha1-TGV6VeB+Xyz5R/ijZlZ8BKDe3IM=", + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha1-0mBYUyqgbTZf4JH2ofwGsvfl7KA=", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/request/-/request-2.88.0.tgz", + "integrity": "sha1-nC/KT301tZLv5Xx/ClXoEFIST+8=", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "form-data": { + "version": "2.3.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha1-3M5SwF9kTymManq5Nr1yTO/786Y=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha1-U/Nto/R3g7CSWvoG/587FlKA94E=", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + } + } + }, + "request-progress": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-0.3.1.tgz", + "integrity": "sha1-ByHBBdipasayzossia4tXs/Pazo=", + "requires": { + "throttleit": "~0.0.2" + } + }, + "request-promise-core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", + "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "requires": { + "lodash": "^4.17.15" + } + }, + "request-promise-native": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", + "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", + "requires": { + "request-promise-core": "1.1.3", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "http://bbnpm.azurewebsites.net/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha1-0LMp7MfMD2Fkn2IhW+aa9UqomJs=" + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + }, + "resolve": { + "version": "1.12.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/resolve/-/resolve-1.12.0.tgz", + "integrity": "sha1-P8ZEo1yEpIVUYJ/ybsUrZvpXffY=", + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "requires": { + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha1-SrzYUq0y3Xuqv+m0DgCjbbXzkuY=" + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "requires": { + "lowercase-keys": "^1.0.0" + } + }, + "restify": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/restify/-/restify-7.7.0.tgz", + "integrity": "sha512-BGirRv70pIy5W7tqX7s7+NNjBcjzU2YYgV4KABVbR5g8JjMeucgUzaf2VvTUSmz83qMZAuQ/gXEmPFyPHIcfJQ==", + "requires": { + "assert-plus": "^1.0.0", + "bunyan": "^1.8.12", + "csv": "^1.1.1", + "dtrace-provider": "^0.8.1", + "escape-regexp-component": "^1.0.2", + "ewma": "^2.0.1", + "find-my-way": "^1.13.0", + "formidable": "^1.2.1", + "http-signature": "^1.2.0", + "lodash": "^4.17.10", + "lru-cache": "^4.1.3", + "mime": "^1.5.0", + "negotiator": "^0.6.1", + "once": "^1.4.0", + "pidusage": "^1.2.0", + "qs": "^6.5.2", + "restify-errors": "^5.0.0", + "semver": "^5.4.1", + "spdy": "^3.4.7", + "uuid": "^3.1.0", + "vasync": "^1.6.4", + "verror": "^1.10.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + } + } + }, + "restify-errors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/restify-errors/-/restify-errors-5.0.0.tgz", + "integrity": "sha512-+vby9Kxf7qlzvbZSTIEGkIixkeHG+pVCl34dk6eKnL+ua4pCezpdLT/1/eabzPZb65ADrgoc04jeWrrF1E1pvQ==", + "requires": { + "assert-plus": "^1.0.0", + "lodash": "^4.2.1", + "safe-json-stringify": "^1.0.3", + "verror": "^1.8.1" + } + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ret/-/ret-0.1.15.tgz", + "integrity": "sha1-uKSCXVvbH8P29Twrwz+BOIaBx7w=" + }, + "retry": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", + "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=" + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha1-stEE/g2Psnz54KHNqCYt04M8bKs=", + "requires": { + "glob": "^7.1.3" + } + }, + "rsa-pem-from-mod-exp": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/rsa-pem-from-mod-exp/-/rsa-pem-from-mod-exp-0.8.4.tgz", + "integrity": "sha1-NipCxtMEBW1JOz8SvOq7LGV2ptQ=" + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "requires": { + "is-promise": "^2.1.0" + } + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "requires": { + "aproba": "^1.1.1" + } + }, + "rxjs": { + "version": "6.5.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/rxjs/-/rxjs-6.5.3.tgz", + "integrity": "sha1-UQ4mMX9NuRp+sd532d2boKSJmjo=", + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha1-t02uxJsRSPiMZLaNSbHoFcHy9Rk=" + }, + "safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "optional": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/sax/-/sax-1.2.4.tgz", + "integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk=" + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=" + }, + "semaphore": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz", + "integrity": "sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA==" + }, + "semver": { + "version": "5.5.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/semver/-/semver-5.5.0.tgz", + "integrity": "sha1-3Eu8emyp2Rbe5dQ1FvAJK1j3uKs=" + }, + "semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=" + }, + "semver-dsl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz", + "integrity": "sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA=", + "requires": { + "semver": "^5.3.0" + } + }, + "semver-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/semver-store/-/semver-store-0.3.0.tgz", + "integrity": "sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "http://bbnpm.azurewebsites.net/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha1-oY1AUw5vB95CKMfe/kInr4ytAFs=", + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "http://bbnpm.azurewebsites.net/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "http://bbnpm.azurewebsites.net/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "shelljs": { + "version": "0.8.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/shelljs/-/shelljs-0.8.3.tgz", + "integrity": "sha1-p/MxlSDr8J7oEnWyNorbKGZZsJc=", + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" + }, + "should": { + "version": "13.2.3", + "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", + "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", + "requires": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "should-equal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", + "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", + "requires": { + "should-type": "^1.4.0" + } + }, + "should-format": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha1-m/yPdPo5IFxT04w01xcwPidxJPE=", + "requires": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "should-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=" + }, + "should-type-adaptors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "requires": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "should-util": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", + "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "http://bbnpm.azurewebsites.net/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "sinon": { + "version": "7.5.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/sinon/-/sinon-7.5.0.tgz", + "integrity": "sha1-6UiOpGYHDqkI/USj1keP1JI8Z+w=", + "requires": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.3", + "diff": "^3.5.0", + "lolex": "^4.2.0", + "nise": "^1.5.2", + "supports-color": "^5.5.0" + } + }, + "slash": { + "version": "2.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/slash/-/slash-2.0.0.tgz", + "integrity": "sha1-3lUoUaF1nfOo8gZTVEL17E3eq0Q=" + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha1-ys12k0YaY3pXiNkqfdT7oGjoFjY=", + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, + "slice-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-stream/-/slice-stream-1.0.0.tgz", + "integrity": "sha1-WzO9ZvATsaf4ZGCwPUY97DmtPqA=", + "requires": { + "readable-stream": "~1.0.31" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=" + }, + "smart-buffer": { + "version": "4.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/smart-buffer/-/smart-buffer-4.0.2.tgz", + "integrity": "sha1-UgeFjDgVzGkRBwPGuU5GwVY0OV0=" + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha1-ZJIufFZbDhQgS6GqfWlkJ40lGC0=", + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/debug/-/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha1-bBdfhv8UvbByRWPo88GwIaKGhTs=", + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha1-+VZHlIbyrNeXAGk/b3uAXkWrVuI=", + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=" + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "socks": { + "version": "2.3.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/socks/-/socks-2.3.2.tgz", + "integrity": "sha1-reOI6ebYf9sRZJwVdGxXiSKliD4=", + "requires": { + "ip": "^1.1.5", + "smart-buffer": "4.0.2" + } + }, + "socks-proxy-agent": { + "version": "4.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz", + "integrity": "sha1-PImR8xRbJ5nnDhG9X7yLGWMRY4Y=", + "requires": { + "agent-base": "~4.2.1", + "socks": "~2.3.2" + }, + "dependencies": { + "agent-base": { + "version": "4.2.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/agent-base/-/agent-base-4.2.1.tgz", + "integrity": "sha1-2J5ZmfeXh1Z0wH2H8mD8Qeg+jKk=", + "requires": { + "es6-promisify": "^5.0.0" + } + } + } + }, + "sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha1-cuLMNAlVQ+Q7LGKyxMENSpBU8lk=", + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", + "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha1-+4PlBERSaPFUsHTiGMh8ADzTHfQ=", + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha1-LqRQrudPKom/uUUZwH/Nb0EyKXc=" + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha1-meEZt6XaAOBUkcn6M4t5BII7QdA=", + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha1-NpS1gEVnpFjTyARYQqY1hjL2JlQ=" + }, + "spdy": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz", + "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=", + "requires": { + "debug": "^2.6.8", + "handle-thing": "^1.2.5", + "http-deceiver": "^1.2.7", + "safe-buffer": "^5.0.1", + "select-hose": "^2.0.0", + "spdy-transport": "^2.0.18" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "spdy-transport": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.1.1.tgz", + "integrity": "sha512-q7D8c148escoB3Z7ySCASadkegMmUZW8Wb/Q1u0/XBgDKMO880rLQDj8Twiew/tYi7ghemKUi/whSYOwE17f5Q==", + "requires": { + "debug": "^2.6.8", + "detect-node": "^2.0.3", + "hpack.js": "^2.1.6", + "obuf": "^1.1.1", + "readable-stream": "^2.2.9", + "safe-buffer": "^5.0.1", + "wbuf": "^1.7.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "split": { + "version": "1.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/split/-/split-1.0.1.tgz", + "integrity": "sha1-YFvZvjA6pZ+zX5Ip++oN3snqB9k=", + "requires": { + "through": "2" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha1-fLCd2jqGWFcFxks5pkZgOGguj+I=", + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "split2": { + "version": "2.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/split2/-/split2-2.2.0.tgz", + "integrity": "sha1-GGsldbz4PoW30YRldWI47k7kJJM=", + "requires": { + "through2": "^2.0.2" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha1-+2YcC+8ps520B2nuOfpwCT1vaHc=", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha1-KjxBso3UW2K2Nnbst0ABJlrp7dg=", + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "stack-chain": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", + "integrity": "sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=" + }, + "static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "requires": { + "escodegen": "^1.8.1" + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha1-6+J6DDibBPvMIzZClS4Qcxr6m64=", + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + }, + "stream-transform": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-0.2.2.tgz", + "integrity": "sha1-dYZ0h/SVKPi/HYJJllh1PQLfeDg=" + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "string.prototype.trimleft": { + "version": "2.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", + "integrity": "sha1-bMR/DX641isPNwFhFxWjlUWR1jQ=", + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", + "integrity": "sha1-Zp0WS+nfm291WfqOiZRbFopabFg=", + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "requires": { + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=" + } + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "http://bbnpm.azurewebsites.net/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=" + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "strong-log-transformer": { + "version": "2.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz", + "integrity": "sha1-D17XjTJeBCGsb5D38Q5pHWrjrhA=", + "requires": { + "duplexer": "^0.1.1", + "minimist": "^1.2.0", + "through": "^2.3.4" + } + }, + "superagent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-2.3.0.tgz", + "integrity": "sha1-cDUpoHFOV+EjlZ3e+84ZOy5Q0RU=", + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.0.6", + "debug": "^2.2.0", + "extend": "^3.0.0", + "form-data": "1.0.0-rc4", + "formidable": "^1.0.17", + "methods": "^1.1.1", + "mime": "^1.3.4", + "qs": "^6.1.0", + "readable-stream": "^2.0.5" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "form-data": { + "version": "1.0.0-rc4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz", + "integrity": "sha1-BaxrwiIntD5EYfSIFhVUaZ1Pi14=", + "requires": { + "async": "^1.5.2", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.10" + } + } + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=", + "requires": { + "has-flag": "^3.0.0" + } + }, + "swagger-client": { + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-2.2.21.tgz", + "integrity": "sha1-WWa+I0dyRm5EcW9l4yAIFm2u66Q=", + "requires": { + "btoa": "^1.1.2", + "cookiejar": "^2.0.1", + "js-yaml": "^3.3.0", + "lodash-compat": "^3.5.0", + "q": "^1.4.1", + "superagent": "^2.2" + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/table/-/table-5.4.6.tgz", + "integrity": "sha1-EpLRlQDOP4YFOwXw6Ofko7shB54=", + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc=" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha1-InZ74htirxCBV0MG9prFG2IgOWE=", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "tar": { + "version": "4.4.13", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/tar/-/tar-4.4.13.tgz", + "integrity": "sha1-Q7NkvFKIjVVSmGN7ENYHkCVKtSU=", + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "temp-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", + "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=" + }, + "temp-write": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-write/-/temp-write-3.4.0.tgz", + "integrity": "sha1-jP9jD7fp2gXwR8dM5M5NaFRX1JI=", + "requires": { + "graceful-fs": "^4.1.2", + "is-stream": "^1.1.0", + "make-dir": "^1.0.0", + "pify": "^3.0.0", + "temp-dir": "^1.0.0", + "uuid": "^3.0.1" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + } + } + }, + "text-extensions": { + "version": "2.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/text-extensions/-/text-extensions-2.0.0.tgz", + "integrity": "sha1-Q+q9G0lUgvrkor9l5fVsKfaSIPY=" + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" + }, + "thenify": { + "version": "3.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/thenify/-/thenify-3.3.0.tgz", + "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", + "requires": { + "any-promise": "^1.0.0" + } + }, + "thenify-all": { + "version": "1.6.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", + "requires": { + "thenify": ">= 3.1.0 < 4" + } + }, + "throttleit": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz", + "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=" + }, + "through": { + "version": "2.3.8", + "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "through2": { + "version": "2.0.5", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/through2/-/through2-2.0.5.tgz", + "integrity": "sha1-AcHjnrMdB8t9A6lqcIIyYLIxMs0=", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=", + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=" + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha1-E8/dmzNlUvMLUfM6iuG0Knp1mc4=", + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha1-zZ+yoKodWhK0c72fuW+j3P9lreI=", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "requires": { + "punycode": "^2.1.0" + } + }, + "traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=" + }, + "trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=" + }, + "trim-off-newlines": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", + "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=" + }, + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "ts-node": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-4.1.0.tgz", + "integrity": "sha512-xcZH12oVg9PShKhy3UHyDmuDLV3y7iKwX25aMVPt1SIXSuAfWkFiGPEkg+th8R4YKW/QCxDoW7lJdb15lx6QWg==", + "requires": { + "arrify": "^1.0.0", + "chalk": "^2.3.0", + "diff": "^3.1.0", + "make-error": "^1.1.1", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "source-map-support": "^0.5.0", + "tsconfig": "^7.0.0", + "v8flags": "^3.0.0", + "yn": "^2.0.0" + } + }, + "tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "requires": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "tslib": { + "version": "1.10.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha1-w8GflZc/sKYpc/sJ2Q2WHuQ+XIo=" + }, + "tslint": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz", + "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==", + "requires": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" + }, + "dependencies": { + "diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==" + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "tslint-microsoft-contrib": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/tslint-microsoft-contrib/-/tslint-microsoft-contrib-5.2.1.tgz", + "integrity": "sha512-PDYjvpo0gN9IfMULwKk0KpVOPMhU6cNoT9VwCOLeDl/QS8v8W2yspRpFFuUS7/c5EIH/n8ApMi8TxJAz1tfFUA==", + "requires": { + "tsutils": "^2.27.2 <2.29.0" + }, + "dependencies": { + "tsutils": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.28.0.tgz", + "integrity": "sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA==", + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha1-7XGZF/EcoN7lhicrKsSeAVot11k=", + "requires": { + "tslib": "^1.8.1" + } + }, + "tunnel": { + "version": "0.0.6", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha1-cvExSzSlsZLbASMk3yzFh8pH+Sw=" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "http://bbnpm.azurewebsites.net/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "http://bbnpm.azurewebsites.net/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "type-check": { + "version": "0.3.2", + "resolved": "http://bbnpm.azurewebsites.net/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha1-dkb7XxiHHPu3dJ5pvTmmOI63RQw=" + }, + "type-fest": { + "version": "0.3.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha1-Y9ANIE4FlHT+Xht8ARESu9HcKeE=" + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "typedoc": { + "version": "0.15.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/typedoc/-/typedoc-0.15.0.tgz", + "integrity": "sha1-Ier020HPJ5e60CenTyp1zQiuDC0=", + "requires": { + "@types/minimatch": "3.0.3", + "fs-extra": "^8.1.0", + "handlebars": "^4.1.2", + "highlight.js": "^9.15.8", + "lodash": "^4.17.15", + "marked": "^0.7.0", + "minimatch": "^3.0.0", + "progress": "^2.0.3", + "shelljs": "^0.8.3", + "typedoc-default-themes": "^0.6.0", + "typescript": "3.5.x" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha1-SdQ8RaiM2Wd2aMt74bRu/bjS4cA=", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "typescript": { + "version": "3.5.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/typescript/-/typescript-3.5.3.tgz", + "integrity": "sha1-yDD2V/k/HqhGgZ6SkJL1/lmD6Xc=" + } + } + }, + "typedoc-default-themes": { + "version": "0.6.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/typedoc-default-themes/-/typedoc-default-themes-0.6.0.tgz", + "integrity": "sha1-fnO/VN2eEVUN0PtXbVF2t1j4+LU=", + "requires": { + "backbone": "^1.4.0", + "jquery": "^3.4.1", + "lunr": "^2.3.6", + "underscore": "^1.9.1" + } + }, + "typedoc-plugin-external-module-name": { + "version": "2.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/typedoc-plugin-external-module-name/-/typedoc-plugin-external-module-name-2.1.0.tgz", + "integrity": "sha1-JfEI6ZZzrTTzQkcZwQwpnuUOkvA=" + }, + "typedoc-plugin-markdown": { + "version": "2.2.11", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/typedoc-plugin-markdown/-/typedoc-plugin-markdown-2.2.11.tgz", + "integrity": "sha1-gepgenvILPOuC8beS7t+jqa0N/I=", + "requires": { + "fs-extra": "^8.1.0" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha1-SdQ8RaiM2Wd2aMt74bRu/bjS4cA=", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } + } + }, + "typescript": { + "version": "3.7.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/typescript/-/typescript-3.7.2.tgz", + "integrity": "sha1-J+SJuV+lkJRF6f717kjYFpetGPs=" + }, + "typescript-tslint-plugin": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/typescript-tslint-plugin/-/typescript-tslint-plugin-0.3.1.tgz", + "integrity": "sha512-h8HEPBm36UEs901ld1x6m5eY/UFb4mF+A0nvERr4BWMww5wnV5nfcm9ZFt18foYL0GQ5NVMt1Tb3466WUU8dRQ==", + "requires": { + "minimatch": "^3.0.4", + "mock-require": "^3.0.2", + "vscode-languageserver": "^5.1.0" + } + }, + "uglify-js": { + "version": "3.6.8", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/uglify-js/-/uglify-js-3.6.8.tgz", + "integrity": "sha1-Xty8+dScuwQD3En4Vv6BUw1lFF4=", + "optional": true, + "requires": { + "commander": "~2.20.3", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", + "optional": true + } + } + }, + "uid-number": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", + "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" + }, + "umask": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz", + "integrity": "sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=" + }, + "underscore": { + "version": "1.9.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/underscore/-/underscore-1.9.1.tgz", + "integrity": "sha1-BtzjSg5op7q8KbNluOdLiSUgOWE=" + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha1-C2/nuDWuzaYcbqTU8CwUIh4QmEc=", + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha1-HWl2k2mtoFgxA6HmrodoG1ZXMjA=", + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha1-uqvOkQg/xk6UWw861hPiZPfNTmw=", + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "universal-user-agent": { + "version": "4.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/universal-user-agent/-/universal-user-agent-4.0.0.tgz", + "integrity": "sha1-J9ouyH4ydpYZ9ooUmWRl6hy53xY=", + "requires": { + "os-name": "^3.1.0" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha1-tkb2m+OULavOzJ1mOcgNwQXvqmY=" + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + } + } + }, + "unzip": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/unzip/-/unzip-0.1.11.tgz", + "integrity": "sha1-iXScY7BY19kNYZ+GuYqhU107l/A=", + "requires": { + "binary": ">= 0.3.0 < 1", + "fstream": ">= 0.1.30 < 1", + "match-stream": ">= 0.0.2 < 1", + "pullstream": ">= 0.4.1 < 1", + "readable-stream": "~1.0.31", + "setimmediate": ">= 1.0.1 < 2" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha1-lMVA4f93KVbiKZUHwBCupsiDjrA=", + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "requires": { + "prepend-http": "^2.0.0" + } + }, + "url-to-options": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", + "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=" + }, + "use": { + "version": "3.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/use/-/use-3.1.1.tgz", + "integrity": "sha1-1QyMrHmhn7wg8pEfVuuXP04QBw8=" + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "requires": { + "inherits": "2.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "util-promisify": { + "version": "2.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/util-promisify/-/util-promisify-2.1.0.tgz", + "integrity": "sha1-PCI2R2xNMsX/PEcAKt18E7moKlM=", + "requires": { + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha1-RA9xZaRZyaFtwUXrjnLzVocJcDA=", + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "uuid": { + "version": "3.3.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha1-RWjwIW54dg7h2/Ok0s9T4iQRKGY=" + }, + "v8flags": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", + "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==", + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha1-/JH2uce6FchX9MssXe/uw51PQQo=", + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validate-npm-package-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", + "requires": { + "builtins": "^1.0.3" + } + }, + "validator": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz", + "integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA==" + }, + "vasync": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.4.tgz", + "integrity": "sha1-3+k2Fq0OeugBszKp2Iv8XNyOHR8=", + "requires": { + "verror": "1.6.0" + }, + "dependencies": { + "extsprintf": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.2.0.tgz", + "integrity": "sha1-WtlGwi9bMrp/jNdCZxHG6KP8JSk=" + }, + "verror": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz", + "integrity": "sha1-fROyex+swuLakEBetepuW90lLqU=", + "requires": { + "extsprintf": "1.2.0" + } + } + } + }, + "verror": { + "version": "1.10.0", + "resolved": "http://bbnpm.azurewebsites.net/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vscode-jsonrpc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-4.0.0.tgz", + "integrity": "sha512-perEnXQdQOJMTDFNv+UF3h1Y0z4iSiaN9jIlb0OqIYgosPCZGYh/MCUlkFtV2668PL69lRDO32hmvL2yiidUYg==" + }, + "vscode-languageserver": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-5.2.1.tgz", + "integrity": "sha512-GuayqdKZqAwwaCUjDvMTAVRPJOp/SLON3mJ07eGsx/Iq9HjRymhKWztX41rISqDKhHVVyFM+IywICyZDla6U3A==", + "requires": { + "vscode-languageserver-protocol": "3.14.1", + "vscode-uri": "^1.0.6" + } + }, + "vscode-languageserver-protocol": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.14.1.tgz", + "integrity": "sha512-IL66BLb2g20uIKog5Y2dQ0IiigW0XKrvmWiOvc0yXw80z3tMEzEnHjaGAb3ENuU7MnQqgnYJ1Cl2l9RvNgDi4g==", + "requires": { + "vscode-jsonrpc": "^4.0.0", + "vscode-languageserver-types": "3.14.0" + } + }, + "vscode-languageserver-types": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.14.0.tgz", + "integrity": "sha512-lTmS6AlAlMHOvPQemVwo3CezxBp0sNB95KNPkqp3Nxd5VFEnuG1ByM0zlRWos0zjO3ZWtkvhal0COgiV1xIA4A==" + }, + "vscode-uri": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.8.tgz", + "integrity": "sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ==" + }, + "watershed": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/watershed/-/watershed-0.4.0.tgz", + "integrity": "sha1-5BJLh3EptHZSJHdn63IgKfgJVTE=", + "requires": { + "dtrace-provider": "~0.8", + "readable-stream": "1.0.2" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.2.tgz", + "integrity": "sha1-ITzjaGT8Hw1OmOA7nrksZAQimdQ=" + } + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "requires": { + "defaults": "^1.0.3" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha1-qFWYCx8LazWbodXZ+zmulB+qY60=" + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha1-wsSS8eymEpiO/T0iZr4bn8YXDQY=", + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/which/-/which-1.3.1.tgz", + "integrity": "sha1-pFBD1U9YBTFtqNYvn1CRjT2nCwo=", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "http://bbnpm.azurewebsites.net/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha1-rgdOa9wMFKQx6ATmJFScYzsABFc=", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "window-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-1.1.1.tgz", + "integrity": "sha512-5D/9vujkmVQ7pSmc0SCBmHXbkv6eaHwXEx65MywhmUMsI8sGqJ972APq1lotfcwMKPFLuCFfL8xGHLIp7jaBmA==", + "requires": { + "define-property": "^1.0.0", + "is-number": "^3.0.0" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "windows-release": { + "version": "3.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/windows-release/-/windows-release-3.2.0.tgz", + "integrity": "sha1-gSLa1a/DA9gzQiOAaAp5zfqReF8=", + "requires": { + "execa": "^1.0.0" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha1-H9H2cjXVttD+54EFYAG/tpTAOwk=", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc=" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha1-InZ74htirxCBV0MG9prFG2IgOWE=", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "http://bbnpm.azurewebsites.net/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write": { + "version": "1.0.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/write/-/write-1.0.3.tgz", + "integrity": "sha1-CADhRSO5I6OH5BUSPIZWFqrg9cM=", + "requires": { + "mkdirp": "^0.5.1" + } + }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha1-H9Lprh3z51uNjDZ0Q8aS1MqB9IE=", + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "write-json-file": { + "version": "3.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/write-json-file/-/write-json-file-3.2.0.tgz", + "integrity": "sha1-Zbvcns2KFFjhWVJ3DMut/P9f5io=", + "requires": { + "detect-indent": "^5.0.0", + "graceful-fs": "^4.1.15", + "make-dir": "^2.1.0", + "pify": "^4.0.1", + "sort-keys": "^2.0.0", + "write-file-atomic": "^2.4.2" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha1-XwMQ4YuL6JjMBwCSlaMK5B6R5vU=", + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/semver/-/semver-5.7.1.tgz", + "integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=" + } + } + }, + "write-pkg": { + "version": "3.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/write-pkg/-/write-pkg-3.2.0.tgz", + "integrity": "sha1-DheP6Xgg04mokovHlTXb5ows/yE=", + "requires": { + "sort-keys": "^2.0.0", + "write-json-file": "^2.2.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, + "write-json-file": { + "version": "2.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/write-json-file/-/write-json-file-2.3.0.tgz", + "integrity": "sha1-K2TIozAE1UuGmMdtWFp3zrYdoy8=", + "requires": { + "detect-indent": "^5.0.0", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "pify": "^3.0.0", + "sort-keys": "^2.0.0", + "write-file-atomic": "^2.0.0" + } + } + } + }, + "ws": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.0.tgz", + "integrity": "sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg==", + "requires": { + "async-limiter": "^1.0.0" + } + }, + "xml2js": { + "version": "0.4.22", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/xml2js/-/xml2js-0.4.22.tgz", + "integrity": "sha1-T6LYRuyAMjfehvMKqbX3C2YA3gI=", + "requires": { + "sax": ">=0.6.0", + "util.promisify": "~1.0.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha1-vpuuHIoEbnazESdyY0fQrXACvrM=" + }, + "xmldom": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", + "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=" + }, + "xpath": { + "version": "0.0.27", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz", + "integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==" + }, + "xpath.js": { + "version": "1.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/xpath.js/-/xpath.js-1.1.0.tgz", + "integrity": "sha1-OBakTtS7NSCRCD0AKjg91RBKX/E=" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha1-u3J3n1+kZRhrH0OPZ0+jR/2121Q=" + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha1-le+U+F7MgdAHwmThkKEg8KPIVms=" + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha1-27fa+b/YusmrRev2ArjLrQ1dCP0=" + }, + "yargs": { + "version": "14.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/yargs/-/yargs-14.2.0.tgz", + "integrity": "sha1-8RapJCxO2GaHkLQHWbSQbCdudsM=", + "requires": { + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^15.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc=" + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha1-SRafHXmTQwZG2mHsxa41XCHJe3M=", + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha1-2+w7OrdZdYBxtY/ln8QYca8hQA4=", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha1-qgeniMwxUck5tRMfY1cPDdIAlTc=", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha1-Mi1poFwCZLJZl9n0DNiokasAZKQ=", + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha1-yyhoVA4xPWHeWPr741zpAE1VQOY=" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha1-InZ74htirxCBV0MG9prFG2IgOWE=", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "15.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/yargs-parser/-/yargs-parser-15.0.0.tgz", + "integrity": "sha1-zdepdJDsg2GV9Z8/Tb5eqej3Xwg=", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha1-48mzFWnhBoEd8kL3FXJaH0xJQyA=" + } + } + }, + "yargs-unparser": { + "version": "1.6.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha1-7yXCx2n/a9CeSw+dfGBfsnhG6p8=", + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.15", + "yargs": "^13.3.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc=" + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha1-48mzFWnhBoEd8kL3FXJaH0xJQyA=" + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha1-SRafHXmTQwZG2mHsxa41XCHJe3M=", + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha1-2+w7OrdZdYBxtY/ln8QYca8hQA4=", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha1-qgeniMwxUck5tRMfY1cPDdIAlTc=", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha1-Mi1poFwCZLJZl9n0DNiokasAZKQ=", + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha1-yyhoVA4xPWHeWPr741zpAE1VQOY=" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha1-InZ74htirxCBV0MG9prFG2IgOWE=", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "yargs": { + "version": "13.3.0", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha1-TGV6VeB+Xyz5R/ijZlZ8BKDe3IM=", + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha1-0mBYUyqgbTZf4JH2ofwGsvfl7KA=", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=" + } + } } diff --git a/package.json b/package.json index 366ce117a0..8cc3c22ef2 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,8 @@ "build-docs": "lerna run build-docs", "eslint": "eslint ./libraries/*/src/*.ts ./libraries/*/src/**/*.ts", "eslint-fix": "eslint ./libraries/*/src/*.ts ./libraries/*/src/**/*.ts --fix", - "set-dependency-versions": "node tools/util/updateDependenciesInPackageJsons.js ./libraries ^${Version} botbuilder botbuilder-choices botbuilder-dialogs botbuilder-dialogs-adaptive botbuilder-core botbuilder-prompts botbuilder-testing botframework-connector botframework-config botframework-schema testbot && node tools/util/updateDependenciesInPackageJsons.js ./transcripts ^${Version} botbuilder botbuilder-ai botbuilder-dialogs botbuilder-testing", - "set-samples-dependency-versions": "node tools/util/updateDependenciesInPackageJsons.js ./samples ^${Version} botbuilder botbuilder-choices botbuilder-dialogs botbuilder-dialogs-adaptive botbuilder-core botbuilder-prompts botbuilder-testing botframework-connector botframework-config botframework-schema testbot", - "update-versions": "lerna run set-version && npm run set-dependency-versions && npm run set-samples-dependency-versions" + "set-dependency-versions": "node tools/util/updateDependenciesInPackageJsons.js ./libraries ^${Version} botframework-streaming botbuilder botbuilder-choices botbuilder-dialogs botbuilder-dialogs-adaptive botbuilder-core botbuilder-prompts botbuilder-testing botframework-connector botframework-config botframework-schema testbot && node tools/util/updateDependenciesInPackageJsons.js ./transcripts ^${Version} botframework-streaming botbuilder botbuilder-ai botbuilder-dialogs botbuilder-testing", + "update-versions": "lerna run set-version && npm run set-dependency-versions" }, "dependencies": { "@azure/ms-rest-js": "^1.8.13", @@ -32,11 +31,9 @@ "read-text-file": "^1.1.0", "replace-in-file": "^4.1.0", "sinon": "^7.3.2", - "tslint": "^5.18.0", - "tslint-microsoft-contrib": "^6.2.0", - "typedoc": "^0.14.2", + "typedoc": "^0.15.0", "typedoc-plugin-external-module-name": "^2.1.0", - "typedoc-plugin-markdown": "^2.0.6", + "typedoc-plugin-markdown": "^2.2.10", "typescript": "^3.5.2" }, "nyc": { diff --git a/recognizers-text/Utterance Changes/DateTime/Differences.md b/recognizers-text/Utterance Changes/DateTime/Differences.md new file mode 100644 index 0000000000..e1a01df62c --- /dev/null +++ b/recognizers-text/Utterance Changes/DateTime/Differences.md @@ -0,0 +1,9 @@ +## Changes +### V1.1.4 +**DateTime - Recognize** + + Changed Recognized inputs: + - [good now](https://github.com/microsoft/botbuilder-js/blob/72c3b6c4771090226cbf7daabe9c75409f9f4fc9/recognizers-text/Utterance%20Changes/DateTime/datetime-prompt-differences.json#L3) + - [now](https://github.com/microsoft/botbuilder-js/blob/72c3b6c4771090226cbf7daabe9c75409f9f4fc9/recognizers-text/Utterance%20Changes/DateTime/datetime-prompt-differences.json#L40) + - [previously](https://github.com/microsoft/botbuilder-js/blob/72c3b6c4771090226cbf7daabe9c75409f9f4fc9/recognizers-text/Utterance%20Changes/DateTime/datetime-prompt-differences.json#L77) + - [recently](https://github.com/microsoft/botbuilder-js/blob/72c3b6c4771090226cbf7daabe9c75409f9f4fc9/recognizers-text/Utterance%20Changes/DateTime/datetime-prompt-differences.json#L114) diff --git a/recognizers-text/Utterance Changes/DateTime/datetime-prompt-differences.json b/recognizers-text/Utterance Changes/DateTime/datetime-prompt-differences.json new file mode 100644 index 0000000000..fc948d3a09 --- /dev/null +++ b/recognizers-text/Utterance Changes/DateTime/datetime-prompt-differences.json @@ -0,0 +1,150 @@ +[ + { + "input": "good now", + "was": [ + { + "start": 5, + "end": 7, + "resolution": { + "values": [ + { + "timex": "PRESENT_REF", + "type": "datetime", + "value": "2019-09-03 14:26:43" + } + ] + }, + "text": "now", + "typeName": "datetimeV2.datetime" + } + ], + "now": [ + { + "start": 5, + "end": 7, + "resolution": { + "values": [ + { + "timex": "PRESENT_REF", + "type": "datetime", + "value": "2019-09-03 14:59:20" + } + ] + }, + "text": "now", + "typeName": "datetimeV2.datetime" + } + ] + }, + { + "input": "now", + "was": [ + { + "start": 0, + "end": 2, + "resolution": { + "values": [ + { + "timex": "PRESENT_REF", + "type": "datetime", + "value": "2019-09-03 14:29:28" + } + ] + }, + "text": "now", + "typeName": "datetimeV2.datetime" + } + ], + "now": [ + { + "start": 0, + "end": 2, + "resolution": { + "values": [ + { + "timex": "PRESENT_REF", + "type": "datetime", + "value": "2019-09-03 15:01:39" + } + ] + }, + "text": "now", + "typeName": "datetimeV2.datetime" + } + ] + }, + { + "input": "previously", + "was": [ + { + "start": 0, + "end": 9, + "resolution": { + "values": [ + { + "timex": "PAST_REF", + "type": "datetime", + "value": "2019-09-03 14:30:47" + } + ] + }, + "text": "previously", + "typeName": "datetimeV2.datetime" + } + ], + "now": [ + { + "start": 0, + "end": 9, + "resolution": { + "values": [ + { + "timex": "PAST_REF", + "type": "datetime", + "value": "2019-09-03 15:02:49" + } + ] + }, + "text": "previously", + "typeName": "datetimeV2.datetime" + } + ] + }, + { + "input": "recently", + "was": [ + { + "start": 0, + "end": 7, + "resolution": { + "values": [ + { + "timex": "PAST_REF", + "type": "datetime", + "value": "2019-09-03 14:31:13" + } + ] + }, + "text": "recently", + "typeName": "datetimeV2.datetime" + } + ], + "now": [ + { + "start": 0, + "end": 7, + "resolution": { + "values": [ + { + "timex": "PAST_REF", + "type": "datetime", + "value": "2019-09-03 15:03:16" + } + ] + }, + "text": "recently", + "typeName": "datetimeV2.datetime" + } + ] + } +] \ No newline at end of file diff --git a/samples/01. sendActivity/package.json b/samples/01. sendActivity/package.json index 2df6f16cc2..30dfdcaebd 100644 --- a/samples/01. sendActivity/package.json +++ b/samples/01. sendActivity/package.json @@ -4,7 +4,6 @@ "description": "", "main": "./lib/index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", "start": "node ./lib/index.js" }, "author": "", diff --git a/samples/02. textInput/package.json b/samples/02. textInput/package.json index 5a36eecc82..41e32369be 100644 --- a/samples/02. textInput/package.json +++ b/samples/02. textInput/package.json @@ -4,7 +4,6 @@ "description": "", "main": "./lib/index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", "start": "node ./lib/index.js" }, "author": "", diff --git a/samples/03. ifCondition/package.json b/samples/03. ifCondition/package.json index 5221b26e9d..53913b56be 100644 --- a/samples/03. ifCondition/package.json +++ b/samples/03. ifCondition/package.json @@ -4,7 +4,6 @@ "description": "", "main": "./lib/index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", "start": "node ./lib/index.js" }, "author": "", diff --git a/samples/04. onIntent/package.json b/samples/04. onIntent/package.json index 745e1c59ba..e34be9302c 100644 --- a/samples/04. onIntent/package.json +++ b/samples/04. onIntent/package.json @@ -4,7 +4,6 @@ "description": "", "main": "./lib/index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", "start": "node ./lib/index.js" }, "author": "", diff --git a/samples/05. beginDialog/lib/index.js b/samples/05. beginDialog/lib/index.js index 6ba38cb7d2..7c7d71ded2 100644 --- a/samples/05. beginDialog/lib/index.js +++ b/samples/05. beginDialog/lib/index.js @@ -54,12 +54,12 @@ const askNameDialog = new botbuilder_dialogs_adaptive_1.AdaptiveDialog('AskNameD new botbuilder_dialogs_adaptive_1.SendActivity(`Hi {user.name}. It's nice to meet you.`), new botbuilder_dialogs_adaptive_1.EndDialog() ]); -dialogs.addDialog(askNameDialog); +dialogs.actions.push(askNameDialog); const tellJokeDialog = new botbuilder_dialogs_adaptive_1.AdaptiveDialog('TellJokeDialog', [ new botbuilder_dialogs_adaptive_1.SendActivity(`Why did the 🐔 cross the 🛣️?`), new botbuilder_dialogs_adaptive_1.EndTurn(), new botbuilder_dialogs_adaptive_1.SendActivity(`To get to the other side...`), new botbuilder_dialogs_adaptive_1.EndDialog() ]); -dialogs.addDialog(tellJokeDialog); +dialogs.actions.push(tellJokeDialog); //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/samples/05. beginDialog/lib/index.js.map b/samples/05. beginDialog/lib/index.js.map index f0d6b02764..0f55b3c0b7 100644 --- a/samples/05. beginDialog/lib/index.js.map +++ b/samples/05. beginDialog/lib/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,4DAA4D;AAC5D,kCAAkC;;AAElC,mCAAmC;AACnC,2CAAgE;AAChE,6EAAiL;AACjL,2DAAmD;AAEnD,kBAAkB;AAClB,oGAAoG;AACpG,MAAM,OAAO,GAAG,IAAI,gCAAmB,CAAC;IACpC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc;IACjC,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,oBAAoB;CAChD,CAAC,CAAC;AAEH,sBAAsB;AACtB,MAAM,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;AACtC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,EAAE,GAAG,EAAE;IAC7D,OAAO,CAAC,GAAG,CAAC,KAAK,MAAM,CAAC,IAAI,iBAAiB,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;IAC3D,OAAO,CAAC,GAAG,CAAC,oEAAoE,CAAC,CAAC;IAClF,OAAO,CAAC,GAAG,CAAC,+DAA+D,CAAC,CAAC;AACjF,CAAC,CAAC,CAAC;AAEH,sDAAsD;AACtD,MAAM,GAAG,GAAG,IAAI,kCAAa,EAAE,CAAC;AAChC,GAAG,CAAC,OAAO,GAAG,IAAI,0BAAa,EAAE,CAAC;AAElC,kCAAkC;AAClC,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IACtC,OAAO,CAAC,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QAChD,yBAAyB;QACzB,MAAM,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC;AAEH,8BAA8B;AAC9B,MAAM,OAAO,GAAG,IAAI,4CAAc,EAAE,CAAC;AACrC,GAAG,CAAC,UAAU,GAAG,OAAO,CAAC;AAEzB,mGAAmG;AACnG,QAAQ;AACR,mGAAmG;AAEnG,OAAO,CAAC,UAAU,GAAG,IAAI,8CAAgB,EAAE,CAAC,SAAS,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;AAEpF,uBAAuB;AACvB,OAAO,CAAC,OAAO,CAAC,IAAI,sCAAQ,CAAC,aAAa,EAAE;IACxC,IAAI,yCAAW,CAAC,gBAAgB,CAAC;CACpC,CAAC,CAAC,CAAC;AAEJ,yBAAyB;AACzB,OAAO,CAAC,OAAO,CAAC,IAAI,6CAAe,CAAC;IAChC,IAAI,yCAAW,CAAC,eAAe,CAAC;CACnC,CAAC,CAAC,CAAC;AAGJ,mGAAmG;AACnG,gBAAgB;AAChB,mGAAmG;AAEnG,MAAM,aAAa,GAAG,IAAI,4CAAc,CAAC,eAAe,EAAE;IACtD,IAAI,yCAAW,CAAC,mBAAmB,EAAE;QACjC,IAAI,uCAAS,CAAC,WAAW,EAAE,uBAAuB,CAAC;KACtD,CAAC;IACF,IAAI,0CAAY,CAAC,wCAAwC,CAAC;IAC1D,IAAI,uCAAS,EAAE;CAClB,CAAC,CAAC;AACH,OAAO,CAAC,SAAS,CAAC,aAAa,CAAC,CAAA;AAEhC,MAAM,cAAc,GAAG,IAAI,4CAAc,CAAC,gBAAgB,EAAC;IACvD,IAAI,0CAAY,CAAC,+BAA+B,CAAC;IACjD,IAAI,qCAAO,EAAE;IACb,IAAI,0CAAY,CAAC,6BAA6B,CAAC;IAC/C,IAAI,uCAAS,EAAE;CAClB,CAAC,CAAC;AACH,OAAO,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,4DAA4D;AAC5D,kCAAkC;;AAElC,mCAAmC;AACnC,2CAAgE;AAChE,6EAAiL;AACjL,2DAAmD;AAEnD,kBAAkB;AAClB,oGAAoG;AACpG,MAAM,OAAO,GAAG,IAAI,gCAAmB,CAAC;IACpC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc;IACjC,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,oBAAoB;CAChD,CAAC,CAAC;AAEH,sBAAsB;AACtB,MAAM,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;AACtC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,EAAE,GAAG,EAAE;IAC7D,OAAO,CAAC,GAAG,CAAC,KAAK,MAAM,CAAC,IAAI,iBAAiB,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;IAC3D,OAAO,CAAC,GAAG,CAAC,oEAAoE,CAAC,CAAC;IAClF,OAAO,CAAC,GAAG,CAAC,+DAA+D,CAAC,CAAC;AACjF,CAAC,CAAC,CAAC;AAEH,sDAAsD;AACtD,MAAM,GAAG,GAAG,IAAI,kCAAa,EAAE,CAAC;AAChC,GAAG,CAAC,OAAO,GAAG,IAAI,0BAAa,EAAE,CAAC;AAElC,kCAAkC;AAClC,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IACtC,OAAO,CAAC,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QAChD,yBAAyB;QACzB,MAAM,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC;AAEH,8BAA8B;AAC9B,MAAM,OAAO,GAAG,IAAI,4CAAc,EAAE,CAAC;AACrC,GAAG,CAAC,UAAU,GAAG,OAAO,CAAC;AAEzB,mGAAmG;AACnG,QAAQ;AACR,mGAAmG;AAEnG,OAAO,CAAC,UAAU,GAAG,IAAI,8CAAgB,EAAE,CAAC,SAAS,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;AAEpF,uBAAuB;AACvB,OAAO,CAAC,OAAO,CAAC,IAAI,sCAAQ,CAAC,aAAa,EAAE;IACxC,IAAI,yCAAW,CAAC,gBAAgB,CAAC;CACpC,CAAC,CAAC,CAAC;AAEJ,yBAAyB;AACzB,OAAO,CAAC,OAAO,CAAC,IAAI,6CAAe,CAAC;IAChC,IAAI,yCAAW,CAAC,eAAe,CAAC;CACnC,CAAC,CAAC,CAAC;AAGJ,mGAAmG;AACnG,gBAAgB;AAChB,mGAAmG;AAEnG,MAAM,aAAa,GAAG,IAAI,4CAAc,CAAC,eAAe,EAAE;IACtD,IAAI,yCAAW,CAAC,mBAAmB,EAAE;QACjC,IAAI,uCAAS,CAAC,WAAW,EAAE,uBAAuB,CAAC;KACtD,CAAC;IACF,IAAI,0CAAY,CAAC,wCAAwC,CAAC;IAC1D,IAAI,uCAAS,EAAE;CAClB,CAAC,CAAC;AACH,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;AAEpC,MAAM,cAAc,GAAG,IAAI,4CAAc,CAAC,gBAAgB,EAAC;IACvD,IAAI,0CAAY,CAAC,+BAA+B,CAAC;IACjD,IAAI,qCAAO,EAAE;IACb,IAAI,0CAAY,CAAC,6BAA6B,CAAC;IAC/C,IAAI,uCAAS,EAAE;CAClB,CAAC,CAAC;AACH,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC"} \ No newline at end of file diff --git a/samples/05. beginDialog/package.json b/samples/05. beginDialog/package.json index 4102eb2507..7d649877be 100644 --- a/samples/05. beginDialog/package.json +++ b/samples/05. beginDialog/package.json @@ -4,7 +4,6 @@ "description": "", "main": "./lib/index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", "start": "node ./lib/index.js" }, "author": "", diff --git a/samples/05. beginDialog/src/index.ts b/samples/05. beginDialog/src/index.ts index 9228fa335a..d99c21b934 100644 --- a/samples/05. beginDialog/src/index.ts +++ b/samples/05. beginDialog/src/index.ts @@ -65,7 +65,7 @@ const askNameDialog = new AdaptiveDialog('AskNameDialog', [ new SendActivity(`Hi {user.name}. It's nice to meet you.`), new EndDialog() ]); -dialogs.addDialog(askNameDialog) +dialogs.actions.push(askNameDialog); const tellJokeDialog = new AdaptiveDialog('TellJokeDialog',[ new SendActivity(`Why did the 🐔 cross the 🛣️?`), @@ -73,4 +73,4 @@ const tellJokeDialog = new AdaptiveDialog('TellJokeDialog',[ new SendActivity(`To get to the other side...`), new EndDialog() ]); -dialogs.addDialog(tellJokeDialog); \ No newline at end of file +dialogs.actions.push(tellJokeDialog); \ No newline at end of file diff --git a/samples/06. codeAction/lib/index.js b/samples/06. codeAction/lib/index.js index d12bfda072..f0bbea87a4 100644 --- a/samples/06. codeAction/lib/index.js +++ b/samples/06. codeAction/lib/index.js @@ -33,8 +33,8 @@ server.post('/api/messages', (req, res) => { const dialogs = new botbuilder_dialogs_adaptive_1.AdaptiveDialog(); bot.rootDialog = dialogs; // Add a default rule for handling incoming messages -dialogs.addRule(new botbuilder_dialogs_adaptive_1.UnknownIntentRule([ - new botbuilder_dialogs_adaptive_1.CodeStep(async (dc) => { +dialogs.addRule(new botbuilder_dialogs_adaptive_1.OnUnknownIntent([ + new botbuilder_dialogs_adaptive_1.CodeAction(async (dc) => { const count = dc.state.getValue('conversation.count') || 0; dc.state.setValue('conversation.count', count + 1); return await dc.endDialog(); diff --git a/samples/06. codeAction/lib/index.js.map b/samples/06. codeAction/lib/index.js.map index d9833f6540..ff49bbda59 100644 --- a/samples/06. codeAction/lib/index.js.map +++ b/samples/06. codeAction/lib/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,4DAA4D;AAC5D,kCAAkC;;AAElC,mCAAmC;AACnC,2CAAgE;AAChE,6EAAwG;AACxG,2DAAmD;AAEnD,sBAAsB;AACtB,MAAM,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;AACtC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,EAAE,GAAG,EAAE;IAC7D,OAAO,CAAC,GAAG,CAAC,KAAK,MAAM,CAAC,IAAI,iBAAiB,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;IAC3D,OAAO,CAAC,GAAG,CAAC,oEAAoE,CAAC,CAAC;IAClF,OAAO,CAAC,GAAG,CAAC,+DAA+D,CAAC,CAAC;AACjF,CAAC,CAAC,CAAC;AAEH,kBAAkB;AAClB,oGAAoG;AACpG,MAAM,OAAO,GAAG,IAAI,gCAAmB,CAAC;IACpC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc;IACjC,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,oBAAoB;CAChD,CAAC,CAAC;AAEH,sDAAsD;AACtD,MAAM,GAAG,GAAG,IAAI,kCAAa,EAAE,CAAC;AAChC,GAAG,CAAC,OAAO,GAAG,IAAI,0BAAa,EAAE,CAAC;AAElC,kCAAkC;AAClC,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IACtC,OAAO,CAAC,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QAChD,yBAAyB;QACzB,MAAM,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC;AAEH,8BAA8B;AAC9B,MAAM,OAAO,GAAG,IAAI,4CAAc,EAAE,CAAC;AACrC,GAAG,CAAC,UAAU,GAAG,OAAO,CAAC;AAEzB,oDAAoD;AACpD,OAAO,CAAC,OAAO,CAAC,IAAI,+CAAiB,CAAC;IAClC,IAAI,sCAAQ,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACtB,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC;QAC3D,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,oBAAoB,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;QACnD,OAAO,MAAM,EAAE,CAAC,SAAS,EAAE,CAAC;IAChC,CAAC,CAAC;IACF,IAAI,0CAAY,CAAC,6CAA6C,CAAC;CAClE,CAAC,CAAC,CAAC"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,4DAA4D;AAC5D,kCAAkC;;AAElC,mCAAmC;AACnC,2CAAgE;AAChE,6EAAwG;AACxG,2DAAmD;AAEnD,sBAAsB;AACtB,MAAM,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;AACtC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,EAAE,GAAG,EAAE;IAC7D,OAAO,CAAC,GAAG,CAAC,KAAK,MAAM,CAAC,IAAI,iBAAiB,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;IAC3D,OAAO,CAAC,GAAG,CAAC,oEAAoE,CAAC,CAAC;IAClF,OAAO,CAAC,GAAG,CAAC,+DAA+D,CAAC,CAAC;AACjF,CAAC,CAAC,CAAC;AAEH,kBAAkB;AAClB,oGAAoG;AACpG,MAAM,OAAO,GAAG,IAAI,gCAAmB,CAAC;IACpC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc;IACjC,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,oBAAoB;CAChD,CAAC,CAAC;AAEH,sDAAsD;AACtD,MAAM,GAAG,GAAG,IAAI,kCAAa,EAAE,CAAC;AAChC,GAAG,CAAC,OAAO,GAAG,IAAI,0BAAa,EAAE,CAAC;AAElC,kCAAkC;AAClC,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IACtC,OAAO,CAAC,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QAChD,yBAAyB;QACzB,MAAM,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC;AAEH,8BAA8B;AAC9B,MAAM,OAAO,GAAG,IAAI,4CAAc,EAAE,CAAC;AACrC,GAAG,CAAC,UAAU,GAAG,OAAO,CAAC;AAEzB,oDAAoD;AACpD,OAAO,CAAC,OAAO,CAAC,IAAI,6CAAe,CAAC;IAChC,IAAI,wCAAU,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACxB,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC;QAC3D,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,oBAAoB,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;QACnD,OAAO,MAAM,EAAE,CAAC,SAAS,EAAE,CAAC;IAChC,CAAC,CAAC;IACF,IAAI,0CAAY,CAAC,6CAA6C,CAAC;CAClE,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/samples/06. codeAction/package.json b/samples/06. codeAction/package.json index 4937cbe1a7..c7012c3dae 100644 --- a/samples/06. codeAction/package.json +++ b/samples/06. codeAction/package.json @@ -4,7 +4,6 @@ "description": "", "main": "./lib/index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", "start": "node ./lib/index.js" }, "author": "", diff --git a/samples/10. sidecarDebugging/package.json b/samples/10. sidecarDebugging/package.json index 7cb9e74e82..3119f8e9ca 100644 --- a/samples/10. sidecarDebugging/package.json +++ b/samples/10. sidecarDebugging/package.json @@ -4,7 +4,6 @@ "description": "", "main": "./lib/index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", "start": "node --inspect=6009 ./lib/index.js" }, "author": "", diff --git a/samples/30. stateMachineDialog/package.json b/samples/30. stateMachineDialog/package.json index 4525fc89f4..fec2ddb58b 100644 --- a/samples/30. stateMachineDialog/package.json +++ b/samples/30. stateMachineDialog/package.json @@ -4,7 +4,6 @@ "description": "", "main": "./lib/index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", "start": "node ./lib/index.js" }, "author": "", diff --git a/samples/50. todo-bot/package.json b/samples/50. todo-bot/package.json index 8caaf3653c..49de63ab8d 100644 --- a/samples/50. todo-bot/package.json +++ b/samples/50. todo-bot/package.json @@ -4,7 +4,6 @@ "description": "", "main": "./lib/index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", "start": "node ./lib/index.js" }, "author": "",