diff --git a/package.json b/package.json index b64cc7c0..68dbabf4 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,11 @@ "oauth": "^0.9.15", "q": "^1.4.1", "random-number-csprng": "^1.0.2", - "serve-favicon": "^2.4.0" + "serve-favicon": "^2.4.0", + "stjs": "0.0.5" }, "devDependencies": { + "@microsoft/teams-js": "^1.3.7", "@types/bluebird": "^3.0.37", "@types/chai": "^3.4.34", "@types/mocha": "^2.2.39", diff --git a/src/Bot.ts b/src/Bot.ts index 4ac86158..b6dd3396 100644 --- a/src/Bot.ts +++ b/src/Bot.ts @@ -10,6 +10,8 @@ import { Strings } from "./locale/locale"; import { loadSessionAsync } from "./utils/DialogUtils"; import * as teams from "botbuilder-teams"; import { ComposeExtensionHandlers } from "./composeExtension/ComposeExtensionHandlers"; +import {fetchTemplates, cardTemplates} from "./dialogs/Templates/CardTemplates"; +import {renderACAttachment} from "./utils/CardUtils"; // ========================================================= // Bot Setup @@ -89,6 +91,7 @@ export class Bot extends builder.UniversalBot { session.clearDialogStack(); let payload = (event as any).value; + let invokeType = (event as any).name; // Invokes don't participate in middleware // If payload has an address, then it is from a button to update a message so we do not what to send typing @@ -99,6 +102,48 @@ export class Bot extends builder.UniversalBot { if (payload && payload.dialog) { session.beginDialog(payload.dialog, payload); } + + switch (invokeType) { + case "task/fetch": + let taskModule = payload.data.taskModule.toLowerCase(); + if (fetchTemplates[taskModule] !== undefined) { + // Return the specified task module response to the bot + callback(null, fetchTemplates[taskModule], 200); + } + else { + callback(new Error(`Error: task module template for ${(payload.taskModule === undefined ? "" : payload.taskModule)} not found.`), null, 500); + } + break; + case "task/submit": + if (payload.data !== undefined) { + switch (payload.data.taskResponse) { + case "message": + // Echo the results to the chat stream + session.send("**task/submit results from the Adaptive card:**\n```" + JSON.stringify(payload) + "```"); + break; + case "continue": + let fetchResponse = fetchTemplates.submitResponse; + fetchResponse.task.value.card = renderACAttachment(cardTemplates.adaptiveCardSubmitResponse, { results: JSON.stringify(payload.data) }); + callback(null, fetchResponse, 200); + break; + + case "final": + // do nothing + default: + if (payload.data.levelType !== undefined && payload.data.levelType === "multistep") { + callback(null, fetchTemplates.submitMessageResponse, 200); + } + if (payload.data.levelType === undefined) { + session.send("**task/submit results from Task Module adaptive card:**\n\n```" + JSON.stringify(payload) + "```"); + } + else { + session.send("**task/submit results from Task Module Html:**\n\n```" + JSON.stringify(payload) + "```"); + } + } + } + break; + } + } callback(null, "", 200); }; diff --git a/src/app.ts b/src/app.ts index c9de1d4f..e782717a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -21,6 +21,7 @@ import { AADUserValidation } from "./apis/AADUserValidation"; import { ValidateAADToken } from "./apis/ValidateAADToken"; import { ManifestCreatorStart } from "./pages/ManifestCreatorStart"; import { ManifestCreatorEnd } from "./pages/ManifestCreatorEnd"; +import {TaskModuleTab} from "./pages/TaskModuleTab"; import * as builder from "botbuilder"; // Configure instrumentation - tooling with Azure @@ -41,6 +42,7 @@ let handlebars = exphbs.create({ extname: ".hbs", helpers: { appId: () => { return config.get("app.appId"); }, + baseUri: () => {return config.get("app.baseUri"); }, }, }); app.engine("hbs", handlebars.engine); @@ -54,6 +56,8 @@ app.get("/vstsAuth", VSTSAuthTab.getRequestHandler()); app.get("/vstsAuthFlowStart", VSTSAuthFlowStartPopUp.getRequestHandler()); app.get("/vstsAuthFlowEnd", VSTSAuthFlowEndPopUp.getRequestHandler()); app.get("/composeExtensionSettings", ComposeExtensionSettingsPopUp.getRequestHandler()); +app.get("/TaskModuleTab", TaskModuleTab.getRequestHandler()); +app.get("/customform", (req, res) => { res.render("customform", { appId: config.get("app.appId") , query: req.query}); } ); // Tab authentication sample routes app.get("/tab-auth/simple", (req, res) => { res.render("tab-auth/simple"); }); diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 00000000..ab254e44 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,28 @@ +// Task Module Ids +// tslint:disable-next-line:variable-name +export const TaskModuleTitles = { + AdaptiveCardSingleStepTitle: "Post command to bot using single step adaptive card", + AdaptiveCardMultiStepTitle: "Post command to bot using multi step adaptive card", + ActionSubmitResponseTitle: "Action.Submit Response", + SingleStepHtmlCardTitle: "Post Command to bot using single step html card", + MultistepHtmlCardTitle: "Post Command to bot using multistep html card", +}; + +// Task Module Ids +// tslint:disable-next-line:variable-name +export const TaskModuleIds = { + CustomForm: "customform", +}; + +// Task Module Sizes +// tslint:disable-next-line:variable-name +export const TaskModuleSizes = { + customform: { + width: 510, + height: 500, + }, + adaptivecard: { + width: 700, + height: 255, + }, +}; diff --git a/src/dialogs/RootDialog.ts b/src/dialogs/RootDialog.ts index 7220e4d4..e07e0ada 100644 --- a/src/dialogs/RootDialog.ts +++ b/src/dialogs/RootDialog.ts @@ -41,6 +41,7 @@ import { UpdateTextMsgSetupDialog } from "./examples/teams/UpdateTextMsgSetupDia import { NotifyDialog } from "./examples/teams/NotifyDialog"; import { PopupSignInDialog } from "./examples/basic/PopupSignInDialog"; import { AdaptiveCardDialog } from "./examples/basic/AdaptiveCardDialog"; +import {TaskModuleAdaptiveCardDialog} from "./examples/basic/TaskModuleAdaptiveCard"; // *************************** END OF EXAMPLES ********************************* // Add imports for dialogs @@ -105,6 +106,7 @@ export class RootDialog extends builder.IntentDialog { new NotifyDialog(bot); new PopupSignInDialog(bot); new AdaptiveCardDialog(bot); + new TaskModuleAdaptiveCardDialog(bot); // *************************** END OF EXAMPLES ********************************* // Add child dialogs diff --git a/src/dialogs/Templates/CardTemplates.ts b/src/dialogs/Templates/CardTemplates.ts new file mode 100644 index 00000000..13bcd4c7 --- /dev/null +++ b/src/dialogs/Templates/CardTemplates.ts @@ -0,0 +1,150 @@ + +import * as constants from "../../constants"; +import { renderACAttachment } from "../../utils/CardUtils"; +import * as config from "config"; + +// Function that works both in Node (where window === undefined) or the browser +export function appRoot(): string { + if (typeof window === "undefined") { + return config.get("app.baseUri"); + } else { + return window.location.protocol + "//" + window.location.host; + } +} + +export const cardTemplates: any = { + adaptiveCard: { + "type": "AdaptiveCard", + "body": [ + { + "type": "TextBlock", + "separator": true, + "size": "Large", + "weight": "Bolder", + "text": "Enter Command:", + "isSubtle": true, + "wrap": true, + }, + { + "type": "Input.Text", + "id": "commandToBot", + "placeholder": "E.g. timezone", + }, + ], + "actions": [ + { + "type": "Action.Submit", + "id": "postCommand", + "title": "Post Command", + "data": { + "taskResponse": "{{responseType}}", + }, + }, + { + "type": "Action.Submit", + "id": "cancel", + "title": "Cancel", + }, + ], + "version": "1.0", + }, + adaptiveCardSubmitResponse: { + "type": "AdaptiveCard", + "body": [ + { + "type": "TextBlock", + "weight": "Bolder", + "text": "Action.Submit Results", + }, + { + "type": "TextBlock", + "separator": true, + "size": "Medium", + "text": "{{results}}", + "wrap": true, + }, + ], + "actions": [ + { + "type": "Action.Submit", + "title": "OK", + "data": { + "taskResponse": "final", + "taskModule": "acResponse", + }, + }, + ], + "version": "1.0", + }, +}; + +export const fetchTemplates: any = { + adaptivecardsinglestep: { + "task": { + "type": "continue", + "value": { + "title": constants.TaskModuleTitles.AdaptiveCardSingleStepTitle, + "height": constants.TaskModuleSizes.adaptivecard.height, + "width": constants.TaskModuleSizes.adaptivecard.width, + // Below wraps it as a builder.Attachment + "card": renderACAttachment(cardTemplates.adaptiveCard, { responseType: "message" }), + }, + }, + }, + adaptivecardmultistep: { + "task": { + "type": "continue", + "value": { + "title": constants.TaskModuleTitles.AdaptiveCardMultiStepTitle, + "height": constants.TaskModuleSizes.adaptivecard.height, + "width": constants.TaskModuleSizes.adaptivecard.width, + "fallbackUrl": null, + // Below wraps it as a builder.Attachment + "card": renderACAttachment(cardTemplates.adaptiveCard, { responseType: "continue" }), + }, + }, + }, + + singlestephtmlcard: { + "task": { + "type": "continue", + "value": { + "title": constants.TaskModuleTitles.SingleStepHtmlCardTitle, + "height": constants.TaskModuleSizes.customform.height, + "width": constants.TaskModuleSizes.customform.width, + "fallbackUrl": `${appRoot()}/${constants.TaskModuleIds.CustomForm}?type=singlestep`, + "url": `${appRoot()}/${constants.TaskModuleIds.CustomForm}?type=singlestep`, + }, + }, + }, + multistephtmlcard: { + "task": { + "type": "continue", + "value": { + "title": constants.TaskModuleTitles.MultistepHtmlCardTitle, + "height": constants.TaskModuleSizes.customform.height, + "width": constants.TaskModuleSizes.customform.width, + "fallbackUrl": `${appRoot()}/${constants.TaskModuleIds.CustomForm}?type=multistep`, + "url": `${appRoot()}/${constants.TaskModuleIds.CustomForm}?type=multistep`, + }, + }, + }, + submitMessageResponse: { + "task": { + "type": "message", + "value": "Task completed!", + }, + }, + + submitResponse: { + "task": { + "type": "continue", + "value": { + "title": constants.TaskModuleTitles.ActionSubmitResponseTitle, + "height": "small", + "width": "medium", + "card": {}, + }, + }, + }, +}; diff --git a/src/dialogs/examples/basic/TaskModuleAdaptiveCard.ts b/src/dialogs/examples/basic/TaskModuleAdaptiveCard.ts new file mode 100644 index 00000000..d39b569a --- /dev/null +++ b/src/dialogs/examples/basic/TaskModuleAdaptiveCard.ts @@ -0,0 +1,111 @@ +import * as builder from "botbuilder"; +import { TriggerActionDialog } from "../../../utils/TriggerActionDialog"; +import { DialogIds } from "../../../utils/DialogIds"; +import { DialogMatches } from "../../../utils/DialogMatches"; + +export class TaskModuleAdaptiveCardDialog extends TriggerActionDialog { + private static async sendAdaptiveCard(session: builder.Session, args?: any | builder.IDialogResult, next?: (args?: builder.IDialogResult) => void): Promise { + // Check for the property in the value set by the adaptive card submit action + if (session.message.value && session.message.value.isFromAdaptiveCard) + { + session.send(JSON.stringify(session.message.value)); + } else { // create new adaptive card + let adaptiveCardMessage = new builder.Message(session) + .addAttachment(TaskModuleAdaptiveCardDialog.getAdaptiveCardAttachment()); + session.send(adaptiveCardMessage); + } + } + + // Get the adaptive card attachment + private static getAdaptiveCardAttachment(): any { + let adaptiveCardJson = { + contentType: "application/vnd.microsoft.card.adaptive", + content: { + type: "AdaptiveCard", + version: "1.0", + body: [ + { + type : "Container", + items: + [ + { + type: "TextBlock", + size: "large", + weight: "bolder", + color: null, + isSubtle: false, + text: "Task Module Adaptive Card!", + horizontalAlignment: "left", + wrap: false, + maxLines: 0, + speak: "Adaptive card!", + separation: null, + }, + ], + }, + ], + actions: [ + { + type: "Action.Submit", + title: "Invoke Task Module(Adaptive Card) -single step", + id: "adaptivecardSingleStep", + data: { + "msteams": { + "type": "task/fetch", + }, + "taskModule": "adaptivecardsinglestep", + }, + }, + + { + type: "Action.Submit", + title: "Invoke Task Module(Adaptive Card) -Multi step", + id: "adaptivecardmultistep", + data: { + "msteams": { + "type": "task/fetch", + }, + "taskModule": "adaptivecardMultiStep", + }, + }, + { + type: "Action.Submit", + title: "Invoke Task Module(Html) -Single step", + id: "singlestephtmlcard", + data: { + "msteams": { + "type": "task/fetch", + }, + "taskModule": "singlestephtmlcard", + }, + }, + + { + type: "Action.Submit", + title: "Invoke Task Module(Html) -Multi step", + id: "multistephtmlcard", + data: { + "msteams": { + "type": "task/fetch", + }, + "taskModule": "multistephtmlcard", + }, + }, + ], + }, + }; + return adaptiveCardJson; + } + + constructor( + bot: builder.UniversalBot, + ) { + super(bot, + DialogIds.TaskModuleAdaptiveCardId, + [ + DialogMatches.TaskModuleAdaptiveCardDialogMatch, + ], + TaskModuleAdaptiveCardDialog.sendAdaptiveCard, + ); + } +} diff --git a/src/pages/DefaultTab.ts b/src/pages/DefaultTab.ts index a435afde..e2db8450 100644 --- a/src/pages/DefaultTab.ts +++ b/src/pages/DefaultTab.ts @@ -49,6 +49,7 @@ export class DefaultTab {

+ `; diff --git a/src/pages/TaskModuleTab.ts b/src/pages/TaskModuleTab.ts new file mode 100644 index 00000000..c67e9306 --- /dev/null +++ b/src/pages/TaskModuleTab.ts @@ -0,0 +1,128 @@ +import * as express from "express"; +import * as config from "config"; + +export class TaskModuleTab { + public static getRequestHandler(): express.RequestHandler { + return async function (req: any, res: any, next: any): Promise { + try{ + let htmlPage = ` + Bot Info + + + + + + + + +
+ +
+
+ + + + `; + res.send(htmlPage); + + }catch (e){ + res.send(` +

Some error has occured

+ `); + } + }; + } + +} diff --git a/src/utils/CardUtils.ts b/src/utils/CardUtils.ts new file mode 100644 index 00000000..ea3f69f9 --- /dev/null +++ b/src/utils/CardUtils.ts @@ -0,0 +1,20 @@ +import * as builder from "botbuilder"; +import * as stjs from "stjs"; + +export function renderACAttachment(template: any, data: any): builder.AttachmentType { + // ToDo: + // 1. Optionally validate that the schema is valid (postponed as there are tool/schema issues) + + // Pre-process the template so that template placeholders don't show up for null data values + // Regex: Find everything between {{}} and prepend "#? " to it + template = JSON.parse(JSON.stringify(template).replace(/{{(.+?)}}/g, "{{#? $1}}")); + + // No error handling in the call to stjs functions - what you pass in may be garbage, but it always returns a value + let ac = stjs.select(data) + .transformWith(template) + .root(); + return { + contentType: "application/vnd.microsoft.card.adaptive", + content: ac, + }; +} diff --git a/src/utils/DialogIds.ts b/src/utils/DialogIds.ts index 30f0ea6f..7a885ed7 100644 --- a/src/utils/DialogIds.ts +++ b/src/utils/DialogIds.ts @@ -43,6 +43,7 @@ export const DialogIds = { NotifyDialogId: "NotifyDialog", PopupSignInDialogId: "PopupSignInDialog", AdaptiveCardDialogId: "AdaptiveCardDialog", + TaskModuleAdaptiveCardId: "TaskModuleAdaptiveCard", // *************************** END OF EXAMPLES ********************************* // Add entries for dialog ids diff --git a/src/utils/DialogMatches.ts b/src/utils/DialogMatches.ts index 57dfd8cb..d2d7274e 100644 --- a/src/utils/DialogMatches.ts +++ b/src/utils/DialogMatches.ts @@ -42,6 +42,7 @@ export const DialogMatches = { NotifyDialogMatch: /notify/i, PopUpSignInDialogMatch: /signin/i, AdaptiveCardDialogMatch: /adaptive card/i, + TaskModuleAdaptiveCardDialogMatch: /Task Module/i, // *************************** END OF EXAMPLES ********************************* // Add regex or string intent matches for dialogs diff --git a/src/views/customform.hbs b/src/views/customform.hbs new file mode 100644 index 00000000..031af0bb --- /dev/null +++ b/src/views/customform.hbs @@ -0,0 +1,38 @@ + + + + + + + +

List of all the sample commands

+ +
+
+ Enter Valid Command: +
+
+ + + + + \ No newline at end of file