botbuilder-core
Version:
Core components for Microsoft Bot Builder. Components in this library can run either in a browser or on the server.
487 lines (432 loc) • 15.4 kB
text/typescript
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Activity, SuggestedActions, Attachment, ActivityTypes, ActionTypes, CardAction } from 'botframework-schema';
import { MessageFactory } from './messageFactory';
import { CardFactory } from './cardFactory';
/**
* The ActivityFactory
* to generate text and then uses simple markdown semantics like chatdown to create Activity.
*/
export class ActivityFactory {
private static readonly lgType = 'lgType';
private static adaptiveCardType: string = CardFactory.contentTypes.adaptiveCard;
private static readonly genericCardTypeMapping: Map<string, string> = new Map<string, string>([
['herocard', CardFactory.contentTypes.heroCard],
['thumbnailcard', CardFactory.contentTypes.thumbnailCard],
['audiocard', CardFactory.contentTypes.audioCard],
['videocard', CardFactory.contentTypes.videoCard],
['animationcard', CardFactory.contentTypes.animationCard],
['signincard', CardFactory.contentTypes.signinCard],
['oauthcard', CardFactory.contentTypes.oauthCard],
['receiptcard', CardFactory.contentTypes.receiptCard],
]);
private static readonly activityProperties: string[] = [
'type',
'id',
'timestamp',
'localTimestamp',
'localTimezone',
'callerId',
'serviceUrl',
'channelId',
'from',
'conversation',
'recipient',
'textFormat',
'attachmentLayout',
'membersAdded',
'membersRemoved',
'reactionsAdded',
'reactionsRemoved',
'topicName',
'historyDisclosed',
'locale',
'text',
'speak',
'inputHint',
'summary',
'suggestedActions',
'attachments',
'entities',
'channelData',
'action',
'replyToId',
'label',
'valueType',
'value',
'name',
'typrelatesToe',
'code',
'expiration',
'importance',
'deliveryMode',
'listenFor',
'textHighlights',
'semanticAction',
];
private static readonly cardActionProperties: string[] = [
'type',
'title',
'image',
'text',
'displayText',
'value',
'channelData',
];
private static readonly attachmentProperties: string[] = [
'contentType',
'contentUrl',
'content',
'name',
'thumbnailUrl',
];
private static readonly cardProperties: string[] = [
'title',
'subtitle',
'text',
'images',
'image',
'buttons',
'tap',
'media',
'shareable',
'autoloop',
'autostart',
'aspect',
'duration',
'value',
'connectionName',
'tokenExchangeResource',
'facts',
'items',
'total',
'tax',
'vat',
];
/**
* Generate the activity.
*
* @param lgResult string result from languageGenerator.
* @returns The generated MessageActivity.
*/
static fromObject(lgResult: any): Partial<Activity> {
if (lgResult == null) {
return { type: ActivityTypes.Message };
}
if (typeof lgResult === 'string') {
return this.buildActivityFromText(lgResult.trim());
}
if (typeof lgResult === 'object') {
return this.buildActivityFromLGStructuredResult(lgResult);
} else {
return this.buildActivityFromText(JSON.stringify(lgResult).trim());
}
}
/**
* Given a lg result, create a text activity. This method will create a MessageActivity from text.
*
* @param text lg text output.
* @returns The created MessageActivity.
*/
private static buildActivityFromText(text: string): Partial<Activity> {
const msg: Partial<Activity> = {
type: ActivityTypes.Message,
};
if (text) {
msg.text = text;
msg.speak = text;
}
return msg;
}
/**
* Given a structured lg result, create an activity. This method will create an MessageActivity from object
*
* @param lgValue lg output.
* @returns The created MessageActivity.
*/
private static buildActivityFromLGStructuredResult(lgValue: any): Partial<Activity> {
let activity: Partial<Activity> = {};
const type: string = this.getStructureType(lgValue);
if (this.genericCardTypeMapping.has(type) || type === 'attachment') {
activity = MessageFactory.attachment(this.getAttachment(lgValue));
} else if (type === 'activity') {
activity = this.buildActivity(lgValue);
} else if (lgValue) {
activity = this.buildActivityFromText(JSON.stringify(lgValue).trim());
}
return activity;
}
/**
* Builds an [Activity](xref:botframework-schema.Activity) with a given message.
*
* @param messageValue Message value on which to base the activity.
* @returns [Activity](xref:botframework-schema.Activity) with the given message.
*/
private static buildActivity(messageValue: any): Partial<Activity> {
const activity: Partial<Activity> = { type: ActivityTypes.Message };
for (const key of Object.keys(messageValue)) {
const property: string = key.trim();
if (property === this.lgType) {
continue;
}
const value: any = messageValue[key];
switch (property.toLowerCase()) {
case 'text':
activity.text = typeof value === 'string' ? value : JSON.stringify(value);
break;
case 'speak':
activity.speak = typeof value === 'string' ? value : JSON.stringify(value);
break;
case 'attachments':
activity.attachments = this.getAttachments(value);
break;
case 'suggestedactions':
activity.suggestedActions = this.getSuggestions(value);
break;
default:
activity[this.realProperty(property, this.activityProperties)] = value;
break;
}
}
return activity;
}
/**
* @private
*/
private static getSuggestions(suggestionsValue: any): SuggestedActions {
const actions: any[] = this.normalizedToList(suggestionsValue);
const suggestedActions: SuggestedActions = {
actions: this.getCardActions(actions),
to: [],
};
return suggestedActions;
}
/**
* @private
*/
private static getButtons(buttonsValue: any): CardAction[] {
const actions: any[] = this.normalizedToList(buttonsValue);
return this.getCardActions(actions);
}
/**
* @private
*/
private static getCardActions(actions: any[]): CardAction[] {
return actions.map((u: any): CardAction => this.getCardAction(u));
}
/**
* @private
*/
private static getCardAction(action: any): CardAction {
let cardAction: CardAction;
if (typeof action === 'string') {
cardAction = { type: ActionTypes.ImBack, value: action, title: action, channelData: undefined };
} else {
const type: string = this.getStructureType(action);
cardAction = {
type: ActionTypes.ImBack,
title: '',
value: '',
};
if (type === 'cardaction') {
for (const key of Object.keys(action)) {
const property: string = key.trim();
if (property === this.lgType) {
continue;
}
cardAction[this.realProperty(property, this.cardActionProperties)] = action[key];
}
}
}
return cardAction;
}
/**
* @private
*/
private static getAttachments(input: any): Attachment[] {
const attachments: Attachment[] = [];
const attachmentsJsonList: any[] = this.normalizedToList(input);
for (const attachmentsJson of attachmentsJsonList) {
if (typeof attachmentsJson === 'object') {
attachments.push(this.getAttachment(attachmentsJson));
}
}
return attachments;
}
/**
* @private
*/
private static getAttachment(input: any): Attachment {
let attachment: Attachment = {
contentType: '',
};
const type: string = this.getStructureType(input);
if (this.genericCardTypeMapping.has(type)) {
attachment = this.getCardAttachment(this.genericCardTypeMapping.get(type), input);
} else if (type === 'adaptivecard') {
attachment = CardFactory.adaptiveCard(input);
} else if (type === 'attachment') {
attachment = this.getNormalAttachment(input);
} else {
attachment = { contentType: type, content: input };
}
return attachment;
}
/**
* @private
*/
private static getNormalAttachment(input: any): Attachment {
const attachment: Attachment = { contentType: '' };
for (const key of Object.keys(input)) {
const property: string = key.trim();
const value: any = input[key];
switch (property.toLowerCase()) {
case 'contenttype': {
const type: string = value.toString().toLowerCase();
if (this.genericCardTypeMapping.has(type)) {
attachment.contentType = this.genericCardTypeMapping.get(type);
} else if (type === 'adaptivecard') {
attachment.contentType = this.adaptiveCardType;
} else {
attachment.contentType = type;
}
break;
}
default: {
attachment[this.realProperty(property, this.attachmentProperties)] = value;
break;
}
}
}
return attachment;
}
/**
* @private
*/
private static getCardAttachment(type: string, input: any): Attachment {
const card: any = {};
for (const key of Object.keys(input)) {
const property: string = key.trim().toLowerCase();
const value: any = input[key];
switch (property) {
case 'tap': {
card[property] = this.getCardAction(value);
break;
}
case 'image':
case 'images': {
if (type === CardFactory.contentTypes.heroCard || type === CardFactory.contentTypes.thumbnailCard) {
if (!('images' in card)) {
card['images'] = [];
}
const imageList = this.normalizedToList(value);
imageList.forEach((u): any => card['images'].push(this.normalizedToMediaOrImage(u)));
} else {
card['image'] = this.normalizedToMediaOrImage(value);
}
break;
}
case 'media': {
if (!('media' in card)) {
card['media'] = [];
}
const mediaList = this.normalizedToList(value);
mediaList.forEach((u): any => card['media'].push(this.normalizedToMediaOrImage(u)));
break;
}
case 'buttons': {
if (!('buttons' in card)) {
card['buttons'] = [];
}
const buttons: any[] = this.getButtons(value);
buttons.forEach((u): any => card[property].push(u));
break;
}
case 'autostart':
case 'shareable':
case 'autoloop': {
const boolValue: boolean = this.getValidBooleanValue(value.toString());
if (boolValue !== undefined) {
card[property] = boolValue;
} else {
card[property] = value;
}
break;
}
default: {
card[this.realProperty(key.trim(), this.cardProperties)] = value;
break;
}
}
}
const attachment: Attachment = {
contentType: type,
content: card,
};
return attachment;
}
/**
* @private
*/
private static realProperty(property: string, builtinProperties: string[]): string {
const properties = builtinProperties.map((u: string): string => u.toLowerCase());
if (properties.includes(property.toLowerCase())) {
return builtinProperties[properties.indexOf(property.toLowerCase())];
} else {
return property;
}
}
/**
* @private
*/
private static normalizedToList(item: any): any[] {
if (item === undefined) {
return [];
} else if (Array.isArray(item)) {
return item;
} else {
return [item];
}
}
/**
* @private
*/
private static getStructureType(input: any): string {
let result = '';
if (input && typeof input === 'object') {
if (this.lgType in input) {
result = input[this.lgType].toString();
} else if ('type' in input) {
// Adaptive card type
result = input['type'].toString();
}
}
return result.trim().toLowerCase();
}
/**
* @private
*/
private static normalizedToMediaOrImage(item: any): object {
if (!item) {
return {};
} else if (typeof item === 'string') {
return { url: item };
} else return item;
}
/**
* @private
*/
private static getValidBooleanValue(boolValue: any): boolean {
if (typeof boolValue === 'boolean') {
return boolValue;
}
if (boolValue.toLowerCase() === 'true') {
return true;
} else if (boolValue.toLowerCase() === 'false') {
return false;
}
return undefined;
}
}