botbuilder-dialogs-adaptive
Version:
Rule system for the Microsoft BotBuilder dialog system.
413 lines (364 loc) • 13 kB
text/typescript
/**
* @module botbuilder-dialogs-adaptive
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { StatusCodes } from 'botbuilder-core';
import fetch, { FetchError, Response, Headers } from 'node-fetch';
import { Activity } from 'botbuilder';
import { BoolProperty, EnumProperty, StringProperty, UnknownProperty } from '../properties';
import { evaluateExpression } from '../jsonExtensions';
import {
BoolExpression,
BoolExpressionConverter,
EnumExpression,
EnumExpressionConverter,
StringExpression,
StringExpressionConverter,
ValueExpression,
ValueExpressionConverter,
} from 'adaptive-expressions';
import {
Converter,
ConverterFactory,
DialogContext,
Dialog,
DialogTurnResult,
DialogConfiguration,
} from 'botbuilder-dialogs';
type HeadersInput = Record<string, string>;
type HeadersOutput = Record<string, StringExpression>;
/**
* [HeadersInput](xref:botbuilder-dialogs-adaptive.HeadersInput) or [HeadersOutput](xref:botbuilder-dialogs-adaptive.HeadersOutput) to [HttpHeader](xref:botbuilder-dialogs-adaptive.HttpHeader) converter.
*/
class HttpHeadersConverter implements Converter<HeadersInput, HeadersOutput> {
/**
* Converts a [HeadersInput](xref:botbuilder-dialogs-adaptive.HeadersInput) or [HeadersOutput](xref:botbuilder-dialogs-adaptive.HeadersOutput) to [HttpHeader](xref:botbuilder-dialogs-adaptive.HttpHeader).
*
* @param value [HeadersInput](xref:botbuilder-dialogs-adaptive.HeadersInput) or [HeadersOutput](xref:botbuilder-dialogs-adaptive.HeadersOutput) to convert.
* @returns The [HttpHeader](xref:botbuilder-dialogs-adaptive.HttpHeader).
*/
convert(value: HeadersInput | HeadersOutput): HeadersOutput {
return Object.entries(value).reduce((headers, [key, value]) => {
return {
...headers,
[key]: value instanceof StringExpression ? value : new StringExpression(value),
};
}, {});
}
}
export enum ResponsesTypes {
/**
* No response expected
*/
None,
/**
* Plain JSON response
*/
Json,
/**
* JSON Activity object to send to the user
*/
Activity,
/**
* Json Array of activity objects to send to the user
*/
Activities,
/**
* Binary data parsing from http response content
*/
Binary,
}
export enum HttpMethod {
/**
* Http GET
*/
GET = 'GET',
/**
* Http POST
*/
POST = 'POST',
/**
* Http PATCH
*/
PATCH = 'PATCH',
/**
* Http PUT
*/
PUT = 'PUT',
/**
* Http DELETE
*/
DELETE = 'DELETE',
/**
* Http HEAD
*/
HEAD = 'HEAD',
}
/**
* Result data of HTTP operation.
*/
export class Result {
/**
* Initialize a new instance of Result class.
*
* @param headers Response headers.
*/
constructor(headers?: Headers) {
if (headers) {
headers.forEach((value: string, name: string): void => {
this.headers[name] = value;
});
}
}
/**
* The status code from the response to HTTP operation.
*/
statusCode?: number;
/**
* The reason phrase from the response to HTTP operation.
*/
reasonPhrase?: string;
/**
* The headers from the response to HTTP operation.
*/
headers?: { [key: string]: string } = {};
/**
* The content body from the response to HTTP operation.
*/
content?: any;
}
export interface HttpRequestConfiguration extends DialogConfiguration {
method?: HttpMethod;
contentType?: StringProperty;
url?: StringProperty;
headers?: HeadersInput | HeadersOutput;
body?: UnknownProperty;
responseType?: EnumProperty<ResponsesTypes>;
resultProperty: StringProperty;
disabled?: BoolProperty;
}
/**
* Action for performing an `HttpRequest`.
*/
export class HttpRequest<O extends object = {}> extends Dialog<O> implements HttpRequestConfiguration {
static $kind = 'Microsoft.HttpRequest';
constructor();
/**
* Initializes a new instance of the [HttpRequest](xref:botbuilder-dialogs-adaptive.HttpRequest) class.
*
* @param method The [HttpMethod](xref:botbuilder-dialogs-adaptive.HttpMethod), for example POST, GET, DELETE or PUT.
* @param url URL for the request.
* @param headers The headers of the request.
* @param body The raw body of the request.
*/
constructor(method: HttpMethod, url: string, headers: { [key: string]: string }, body: any);
/**
* Initializes a new instance of the [HttpRequest](xref:botbuilder-dialogs-adaptive.HttpRequest) class.
*
* @param method Optional. The [HttpMethod](xref:botbuilder-dialogs-adaptive.HttpMethod), for example POST, GET, DELETE or PUT.
* @param url Optional. URL for the request.
* @param headers Optional. The headers of the request.
* @param body Optional. The raw body of the request.
*/
constructor(method?: HttpMethod, url?: string, headers?: { [key: string]: string }, body?: any) {
super();
this.method = method || HttpMethod.GET;
this.url = new StringExpression(url);
if (headers) {
this.headers = {};
for (const key in headers) {
this.headers[key] = new StringExpression(headers[key]);
}
}
this.body = new ValueExpression(body);
}
/**
* Http Method
*/
method?: HttpMethod = HttpMethod.GET;
/**
* Content type of request body
*/
contentType?: StringExpression = new StringExpression('application/json');
/**
* Http Url
*/
url?: StringExpression;
/**
* Http Headers
*/
headers?: { [key: string]: StringExpression } = {};
/**
* Http Body
*/
body?: ValueExpression;
/**
* The response type of the response
*/
responseType?: EnumExpression<ResponsesTypes> = new EnumExpression<ResponsesTypes>(ResponsesTypes.Json);
/**
* Gets or sets the property expression to store the HTTP response in.
*/
resultProperty: StringExpression = new StringExpression('turn.results');
/**
* An optional expression which if is true will disable this action.
*/
disabled?: BoolExpression;
/**
* @param property The key of the conditional selector configuration.
* @returns The converter for the selector configuration.
*/
getConverter(property: keyof HttpRequestConfiguration): Converter | ConverterFactory {
switch (property) {
case 'contentType':
return new StringExpressionConverter();
case 'url':
return new StringExpressionConverter();
case 'headers':
return new HttpHeadersConverter();
case 'body':
return new ValueExpressionConverter();
case 'responseType':
return new EnumExpressionConverter<ResponsesTypes>(ResponsesTypes);
case 'resultProperty':
return new StringExpressionConverter();
case 'disabled':
return new BoolExpressionConverter();
default:
return super.getConverter(property);
}
}
/**
* Starts a new [Dialog](xref:botbuilder-dialogs.Dialog) and pushes it onto the dialog stack.
*
* @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation.
* @param _options Optional. Initial information to pass to the dialog.
* @returns A `Promise` representing the asynchronous operation.
*/
async beginDialog(dc: DialogContext, _options?: O): Promise<DialogTurnResult> {
if (this.disabled && this.disabled.getValue(dc.state)) {
return await dc.endDialog();
}
const instanceUrl = this.url.getValue(dc.state);
const instanceMethod = this.method.toString();
const instanceHeaders = {};
for (let key in this.headers) {
if (key.toLowerCase() === 'content-type') {
key = 'Content-Type';
}
instanceHeaders[key] = this.headers[key].getValue(dc.state);
}
const contentType = this.contentType.getValue(dc.state) || 'application/json';
instanceHeaders['Content-Type'] = contentType;
let instanceBody: string;
let traceInfo;
try {
const body = evaluateExpression(dc.state, this.body);
if (body) {
if (typeof body === 'string') {
instanceBody = body;
} else {
instanceBody = JSON.stringify(Object.assign({}, body));
}
}
traceInfo = {
request: {
method: instanceMethod,
url: instanceUrl,
headers: instanceHeaders,
content: instanceBody,
},
response: undefined,
};
let response: Response;
switch (this.method) {
case HttpMethod.DELETE:
case HttpMethod.GET:
case HttpMethod.HEAD:
response = await fetch(instanceUrl, {
method: instanceMethod,
headers: instanceHeaders,
});
break;
case HttpMethod.PUT:
case HttpMethod.PATCH:
case HttpMethod.POST:
response = await fetch(instanceUrl, {
method: instanceMethod,
headers: instanceHeaders,
body: instanceBody,
});
break;
}
const result = new Result(response.headers);
result.statusCode = response.status;
result.reasonPhrase = response.statusText;
switch (this.responseType.getValue(dc.state)) {
case ResponsesTypes.Activity:
result.content = await response.json();
dc.context.sendActivity(result.content as Activity);
break;
case ResponsesTypes.Activities:
result.content = await response.json();
dc.context.sendActivities(result.content as Activity[]);
break;
case ResponsesTypes.Json: {
const content = await response.text();
try {
result.content = JSON.parse(content);
} catch {
result.content = content;
}
break;
}
case ResponsesTypes.Binary: {
const buffer = await response.arrayBuffer();
result.content = new Uint8Array(buffer);
break;
}
case ResponsesTypes.None:
default:
break;
}
return await this.endDialogWithResult(dc, result, traceInfo);
} catch (err) {
if (err instanceof FetchError) {
const result = new Result();
result.content = err.message;
result.statusCode = StatusCodes.NOT_FOUND;
return await this.endDialogWithResult(dc, result, traceInfo);
} else {
throw err;
}
}
}
/**
* Writes Trace Activity for the http request and response values and returns the actionResult as the result of this operation.
*
* @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation.
* @param result Value returned from the dialog that was called. The type
* of the value returned is dependent on the child dialog.
* @param traceInfo Trace information to be written.
* @returns A `Promise` representing the asynchronous operation.
*/
private async endDialogWithResult(dc: DialogContext, result: Result, traceInfo: any): Promise<DialogTurnResult> {
traceInfo.response = result;
// Write trace activity for http request and response values.
await dc.context.sendTraceActivity('HttpRequest', traceInfo, 'Microsoft.HttpRequest', this.id);
if (this.resultProperty) {
dc.state.setValue(this.resultProperty.getValue(dc.state), result);
}
return await dc.endDialog(result);
}
/**
* @protected
* Builds the compute Id for the [Dialog](xref:botbuilder-dialogs.Dialog).
* @returns A `string` representing the compute Id.
*/
protected onComputeId(): string {
return `HttpRequest[${this.method} ${this.url}]`;
}
}