├── .deployment ├── TaskModule.zip ├── tsconfig.prod.json ├── public ├── images │ ├── contoso.png │ ├── favicon.ico │ ├── color_icon.png │ ├── outline_icon.png │ └── color_icon_transparent.png └── styles │ ├── custom.css │ └── msteams-16.css ├── src ├── storage │ ├── index.ts │ ├── BotExtendedStorage.ts │ ├── NullBotStorage.ts │ └── MongoDbBotStorage.ts ├── views │ ├── first.pug │ ├── hello.pug │ ├── second.pug │ ├── youtube.pug │ ├── configure.pug │ ├── layout.pug │ ├── powerapp.pug │ ├── embed.pug │ ├── taskmodule.pug │ └── customform.pug ├── config │ ├── default.json │ └── custom-environment-variables.json ├── utils │ ├── index.ts │ ├── Logger.ts │ ├── DeepLinks.ts │ ├── CardUtils.ts │ └── MessageUtils.ts ├── MessagingExtension.ts ├── constants.ts ├── tabs.ts ├── dialogs │ ├── ACGenerator.ts │ ├── BotFrameworkCard.ts │ ├── RootDialog.ts │ └── CardTemplates.ts ├── manifest.json ├── app.ts ├── TeamsBot.ts └── TaskModuleTab.ts ├── config └── default.json ├── .vscode ├── tasks.json └── launch.json ├── tsconfig.json ├── LICENSE ├── .gitignore ├── package.json ├── web.config ├── SECURITY.md ├── tslint.json ├── deploy.cmd ├── gulpfile.js └── README.md /.deployment: -------------------------------------------------------------------------------- 1 | [config] 2 | command = deploy.cmd -------------------------------------------------------------------------------- /TaskModule.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-sample-task-module-nodejs/HEAD/TaskModule.zip -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "node_modules", 5 | "test/**/*" 6 | ] 7 | } -------------------------------------------------------------------------------- /public/images/contoso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-sample-task-module-nodejs/HEAD/public/images/contoso.png -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-sample-task-module-nodejs/HEAD/public/images/favicon.ico -------------------------------------------------------------------------------- /public/images/color_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-sample-task-module-nodejs/HEAD/public/images/color_icon.png -------------------------------------------------------------------------------- /public/images/outline_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-sample-task-module-nodejs/HEAD/public/images/outline_icon.png -------------------------------------------------------------------------------- /public/styles/custom.css: -------------------------------------------------------------------------------- 1 | html, body, div.surface, div.panel { 2 | height: 100%; 3 | margin: 0; 4 | } 5 | 6 | div.panel { 7 | padding: 15px; 8 | } 9 | -------------------------------------------------------------------------------- /public/images/color_icon_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-sample-task-module-nodejs/HEAD/public/images/color_icon_transparent.png -------------------------------------------------------------------------------- /src/storage/index.ts: -------------------------------------------------------------------------------- 1 | // Re-export types from files in storage 2 | 3 | export * from "./BotExtendedStorage"; 4 | export * from "./MongoDbBotStorage"; 5 | export * from "./NullBotStorage"; 6 | -------------------------------------------------------------------------------- /src/views/first.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | div(class='font-semibold font-title') This is our first tab 5 | p Welcome to Microsoft Teams Hello World sample app (Node.js) 6 | -------------------------------------------------------------------------------- /src/views/hello.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | div(class='font-semibold font-title') Hello, Task Modules! 5 | p Welcome to Microsoft Teams Task Modules sample app (Node.js) 6 | -------------------------------------------------------------------------------- /src/views/second.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | div(class='font-semibold font-title') This is our second tab 5 | p Welcome to Microsoft Teams Hello World sample app (Node.js) 6 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "bot": { 3 | "microsoftAppId": "00000000-0000-0000-0000-000000000000", 4 | "microsoftAppPassword": "yourBotAppPassword" 5 | }, 6 | "storage": "memory" 7 | } -------------------------------------------------------------------------------- /src/views/youtube.pug: -------------------------------------------------------------------------------- 1 | extends embed.pug 2 | 3 | block iframe 4 | iframe(width="1000" height="700" src="https://www.youtube.com/embed/XBpibC4s0bc?start=588" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen) -------------------------------------------------------------------------------- /src/config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "baseUri": "", 4 | "appId": "" 5 | }, 6 | "bot": { 7 | "botId": "", 8 | "botPassword": "" 9 | }, 10 | "storage": "memory" 11 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "type": "shell", 9 | "command": "npm run -s build" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /src/config/custom-environment-variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "baseUri": "BASE_URI", 4 | "appId": "MICROSOFT_APP_ID", 5 | "instrumentationKey": "APPINSIGHTS_INSTRUMENTATIONKEY" 6 | }, 7 | "bot": { 8 | "appId": "MICROSOFT_APP_ID", 9 | "appPassword": "MICROSOFT_APP_PASSWORD" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitAny": false, 5 | "removeComments": false, 6 | "preserveConstEnums": true, 7 | "sourceMap": true, 8 | "target": "es6", 9 | "outDir": "build", 10 | "types": [ "node" ] 11 | }, 12 | "include": [ 13 | "src/**/*.ts", 14 | "test/**/*.ts" 15 | ], 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } -------------------------------------------------------------------------------- /src/views/configure.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | div(class='font-semibold font-title') Configure your app here 5 | p 6 | div 7 | label(for='tabChoice') 8 | | Select the tab you would like to see: 9 | | 10 | select#tabChoice(name='tabChoice') 11 | option(value='' selected=true) (Select a tab) 12 | option(value='first') First 13 | option(value='second') Second 14 | option(value='taskmodule') Task Module Demo 15 | -------------------------------------------------------------------------------- /src/views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | head 4 | style. 5 | body { 6 | margin: 0; 7 | } 8 | 9 | title Microsoft Teams Task Module Sample App 10 | link(rel='stylesheet', type='text/css', href='/styles/msteams-16.css') 11 | link(rel='stylesheet', type='text/css', href='/styles/custom.css') 12 | // script(src='/scripts/MicrosoftTeams.js') 13 | // script(src='/scripts/TaskModuleTab.js') 14 | script(src='/scripts/bundle.js') // bundled version of the above files 15 | script. 16 | let appId = "#{appId}"; 17 | block scripts 18 | 19 | body(class='theme-light') 20 | // span(id="appId" style="display: none") #{appId} 21 | div(class='surface') 22 | div(class='panel') 23 | block content -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # Distributables 61 | build/ 62 | dist/ 63 | manifest/ 64 | public/scripts/bundle.js 65 | 66 | # next.js build output 67 | .next 68 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation 2 | // All rights reserved. 3 | // 4 | // MIT License: 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | // Re-export types from files in utils 25 | 26 | // export * from "./Logger"; 27 | export * from "./MessageUtils"; 28 | export * from "./CardUtils"; 29 | -------------------------------------------------------------------------------- /src/storage/BotExtendedStorage.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation 2 | // All rights reserved. 3 | // 4 | // MIT License: 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | import * as builder from "botbuilder"; 25 | 26 | /** Replacable storage system. */ 27 | export interface IBotExtendedStorage extends builder.IBotStorage { 28 | 29 | /** Reads in user data from storage based on AAD object id. */ 30 | getUserDataByAadObjectIdAsync(aadObjectId: string): Promise; 31 | 32 | /** Gets the AAD object id associated with the user data bag. */ 33 | getAAdObjectId(userData: any): string; 34 | 35 | /** Sets the AAD object id associated with the user data bag. */ 36 | setAAdObjectId(userData: any, aadObjectId: string): void; 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/storage/NullBotStorage.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation 2 | // All rights reserved. 3 | // 4 | // MIT License: 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | import * as builder from "botbuilder"; 25 | 26 | /** Replacable storage system used by UniversalBot. */ 27 | export class NullBotStorage implements builder.IBotStorage { 28 | 29 | // Reads in data from storage 30 | public getData(context: builder.IBotStorageContext, callback: (err: Error, data: builder.IBotStorageData) => void): void { 31 | callback(null, {}); 32 | } 33 | 34 | // Writes out data from storage 35 | public saveData(context: builder.IBotStorageContext, data: builder.IBotStorageData, callback?: (err: Error) => void): void { 36 | callback(null); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "type": "node", 10 | "request": "attach", 11 | "name": "Attach by Process ID", 12 | "processId": "${command:PickProcess}" 13 | }, 14 | { 15 | "type": "node", 16 | "request": "launch", 17 | "name": "Launch - Teams Debug", 18 | "program": "${workspaceRoot}/src/app.ts", 19 | "cwd": "${workspaceRoot}/build/src", 20 | "preLaunchTask": "build", 21 | "sourceMaps": true, 22 | "outFiles": [ "${workspaceRoot}/build/**/*.js" ] 23 | }, 24 | { 25 | "name": "Attach to Teams", 26 | "type": "chrome", 27 | "request": "launch", 28 | "port": 9222, 29 | "runtimeExecutable": "C:/Users/billb/AppData/Local/Microsoft/Teams/Update.exe --processStart 'Teams.exe'", 30 | "cwd": "{workspaceRoot}/build/src", 31 | "webRoot": "${workspaceFolder}" 32 | }, 33 | { 34 | "type": "node", 35 | "request": "launch", 36 | "name": "Launch - Fiddler (only works when running Fiddler)", 37 | "program": "${workspaceRoot}/src/app.ts", 38 | "cwd": "${workspaceRoot}/build/src", 39 | "preLaunchTask": "build", 40 | "sourceMaps": true, 41 | "outFiles": [ "${workspaceRoot}/build/**/*.js" ] 42 | }, 43 | { 44 | "type": "node", 45 | "request": "launch", 46 | "name": "Launch (no build)", 47 | "program": "${workspaceRoot}/build/src/app.js", 48 | "cwd": "${workspaceRoot}/build/src", 49 | "sourceMaps": true, 50 | "outFiles": [ "${workspaceRoot}/build/src/**/*.js" ] 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /src/views/powerapp.pug: -------------------------------------------------------------------------------- 1 | extends embed.pug 2 | 3 | block iframe 4 | iframe(id="powerApp" width="720" height="520" style="width: 94%; display: none" src="https://web.powerapps.com/webplayer/iframeapp?source=iframe&screenColor=rgba(104,101,171,1)&appId=/providers/Microsoft.PowerApps/apps/a3447344-9220-4868-b45b-07759637be0f") 5 | div(id="powerAppError" class="surface" style="display: none; padding: 20px;") 6 | p This task module shows an "Asset Checkout" PowerApp (you'll find more information on it here). Unfortunately, PowerApps are tenant-specific. The one in this sample only works on the Microsoft.com tenant. 7 | p To create your own: 8 | ol 9 | li Go to https://powerapps.microsoft.com and sign in. 10 | li There's a search box in the middle of the page: search for "Asset Checkout". 11 | li Click on it, press the "Make this app" button, and save it to the cloud. 12 | li Press the Share button and you'll see a page with the newly-created PowerApp's App ID. Copy it to the clipboard. 13 | li Replace "a3447344-9220-4868-b45b-07759637be0f" in the src= URL of the <iframe> in this file (src/views/powerapp.pug) with your App ID. 14 | li Modify the JavaScript function in this file (src/views/powerapp.pug) to override the check for Microsoft's Tenant ID by uncommenting one line. 15 | script. 16 | microsoftTeams.getContext(function(context) { 17 | // If not running in the Microsoft tenant, show an error message; otherwise, show the PowerApp 18 | var showPowerApp = (context.tid === "72f988bf-86f1-41af-91ab-2d7cd011db47"); // Microsoft's tenant ID 19 | // Uncomment the line below if you create your own PowerApp and modify the iframe's src= URL 20 | // var showPowerApp = true 21 | if (showPowerApp) { 22 | document.getElementById("powerApp").style.display = "block"; 23 | } 24 | else { 25 | document.getElementById("powerAppError").style.display = "block"; 26 | } 27 | }); -------------------------------------------------------------------------------- /src/utils/Logger.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation 2 | // All rights reserved. 3 | // 4 | // MIT License: 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | /* import { Logger, transports } from "winston"; 25 | 26 | export const logger = new Logger({ 27 | transports: [ 28 | new transports.Console({ 29 | timestamp: () => { return new Date().toLocaleTimeString(); }, 30 | colorize: (process.env.MONOCHROME_CONSOLE) ? false : true, 31 | level: "debug", 32 | }), 33 | ], 34 | }); */ 35 | 36 | /* export class Logger extends winston.Logger { 37 | constructor() { 38 | super(); 39 | this.add( 40 | winston.transports.Console, 41 | { 42 | timestamp: this.tsFormat, 43 | colorize: (process.env.MONOCHROME_CONSOLE) ? false : true, 44 | level: "debug", 45 | }, 46 | ); 47 | } 48 | 49 | private tsFormat(): string { 50 | return new Date().toLocaleTimeString(); 51 | } 52 | } 53 | 54 | export const logger = new Logger(); */ 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msteams-task-module-test", 3 | "version": "1.0.0", 4 | "description": "Test app for testing the Teams task module", 5 | "scripts": { 6 | "start": "cd ./build/src && node app.js", 7 | "build": "gulp build", 8 | "debug": "cd ./build/src && node --debug app.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/billbliss/msteams-sample-hello-world-nodejs-ts" 14 | }, 15 | "author": "billbliss", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/billbliss/msteams-sample-hello-world-nodejs-ts/issues" 19 | }, 20 | "engines": { 21 | "node": ">=8.9.4 <10.17.0", 22 | "npm": ">=5.6.0" 23 | }, 24 | "homepage": "https://github.com/billbliss/msteams-sample-hello-world-nodejs-ts/blob/master/README.md", 25 | "dependencies": { 26 | "adaptivecards": "^1.1.0", 27 | "async": "^2.6.1", 28 | "body-parser": "^1.18.3", 29 | "botbuilder": "^3.15.0", 30 | "botbuilder-teams": "^0.1.7", 31 | "dotenv": "^8.2.0", 32 | "express": "^4.16.4", 33 | "faker": "^4.1.0", 34 | "handlebars": "^4.7.0", 35 | "path": "^0.12.7", 36 | "pug": "^2.0.3", 37 | "request": "^2.88.0", 38 | "serve-favicon": "^2.5.0", 39 | "stjs": "0.0.5", 40 | "winston": "^2.4.4" 41 | }, 42 | "devDependencies": { 43 | "@microsoft/teams-js": "^1.3.7", 44 | "@types/config": "0.0.34", 45 | "@types/express": "^4.16.0", 46 | "@types/faker": "^4.1.4", 47 | "@types/gulp-mocha": "0.0.32", 48 | "@types/node": "^10.17.13", 49 | "@types/winston": "^2.4.4", 50 | "browserify": "^16.2.3", 51 | "del": "^3.0.0", 52 | "gulp": "^4.0.2", 53 | "gulp-develop-server": "^0.5.2", 54 | "gulp-json-transform": "^0.4.5", 55 | "gulp-mocha": "^5.0.0", 56 | "gulp-sourcemaps": "^2.6.4", 57 | "gulp-spawn-mocha": "^5.0.1", 58 | "gulp-tslint": "^8.1.3", 59 | "gulp-typescript": "^5.0.1", 60 | "gulp-zip": "^4.2.0", 61 | "istanbul": "^0.4.5", 62 | "mocha": "^5.2.0", 63 | "mongodb": "^3.4.1", 64 | "ts-node": "^7.0.1", 65 | "tslint": "^5.11.0", 66 | "typescript": "^3.7.4", 67 | "vinyl-source-stream": "^2.0.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/DeepLinks.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation 2 | // All rights reserved. 3 | // 4 | // MIT License: 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | // Helper function to generate task module deep links 25 | export function taskModuleLink( 26 | appId: string, 27 | // tslint:disable:no-inferrable-types 28 | title: string = "", 29 | height: string | number = "medium", 30 | width: string | number = "medium", 31 | url: string = null, 32 | card: any = null, 33 | fallbackUrl?: string, 34 | completionBotId?: string): string { 35 | if ((url === null) && (card === null)) { 36 | return("Error generating deep link: you must specify either a card or URL."); 37 | } 38 | else { 39 | let cardOrUrl = (card === null) ? `url=${url}` : `card=${JSON.stringify(card)}`; 40 | let fallBack = (fallbackUrl === undefined) ? "" : `&fallbackUrl=${fallbackUrl}`; 41 | let completionBot = (completionBotId === undefined) ? "" : `&completionBotId=${completionBotId}`; 42 | return(encodeURI(`https://teams.microsoft.com/l/task/${appId}?${cardOrUrl}&height=${height}&width=${width}&title=${title}${fallBack}${completionBot}`)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/views/embed.pug: -------------------------------------------------------------------------------- 1 | //- Copyright (c) Microsoft Corporation 2 | //- All rights reserved. 3 | //- 4 | //- MIT License: 5 | //- Permission is hereby granted, free of charge, to any person obtaining 6 | //- a copy of this software and associated documentation files (the 7 | //- "Software"), to deal in the Software without restriction, including 8 | //- without limitation the rights to use, copy, modify, merge, publish, 9 | //- distribute, sublicense, and/or sell copies of the Software, and to 10 | //- permit persons to whom the Software is furnished to do so, subject to 11 | //- the following conditions: 12 | //- 13 | //- The above copyright notice and this permission notice shall be 14 | //- included in all copies or substantial portions of the Software. 15 | //- 16 | //- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | //- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | //- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | //- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | //- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | //- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | //- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | //- 24 | //- 25 | 26 | doctype html 27 | html(lang='en') 28 | head 29 | link(rel='stylesheet', type='text/css', href='/styles/msteams-16.css') 30 | link(rel='stylesheet', type='text/css', href='/styles/custom.css') 31 | script(src="https://unpkg.com/@microsoft/teams-js@1.3.7/dist/MicrosoftTeams.min.js" integrity="sha384-glExfvkpce98dO2oN+diZ/Luv/5qrZJiOvWCeR8ng/ZxlhpvBgHKeVFRURrh+NEC" crossorigin="anonymous") 32 | body 33 | style. 34 | body { 35 | margin: 0; 36 | } 37 | #embed-container iframe { 38 | position: absolute; 39 | top: 0; 40 | left: 0; 41 | width: 95%; 42 | height: 95%; 43 | padding-left: 20px; 44 | padding-right: 20px; 45 | padding-top: 10px; 46 | padding-bottom: 10px; 47 | border-style: none; 48 | } 49 | script. 50 | microsoftTeams.initialize(); 51 | 52 | //- Handle the Esc key 53 | document.onkeyup = function(event) { 54 | if ((event.key === 27) || (event.key === "Escape")) { 55 | microsoftTeams.tasks.submitTask(null); //- this will return an err object to the completionHandler() 56 | } 57 | } 58 | div(id="embed-container") 59 | block iframe 60 | -------------------------------------------------------------------------------- /web.config: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/MessagingExtension.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation 2 | // All rights reserved. 3 | // 4 | // MIT License: 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | import * as builder from "botbuilder"; 25 | import * as msteams from "botbuilder-teams"; 26 | import { TeamsBot } from "./TeamsBot"; 27 | import * as faker from "faker"; 28 | 29 | export class MessagingExtension extends TeamsBot { 30 | constructor( 31 | bot: TeamsBot, 32 | ) 33 | { 34 | super(bot._connector, bot._botSettings); 35 | // Cast as an msTeams.TeamsChatConnector and attach the onQuery event 36 | (this._connector as msteams.TeamsChatConnector).onQuery("getRandomText", this.generateRandomResponse); 37 | } 38 | 39 | private generateRandomResponse(event: builder.IEvent, query: msteams.ComposeExtensionQuery, callback: any): void { 40 | // If the user supplied a title via the cardTitle parameter then use it or use a fake title 41 | let title = query.parameters && query.parameters[0].name === "cardTitle" 42 | ? query.parameters[0].value 43 | : faker.lorem.sentence(); 44 | 45 | let randomImageUrl = "https://loremflickr.com/200/200"; // Faker's random images uses lorempixel.com, which has been down a lot 46 | 47 | // Build the data to send 48 | let attachments = []; 49 | 50 | // Generate 5 results to send with fake text and fake images 51 | for (let i = 0; i < 5; i++) { 52 | attachments.push( 53 | new builder.ThumbnailCard() 54 | .title(title) 55 | .text(faker.lorem.paragraph()) 56 | .images([new builder.CardImage().url(`${randomImageUrl}?random=${i}`)]) 57 | .toAttachment()); 58 | } 59 | 60 | // Build the response to be sent 61 | let response = msteams.ComposeExtensionResponse 62 | .result("list") 63 | .attachments(attachments) 64 | .toResponse(); 65 | 66 | // Send the response to teams 67 | callback(null, response, 200); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | true, 5 | "parameters", 6 | "statements" 7 | ], 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "eofline": true, 15 | "forin": false, 16 | "indent": [ true, "spaces" ], 17 | "interface-name": [ false ], 18 | "member-access": true, 19 | "member-ordering": [ 20 | true, 21 | "public-before-private", 22 | "static-before-instance", 23 | "variables-before-functions" 24 | ], 25 | "no-angle-bracket-type-assertion": true, 26 | "no-any": false, 27 | "no-arg": true, 28 | "no-bitwise": true, 29 | "no-conditional-assignment": true, 30 | "no-consecutive-blank-lines": true, 31 | "no-console": [ 32 | true, 33 | "debug", 34 | "info", 35 | "time", 36 | "timeEnd", 37 | "trace" 38 | ], 39 | "no-construct": true, 40 | "no-debugger": true, 41 | "no-duplicate-variable": true, 42 | "no-empty": true, 43 | "no-eval": true, 44 | "no-inferrable-types": true, 45 | "no-internal-module": true, 46 | "no-invalid-this": false, 47 | "no-reference": false, 48 | "no-require-imports": false, 49 | "no-shadowed-variable": true, 50 | "no-string-literal": false, 51 | "no-switch-case-fall-through": true, 52 | "no-trailing-whitespace": true, 53 | "no-unused-expression": true, 54 | "no-use-before-declare": true, 55 | "no-var-keyword": true, 56 | "no-var-requires": false, 57 | "object-literal-sort-keys": false, 58 | "one-line": [ false ], 59 | "quotemark": [ 60 | true, 61 | "double", 62 | "avoid-escape" 63 | ], 64 | "semicolon": [ 65 | true, 66 | "always" 67 | ], 68 | "trailing-comma": [ 69 | true, 70 | { 71 | "multiline": "always", 72 | "singleline": "never" 73 | } 74 | ], 75 | "triple-equals": [ 76 | true, 77 | "allow-null-check", 78 | "allow-undefined-check" 79 | ], 80 | "typedef": [ 81 | true, 82 | "call-signature", 83 | "parameter", 84 | "property-declaration", 85 | "member-variable-declaration" 86 | ], 87 | "typedef-whitespace": [ 88 | true, 89 | { 90 | "call-signature": "nospace", 91 | "index-signature": "nospace", 92 | "parameter": "nospace", 93 | "property-declaration": "nospace", 94 | "variable-declaration": "nospace" 95 | }, 96 | { 97 | "call-signature": "onespace", 98 | "index-signature": "onespace", 99 | "parameter": "onespace", 100 | "property-declaration": "onespace", 101 | "variable-declaration": "onespace" 102 | } 103 | ], 104 | "use-isnan": true, 105 | "variable-name": [ 106 | true, 107 | "check-format", 108 | "allow-leading-underscore", 109 | "ban-keywords" 110 | ], 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-module", 117 | "check-separator", 118 | "check-type", 119 | "check-typecast" 120 | ] 121 | } 122 | } -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation 2 | // All rights reserved. 3 | // 4 | // MIT License: 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | // Activity types 25 | export const messageType = "message"; 26 | export const invokeType = "invoke"; 27 | 28 | // Dialog ids 29 | // tslint:disable-next-line:variable-name 30 | export const DialogId = { 31 | Root: "/", 32 | ACTester: "actester", 33 | BFCard: "bfcard", 34 | }; 35 | 36 | // Telemetry events 37 | // tslint:disable-next-line:variable-name 38 | export const TelemetryEvent = { 39 | UserActivity: "UserActivity", 40 | BotActivity: "BotActivity", 41 | }; 42 | 43 | // URL Placeholders - not currently supported 44 | // tslint:disable-next-line:variable-name 45 | export const UrlPlaceholders = "loginHint={loginHint}&upn={userPrincipalName}&aadId={userObjectId}&theme={theme}&groupId={groupId}&tenantId={tid}&locale={locale}"; 46 | 47 | // Task Module Strings 48 | // tslint:disable-next-line:variable-name 49 | export const TaskModuleStrings = { 50 | YouTubeTitle: "Microsoft Ignite 2018 Vision Keynote", 51 | PowerAppTitle: "PowerApp: Asset Checkout", 52 | CustomFormTitle: "Custom Form", 53 | AdaptiveCardTitle: "Create a new job posting", 54 | AdaptiveCardKitchenSinkTitle: "Adaptive Card: Inputs", 55 | ActionSubmitResponseTitle: "Action.Submit Response", 56 | YouTubeName: "YouTube", 57 | PowerAppName: "PowerApp", 58 | CustomFormName: "Custom Form", 59 | AdaptiveCardSingleName: "Adaptive Card - Single", 60 | AdaptiveCardSequenceName: "Adaptive Card - Sequence", 61 | }; 62 | 63 | // Task Module Ids 64 | // tslint:disable-next-line:variable-name 65 | export const TaskModuleIds = { 66 | YouTube: "youtube", 67 | PowerApp: "powerapp", 68 | CustomForm: "customform", 69 | AdaptiveCard1: "adaptivecard1", 70 | AdaptiveCard2: "adaptivecard2", 71 | }; 72 | 73 | // Task Module Sizes 74 | // tslint:disable-next-line:variable-name 75 | export const TaskModuleSizes = { 76 | youtube: { 77 | width: 1000, 78 | height: 700, 79 | }, 80 | powerapp: { 81 | width: 720, 82 | height: 520, 83 | }, 84 | customform: { 85 | width: 510, 86 | height: 430, 87 | }, 88 | adaptivecard: { 89 | width: 700, 90 | height: 255, 91 | }, 92 | }; 93 | -------------------------------------------------------------------------------- /src/tabs.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation 2 | // All rights reserved. 3 | // 4 | // MIT License: 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | import { Request, Response } from "express"; 25 | import * as bodyParser from "body-parser"; 26 | 27 | module.exports.setup = function(app: any): void { 28 | let path = require("path"); 29 | let express = require("express"); 30 | 31 | // Configure the view engine, views folder and the statics path 32 | app.set("view engine", "pug"); 33 | app.set("views", path.join(__dirname, "views")); 34 | 35 | app.use(bodyParser.json()); 36 | app.use(bodyParser.urlencoded({ 37 | extended: true, 38 | })); 39 | 40 | // Setup home page 41 | app.get("/", function(req: Request, res: Response): void { 42 | res.render("hello"); 43 | }); 44 | 45 | // Setup the static tab 46 | app.get("/hello", function(req: Request, res: Response): void { 47 | res.render("hello"); 48 | }); 49 | 50 | // Setup the configure tab, with first and second as content tabs 51 | app.get("/configure", function(req: Request, res: Response): void { 52 | res.render("configure"); 53 | }); 54 | 55 | app.get("/first", function(req: Request, res: Response): void { 56 | res.render("first"); 57 | }); 58 | 59 | app.get("/second", function(req: Request, res: Response): void { 60 | res.render("second"); 61 | }); 62 | 63 | app.get("/taskmodule", function(req: Request, res: Response): void { 64 | // Render the template, passing the appId so it's included in the rendered HTML 65 | res.render("taskmodule", { appId: process.env.MICROSOFT_APP_ID }); 66 | }); 67 | 68 | app.get("/youtube", function(req: Request, res: Response): void { 69 | res.render("youtube"); 70 | }); 71 | 72 | app.get("/powerapp", function(req: Request, res: Response): void { 73 | res.render("powerapp"); 74 | }); 75 | 76 | app.get("/customform", function(req: Request, res: Response): void { 77 | // Render the template, passing the appId so it's included in the rendered HTML 78 | res.render("customform", { appId: process.env.MICROSOFT_APP_ID }); 79 | }); 80 | 81 | app.post("/register", function(req: Request, res: Response): void { 82 | console.log(`Form body via HTTP POST:\nName: ${req.body.name}\nEmail: ${req.body.email}\nFavorite book: ${req.body.favoriteBook}\n`); 83 | }); 84 | }; 85 | -------------------------------------------------------------------------------- /src/views/taskmodule.pug: -------------------------------------------------------------------------------- 1 | //- Copyright (c) Microsoft Corporation 2 | //- All rights reserved. 3 | //- 4 | //- MIT License: 5 | //- Permission is hereby granted, free of charge, to any person obtaining 6 | //- a copy of this software and associated documentation files (the 7 | //- "Software"), to deal in the Software without restriction, including 8 | //- without limitation the rights to use, copy, modify, merge, publish, 9 | //- distribute, sublicense, and/or sell copies of the Software, and to 10 | //- permit persons to whom the Software is furnished to do so, subject to 11 | //- the following conditions: 12 | //- 13 | //- The above copyright notice and this permission notice shall be 14 | //- included in all copies or substantial portions of the Software. 15 | //- 16 | //- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | //- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | //- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | //- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | //- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | //- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | //- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | //- 24 | //- 25 | 26 | extends layout.pug 27 | 28 | block content 29 | div(class='font-semibold font-title') Microsoft Teams Task Modules Demo 30 | p A task module allows Teams app developers to create one or more custom, modal experiences with custom code or Adaptive cards for their users, particularly useful for initiating and/or completing tasks using a GUI that does not litter a Teams conversation with superfluous, intermediate task completion information. 31 | p Let's try it out! 32 | div(id="buttons" style="text-align: center; margin-left: auto; margin-right: auto") 33 | button(class="taskModuleButton button-primary" id="YouTube") YouTube 34 | div Deep Link 35 | p 36 | button(class="taskModuleButton button-primary" id="PowerApp") PowerApp 37 | div Deep Link 38 | p 39 | button(class="taskModuleButton button-primary" id="customform") Custom Form 40 | div 41 | Deep Link 42 | pre(id="customFormResults" style="white-space: pre-wrap; display: none") 43 | p 44 | button(class="taskModuleButton button-primary" id="adaptivecard1") Adaptive Card (results → tab) 45 | div 46 | Deep Link 47 | pre(id="adaptiveResults" style="white-space: pre-wrap; display: none") 48 | p 49 | button(class="taskModuleButton button-primary" id="adaptivecard2") Adaptive Card (results → bot) 50 | div 51 | Deep Link 52 | script. 53 | // Wire up event handler for the click event 54 | var deepLinks = document.getElementsByClassName("deepLinkWarning"); 55 | for (var i = 0; i < deepLinks.length; i++) { 56 | deepLinks[i].onmousedown = deepLinkAlert; 57 | }; 58 | // If the left button is clicked, show an error message 59 | function deepLinkAlert(event) { 60 | if (event.which === 1) { 61 | event.stopPropagation(); 62 | // event.preventDefault(); 63 | alert("The deep link URL is too long to be used from the browser address bar, but on most browsers, it will work from within an anchor tag () or a button."); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/utils/CardUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation 2 | // All rights reserved. 3 | // 4 | // MIT License: 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | import * as builder from "botbuilder"; 25 | import * as stjs from "stjs"; 26 | import * as adaptiveCard from "adaptivecards"; 27 | 28 | export function renderACAttachment(template: any, data: any): builder.AttachmentType { 29 | // ToDo: 30 | // 1. Optionally validate that the schema is valid (postponed as there are tool/schema issues) 31 | 32 | // Pre-process the template so that template placeholders don't show up for null data values 33 | // Regex: Find everything between {{}} and prepend "#? " to it 34 | template = JSON.parse(JSON.stringify(template).replace(/{{(.+?)}}/g, "{{#? $1}}")); 35 | 36 | // No error handling in the call to stjs functions - what you pass in may be garbage, but it always returns a value 37 | let ac = stjs.select(data) 38 | .transformWith(template) 39 | .root(); 40 | return { 41 | contentType: "application/vnd.microsoft.card.adaptive", 42 | content: ac, 43 | }; 44 | } 45 | 46 | export function renderO365ConnectorAttachment(template: any, data: any): builder.AttachmentType { 47 | // Pre-process the template so that template placeholders don't show up for null data values 48 | // Regex: Find everything between {{}} and prepend "#? " to it 49 | template = JSON.parse(JSON.stringify(template).replace(/{{(.+?)}}/g, "{{#? $1}}")); 50 | 51 | // No error handling in the call to stjs functions - what you pass in may be garbage, but it always returns a value 52 | let card = stjs.select(data) 53 | .transformWith(template) 54 | .root(); 55 | return { 56 | contentType: "application/vnd.microsoft.teams.card.o365connector", 57 | content: card, 58 | }; 59 | } 60 | 61 | export function renderCard(template: any, data: any): any { 62 | // Pre-process the template so that template placeholders don't show up for null data values 63 | // Regex: Find everything between {{}} and prepend "#? " to it 64 | template = JSON.parse(JSON.stringify(template).replace(/{{(.+?)}}/g, "{{#? $1}}")); 65 | 66 | // No error handling in the call to stjs functions - what you pass in may be garbage, but it always returns a value 67 | let card = stjs.select(data) 68 | .transformWith(template) 69 | .root(); 70 | return card; 71 | } 72 | 73 | // This function doesn't work as written (the async logic and error handling aren't right) 74 | // and should (perhaps) be refactored as a promise, but at least it captures the logic for validation 75 | // import * as request from "request"; 76 | // import * as Ajv from "ajv"; 77 | /* function validateSchema(json: any): boolean { 78 | request({ 79 | url: "http://adaptivecards.io/schemas/adaptive-card.json", 80 | json: true, 81 | }, (error, response, body) => { 82 | if (!error && response.statusCode === 200) { 83 | let ajv = new Ajv(); 84 | ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")); 85 | return(ajv.validate(body, json)); 86 | } 87 | }, 88 | ); 89 | return true; 90 | } */ 91 | -------------------------------------------------------------------------------- /deploy.cmd: -------------------------------------------------------------------------------- 1 | @if "%SCM_TRACE_LEVEL%" NEQ "4" @echo off 2 | 3 | :: ---------------------- 4 | :: KUDU Deployment Script 5 | :: Version: 1.0.9 6 | :: ---------------------- 7 | 8 | :: Prerequisites 9 | :: ------------- 10 | 11 | :: Verify node.js installed 12 | where node 2>nul >nul 13 | IF %ERRORLEVEL% NEQ 0 ( 14 | echo Missing node.js executable, please install node.js, if already installed make sure it can be reached from current environment. 15 | goto error 16 | ) 17 | 18 | :: Setup 19 | :: ----- 20 | 21 | setlocal enabledelayedexpansion 22 | 23 | SET ARTIFACTS=%~dp0%..\artifacts 24 | 25 | IF NOT DEFINED DEPLOYMENT_SOURCE ( 26 | SET DEPLOYMENT_SOURCE=%~dp0%. 27 | ) 28 | 29 | IF NOT DEFINED DEPLOYMENT_TARGET ( 30 | SET DEPLOYMENT_TARGET=%ARTIFACTS%\wwwroot 31 | ) 32 | 33 | IF NOT DEFINED NEXT_MANIFEST_PATH ( 34 | SET NEXT_MANIFEST_PATH=%ARTIFACTS%\manifest 35 | 36 | IF NOT DEFINED PREVIOUS_MANIFEST_PATH ( 37 | SET PREVIOUS_MANIFEST_PATH=%ARTIFACTS%\manifest 38 | ) 39 | ) 40 | 41 | IF NOT DEFINED KUDU_SYNC_CMD ( 42 | :: Install kudu sync 43 | echo Installing Kudu Sync 44 | call npm install kudusync -g --silent 45 | IF !ERRORLEVEL! NEQ 0 goto error 46 | 47 | :: Locally just running "kuduSync" would also work 48 | SET KUDU_SYNC_CMD=%appdata%\npm\kuduSync.cmd 49 | ) 50 | goto Deployment 51 | 52 | :: Utility Functions 53 | :: ----------------- 54 | 55 | :SelectNodeVersion 56 | 57 | IF DEFINED KUDU_SELECT_NODE_VERSION_CMD ( 58 | :: The following are done only on Windows Azure Websites environment 59 | call %KUDU_SELECT_NODE_VERSION_CMD% "%DEPLOYMENT_SOURCE%" "%DEPLOYMENT_TARGET%" "%DEPLOYMENT_TEMP%" 60 | IF !ERRORLEVEL! NEQ 0 goto error 61 | 62 | IF EXIST "%DEPLOYMENT_TEMP%\__nodeVersion.tmp" ( 63 | SET /p NODE_EXE=<"%DEPLOYMENT_TEMP%\__nodeVersion.tmp" 64 | IF !ERRORLEVEL! NEQ 0 goto error 65 | ) 66 | 67 | IF EXIST "%DEPLOYMENT_TEMP%\__npmVersion.tmp" ( 68 | SET /p NPM_JS_PATH=<"%DEPLOYMENT_TEMP%\__npmVersion.tmp" 69 | IF !ERRORLEVEL! NEQ 0 goto error 70 | ) 71 | 72 | IF NOT DEFINED NODE_EXE ( 73 | SET NODE_EXE=node 74 | ) 75 | 76 | SET NPM_CMD="!NODE_EXE!" "!NPM_JS_PATH!" 77 | ) ELSE ( 78 | SET NPM_CMD=npm 79 | SET NODE_EXE=node 80 | ) 81 | 82 | goto :EOF 83 | 84 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 85 | :: Deployment 86 | :: ---------- 87 | 88 | :Deployment 89 | echo Handling node.js deployment. 90 | 91 | :: 1. KuduSync 92 | IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" ( 93 | call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 -f "%DEPLOYMENT_SOURCE%" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.hg;.deployment;deploy.cmd" 94 | IF !ERRORLEVEL! NEQ 0 goto error 95 | ) 96 | 97 | :: 2. Select node version 98 | call :SelectNodeVersion 99 | call :ExecuteCmd !NPM_CMD! config set scripts-prepend-node-path true 100 | 101 | :: 3. Install npm packages (including dev dependencies) 102 | IF EXIST "%DEPLOYMENT_TARGET%\package.json" ( 103 | pushd "%DEPLOYMENT_TARGET%" 104 | echo Running npm install of production dependencies! 105 | call :ExecuteCmd !NPM_CMD! install --only=prod 106 | echo Running npm install of dev dependencies. 107 | call :ExecuteCmd !NPM_CMD! install --only=dev 108 | IF !ERRORLEVEL! NEQ 0 goto error 109 | popd 110 | ) 111 | 112 | :: 5. Run gulp transformations 113 | IF EXIST "%DEPLOYMENT_TARGET%\gulpfile.js" ( 114 | echo Running gulp build. 115 | pushd "%DEPLOYMENT_TARGET%" 116 | call :ExecuteCmd !NPM_CMD! run build 117 | IF !ERRORLEVEL! NEQ 0 goto error 118 | popd 119 | ) 120 | 121 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 122 | goto end 123 | 124 | :: Execute command routine that will echo out when error 125 | :ExecuteCmd 126 | setlocal 127 | set _CMD_=%* 128 | call %_CMD_% 129 | if "%ERRORLEVEL%" NEQ "0" echo Failed exitCode=%ERRORLEVEL%, command=%_CMD_% 130 | exit /b %ERRORLEVEL% 131 | 132 | :error 133 | endlocal 134 | echo An error has occurred during web site deployment. 135 | call :exitSetErrorLevel 136 | call :exitFromFunction 2>nul 137 | 138 | :exitSetErrorLevel 139 | exit /b 1 140 | 141 | :exitFromFunction 142 | () 143 | 144 | :end 145 | endlocal 146 | echo Finished successfully. 147 | -------------------------------------------------------------------------------- /src/dialogs/ACGenerator.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation 2 | // All rights reserved. 3 | // 4 | // MIT License: 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | import * as builder from "botbuilder"; 25 | import * as constants from "../constants"; 26 | import * as utils from "../utils"; 27 | import { cardTemplates } from "./CardTemplates"; 28 | import { renderACAttachment, renderO365ConnectorAttachment } from "../utils/CardUtils"; 29 | 30 | // Dialog for the Adaptive Card tester 31 | export class ACGeneratorDialog extends builder.IntentDialog 32 | { 33 | constructor(private dialogId: string) { 34 | super({ recognizeMode: builder.RecognizeMode.onBegin }); 35 | } 36 | 37 | public register(bot: builder.UniversalBot, rootDialog: builder.IntentDialog): void { 38 | bot.dialog(this.dialogId, this); 39 | 40 | this.onBegin((session, args, next) => { this.onDialogBegin(session, args, next); }); 41 | this.onDefault((session) => { this.onMessageReceived(session); }); 42 | } 43 | 44 | // Handle start of dialog 45 | private async onDialogBegin(session: builder.Session, args: any, next: () => void): Promise { 46 | next(); 47 | } 48 | 49 | // Handle message 50 | private async onMessageReceived(session: builder.Session): Promise { 51 | if (session.message.text === "") { 52 | if ((session.message.value !== undefined) && (session.message.value.acBody !== undefined)) { 53 | try { 54 | let card = JSON.parse(session.message.value.acBody); 55 | // Check to see if the body is an Adaptive Card 56 | if ((card.type !== undefined) && (card.type.toLowerCase() === "adaptivecard")) { 57 | session.endDialog(new builder.Message(session).addAttachment( 58 | renderACAttachment(card, null), 59 | )); 60 | } 61 | // Check to see if it's an Office 365 Connector Card 62 | if ((card["@type"] !== undefined) && (card["@type"].toLowerCase() === "messagecard")) { 63 | session.endDialog(new builder.Message(session).addAttachment( 64 | renderO365ConnectorAttachment(card, null), 65 | )); 66 | } 67 | // Check to see if it's a Bot Framework card 68 | if (card.contentType !== undefined) { 69 | session.endDialog(new builder.Message(session).addAttachment(card)); 70 | } 71 | } 72 | catch { 73 | session.send("Error parsing Adaptive Card JSON."); 74 | } 75 | } 76 | else { 77 | console.log("AC payload: " + JSON.stringify(session.message.value)); 78 | } 79 | } 80 | else { 81 | // Message might contain @mentions which we would like to strip off in the response 82 | let text = utils.getTextWithoutMentions(session.message); 83 | if (text === constants.DialogId.ACTester) { 84 | // The user has typed "actester" 85 | session.send(new builder.Message(session).addAttachment( 86 | renderACAttachment(cardTemplates.acTester, null), 87 | )); 88 | } 89 | // session.send("You said: %s", text); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.3/MicrosoftTeams.schema.json", 3 | "manifestVersion": "1.3", 4 | "version": "1.0.0", 5 | "id": "f195eed2-4336-4c33-a11b-a417dcaa8680", 6 | "packageName": "com.microsoft.teams.taskmoduletester", 7 | "developer": { 8 | "name": "Bill Bliss", 9 | "websiteUrl": "https://www.microsoft.com", 10 | "privacyUrl": "https://www.microsoft.com/privacy", 11 | "termsOfUseUrl": "https://www.microsoft.com/termsofuse" 12 | }, 13 | "icons": { 14 | "color": "color_icon.png", 15 | "outline": "outline_icon.png" 16 | }, 17 | "name": { 18 | "short": "Task Module", 19 | "full": "Task Module Test App" 20 | }, 21 | "description": { 22 | "short": "Demo/test app for the Task Module feature", 23 | "full": "This app is used to test the Task Module feature." 24 | }, 25 | "accentColor": "#020081", 26 | "configurableTabs": [ 27 | { 28 | "configurationUrl": "https://taskmoduletest.azurewebsites.net/configure", 29 | "canUpdateConfiguration": true, 30 | "scopes": [ 31 | "team", 32 | "groupchat" 33 | ] 34 | } 35 | ], 36 | "staticTabs": [ 37 | { 38 | "entityId": "com.microsoft.teams.taskmoduletester", 39 | "name": "Tasks!", 40 | "contentUrl": "https://taskmoduletest.azurewebsites.net/taskmodule", 41 | "websiteUrl": "https://taskmoduletest.azurewebsites.net/taskmodule", 42 | "scopes": [ 43 | "personal" 44 | ] 45 | } 46 | ], 47 | "bots": [ 48 | { 49 | "botId": "f195eed2-4336-4c33-a11b-a417dcaa8680", 50 | "scopes": [ 51 | "team", 52 | "personal", 53 | "groupchat" 54 | ], 55 | "commandLists": [ 56 | { 57 | "scopes": [ 58 | "personal" 59 | ], 60 | "commands": [ 61 | { 62 | "title": "tasks", 63 | "description": "Test the task module with an Adaptive card" 64 | }, 65 | { 66 | "title": "bfcard", 67 | "description": "Test task module with a Thumbnail card" 68 | }, 69 | { 70 | "title": "actester", 71 | "description": "See what any Adaptive card looks like in Teams" 72 | } 73 | ] 74 | }, 75 | { 76 | "scopes": [ 77 | "team" 78 | ], 79 | "commands": [ 80 | { 81 | "title": "tasks", 82 | "description": "Test the task module with an Adaptive card" 83 | }, 84 | { 85 | "title": "bfcard", 86 | "description": "Test task module with a Thumbnail card" 87 | }, 88 | { 89 | "title": "actester", 90 | "description": "See what any Adaptive card looks like in Teams" 91 | } 92 | ] 93 | } 94 | ], 95 | "supportsFiles": false, 96 | "isNotificationOnly": false 97 | } 98 | ], 99 | "composeExtensions": [ 100 | { 101 | "botId": "f195eed2-4336-4c33-a11b-a417dcaa8680", 102 | "canUpdateConfiguration": false, 103 | "commands": [ 104 | { 105 | "id": "getRandomText", 106 | "title": "Get some random text for fun", 107 | "description": "Gets some random text and images that you can insert in messages for fun.", 108 | "initialRun": true, 109 | "parameters": [ 110 | { 111 | "name": "cardTitle", 112 | "title": "Card title", 113 | "description": "Card title to use" 114 | } 115 | ] 116 | } 117 | ] 118 | } 119 | ], 120 | "permissions": [ 121 | "identity", 122 | "messageTeamMembers" 123 | ], 124 | "validDomains": [ 125 | "taskmoduletest.azurewebsites.net" 126 | ] 127 | } -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation 2 | // All rights reserved. 3 | // 4 | // MIT License: 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | import * as dotenv from "dotenv"; 25 | dotenv.config({ path: `${process.cwd().replace(/\\/g, "/")}/../../.env` }); // Init environment variables 26 | 27 | import * as express from "express"; 28 | import * as favicon from "serve-favicon"; 29 | import * as bodyParser from "body-parser"; 30 | import * as path from "path"; 31 | import * as logger from "winston"; 32 | import * as winston from "winston"; 33 | import * as builder from "botbuilder"; 34 | import * as msteams from "botbuilder-teams"; 35 | import * as storage from "./storage"; 36 | import { TeamsBot } from "./TeamsBot"; 37 | import { MessagingExtension } from "./MessagingExtension"; 38 | 39 | // initLogger(); 40 | winston.verbose("hello world"); 41 | 42 | // initLogger(); 43 | winston.verbose("hello world"); 44 | 45 | let app = express(); 46 | app.set("port", process.env.PORT || 3333); 47 | app.use(express.static(path.join(__dirname, "../../public"))); 48 | app.use(favicon(path.join(__dirname, "../../public/images", "favicon.ico"))); 49 | app.use(bodyParser.json()); 50 | 51 | // Configure bot storage 52 | let botStorageProvider = process.env.BOT_STORAGE; 53 | let botStorage = null; 54 | switch (botStorageProvider) { 55 | case "mongoDb": 56 | botStorage = new storage.MongoDbBotStorage(process.env.MONGODB_BOT_STATE_COLLECTION, process.env.MONGODB_CONNECTION_STRING); 57 | break; 58 | case "memory": 59 | botStorage = new builder.MemoryBotStorage(); 60 | break; 61 | case "null": 62 | botStorage = new storage.NullBotStorage(); 63 | break; 64 | } 65 | 66 | // Create bot 67 | let connector = new msteams.TeamsChatConnector({ 68 | appId: process.env.MICROSOFT_APP_ID, 69 | appPassword: process.env.MICROSOFT_APP_PASSWORD, 70 | }); 71 | let botSettings = { 72 | storage: botStorage, 73 | }; 74 | let bot = new TeamsBot(connector as builder.ChatConnector, botSettings); 75 | 76 | // Adding a messaging extension to our bot 77 | let messagingExtension = new MessagingExtension(bot); 78 | 79 | // Set up route for the bot to listen. 80 | // NOTE: This endpoint cannot be changed and must be api/messages 81 | app.post("/api/messages", connector.listen()); 82 | 83 | // Log bot errors 84 | bot.on("error", (error: Error) => { 85 | logger.error(error.message); 86 | }); 87 | 88 | // Adding tabs to our app. This will setup routes to various views 89 | let tabs = require("./tabs"); 90 | tabs.setup(app); 91 | 92 | // Configure ping route 93 | app.get("/ping", (req, res) => { 94 | res.status(200).send("OK"); 95 | }); 96 | 97 | // Start our nodejs app 98 | app.listen(app.get("port"), function(): void { 99 | console.log("Express server listening on port " + app.get("port")); 100 | console.log("Bot messaging endpoint: " + process.env.BASE_URI + "/api/messages"); 101 | 102 | logger.verbose("Express server listening on port " + app.get("port")); 103 | logger.verbose("Bot messaging endpoint: " + process.env.BASE_URI + "/api/messages"); 104 | }); 105 | 106 | function initLogger(): void { 107 | 108 | logger.addColors({ 109 | error: "red", 110 | warn: "yellow", 111 | info: "green", 112 | verbose: "cyan", 113 | debug: "blue", 114 | silly: "magenta", 115 | }); 116 | 117 | logger.remove(logger.transports.Console); 118 | logger.add(logger.transports.Console, 119 | { 120 | timestamp: () => { return new Date().toLocaleTimeString(); }, 121 | colorize: (process.env.MONOCHROME_CONSOLE) ? false : true, 122 | prettyPrint: true, 123 | level: "debug", 124 | }, 125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /src/views/customform.pug: -------------------------------------------------------------------------------- 1 | //- Copyright (c) Microsoft Corporation 2 | //- All rights reserved. 3 | //- 4 | //- MIT License: 5 | //- Permission is hereby granted, free of charge, to any person obtaining 6 | //- a copy of this software and associated documentation files (the 7 | //- "Software"), to deal in the Software without restriction, including 8 | //- without limitation the rights to use, copy, modify, merge, publish, 9 | //- distribute, sublicense, and/or sell copies of the Software, and to 10 | //- permit persons to whom the Software is furnished to do so, subject to 11 | //- the following conditions: 12 | //- 13 | //- The above copyright notice and this permission notice shall be 14 | //- included in all copies or substantial portions of the Software. 15 | //- 16 | //- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | //- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | //- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | //- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | //- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | //- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | //- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | //- 24 | //- 25 | 26 | doctype html 27 | html(lang='en') 28 | head 29 | style. 30 | body { 31 | margin: 0; 32 | padding-left: 4px; 33 | padding-right: 4px; 34 | } 35 | title Microsoft Teams Task Module Tester - Custom Form 36 | link(rel='stylesheet', type='text/css', href='/styles/msteams-16.css') 37 | link(rel='stylesheet', type='text/css', href='/styles/custom.css') 38 | script(src="https://unpkg.com/@microsoft/teams-js@1.3.7/dist/MicrosoftTeams.min.js" integrity="sha384-glExfvkpce98dO2oN+diZ/Luv/5qrZJiOvWCeR8ng/ZxlhpvBgHKeVFRURrh+NEC" crossorigin="anonymous") 39 | body(class='theme-light') 40 | script. 41 | microsoftTeams.initialize(); 42 | 43 | //- Handle the Esc key 44 | document.onkeyup = function(event) { 45 | if ((event.key === 27) || (event.key === "Escape")) { 46 | microsoftTeams.tasks.submitTask(null); //- this will return an err object to the completionHandler() 47 | } 48 | } 49 | 50 | //- Retrieve the current Teams theme and set it 51 | let currentTheme = ""; 52 | let queryParameters = getQueryParameters(); 53 | if (queryParameters["theme"] === undefined) { 54 | //- Try getting it from microsoftTeams.GetContext() - this will happen after render time and will flash briefly 55 | microsoftTeams.getContext(function(context) { 56 | if (context && context.theme) { 57 | setTheme(context.theme); 58 | } 59 | }); 60 | } 61 | else { 62 | setTheme(queryParameters["theme"]); 63 | } 64 | 65 | function validateForm() { 66 | let customerInfo = { 67 | name: document.forms["customerForm"]["name"].value, 68 | email: document.forms["customerForm"]["email"].value, 69 | favoriteBook: document.forms["customerForm"]["favoriteBook"].value 70 | } 71 | guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; 72 | let password = document.getElementById("pw").value; 73 | if (guidRegex.test(password)) { 74 | microsoftTeams.tasks.submitTask(customerInfo, password); // hidden feature to test bogus completion appId 75 | } 76 | else { 77 | microsoftTeams.tasks.submitTask(customerInfo, "#{appId}"); //- appId is passed at render time in tabs.ts 78 | } 79 | return true; 80 | } 81 | 82 | //- Parse query parameters into key-value pairs 83 | function getQueryParameters() { 84 | let queryParams = {}; 85 | location.search.substr(1).split("&").forEach(function(item) { 86 | let s = item.split("="), 87 | k = s[0], 88 | v = s[1] && decodeURIComponent(s[1]); 89 | queryParams[k] = v; 90 | }); 91 | return queryParams; 92 | } 93 | 94 | //- Set the desired theme 95 | function setTheme(theme) { 96 | if (theme) { 97 | //- Possible values for theme: 'default', 'light', 'dark' and 'contrast' 98 | document.body.className = 'theme-' + (theme === 'default' ? 'light' : theme); 99 | } 100 | } 101 | div(class='surface') 102 | div(class='panel') 103 | div(class='font-semibold font-title') Enter new customer information: 104 | form(method='POST' id="customerForm" action='/register' onSubmit="return validateForm()") 105 | div 106 | div.form-group(class="form-field-input" style="margin-bottom: 10px; margin-top: 10px") 107 | label(for='name') Name: 108 | input#name.form-control.input-field(type='text', placeholder='first and last' name='name' tabindex=1 autofocus) 109 | div.form-group(class="form-field-input" style="margin-bottom: 10px;") 110 | label(for='email') Email: 111 | input#email.form-control.input-field(type='email', placeholder='name@email.com' name='email' tabindex=2) 112 | div.form-group(class="form-field-input" style="margin-bottom: 10px;") 113 | label(for='favoriteBook') Favorite book: 114 | input#favoriteBook.form-control.input-field(type='text', placeholder='title of book' name='favoriteBook' tabindex=3) 115 | div.form-group(class="form-field-input" style="margin-bottom: 10px;") 116 | label(for='pw') Password: 117 | input#pw.form-control.input-field(type='password' name='password' tabindex=4) 118 | div.form-group(class="form-field-input" style="margin-bottom: 10px;") 119 | label(for='pw2') Confirm password: 120 | input#pw2.form-control.input-field(type='password' name='confirmPassword' style="margin-bottom: 10px;" tabindex=4) 121 | button.btn.button-primary(type='submit' tabindex=5) Sign up 122 | -------------------------------------------------------------------------------- /src/TeamsBot.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation 2 | // All rights reserved. 3 | // 4 | // MIT License: 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | import * as builder from "botbuilder"; 25 | import * as msteams from "botbuilder-teams"; 26 | import * as utils from "./utils"; 27 | import * as logger from "winston"; 28 | 29 | import { RootDialog } from "./dialogs/RootDialog"; 30 | import { fetchTemplates, cardTemplates } from "./dialogs/CardTemplates"; 31 | import { renderACAttachment } from "./utils"; 32 | 33 | export class TeamsBot extends builder.UniversalBot { 34 | 35 | constructor( 36 | public _connector: builder.IConnector, 37 | public _botSettings: any, 38 | ) 39 | { 40 | super(_connector, _botSettings); 41 | this.set("persistentConversationData", true); 42 | 43 | // Handle generic invokes 44 | let teamsConnector = this._connector as msteams.TeamsChatConnector; 45 | teamsConnector.onInvoke(async (event, cb) => { 46 | try { 47 | await this.onInvoke(event, cb); 48 | } catch (e) { 49 | logger.error("Invoke handler failed", e); 50 | cb(e, null, 500); 51 | } 52 | }); 53 | 54 | // Register dialogs 55 | new RootDialog().register(this); 56 | } 57 | 58 | // Handle incoming invoke 59 | private async onInvoke(event: builder.IEvent, cb: (err: Error, body: any, status?: number) => void): Promise { 60 | // console.log("Context: " + JSON.stringify(utils.getContext(event))); 61 | let session = await utils.loadSessionAsync(this, event); 62 | if (session) { 63 | let invokeType = (event as any).name; 64 | let invokeValue = (event as any).value; 65 | if (invokeType === undefined) { 66 | invokeType = null; 67 | } 68 | switch (invokeType) { 69 | case "task/fetch": { 70 | let taskModule = invokeValue.data.taskModule.toLowerCase(); 71 | if (fetchTemplates[taskModule] !== undefined) { 72 | // Return the specified task module response to the bot 73 | cb(null, fetchTemplates[taskModule], 200); 74 | } 75 | else { 76 | cb(new Error(`Error: task module template for ${(invokeValue.taskModule === undefined ? "" : invokeValue.taskModule)} not found.`), null, 500); 77 | } 78 | break; 79 | } 80 | case "task/submit": { 81 | if (invokeValue.data !== undefined) { 82 | // It's a valid task module response 83 | switch (invokeValue.data.taskResponse) { 84 | case "message": 85 | // Echo the results to the chat stream 86 | // Returning a response to the invoke message is not necessary 87 | session.send("**task/submit results from the Adaptive card:**\n```" + JSON.stringify(invokeValue) + "```"); 88 | break; 89 | case "continue": 90 | let fetchResponse = fetchTemplates.submitResponse; 91 | fetchResponse.task.value.card = renderACAttachment(cardTemplates.acSubmitResponse, { results: JSON.stringify(invokeValue.data) }); 92 | cb(null, fetchResponse, 200); 93 | break; 94 | case "final": 95 | // Don't show anything 96 | break; 97 | default: 98 | // It's a response from an HTML task module 99 | cb(null, fetchTemplates.submitMessageResponse, 200); 100 | session.send("**task/submit results from HTML or deep link:**\n\n```" + JSON.stringify(invokeValue.data) + "```"); 101 | } 102 | } 103 | break; 104 | } 105 | // Invokes don't participate in middleware 106 | // If the message is not task/*, simulate a normal message and route it, but remember the original invoke message 107 | case null: { 108 | let fakeMessage: any = { 109 | ...event, 110 | text: invokeValue.command + " " + JSON.stringify(invokeValue), 111 | originalInvoke: event, 112 | }; 113 | 114 | session.message = fakeMessage; 115 | session.dispatch(session.sessionState, session.message, () => { 116 | session.routeToActiveDialog(); 117 | }); 118 | } 119 | } 120 | } 121 | cb(null, ""); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/dialogs/BotFrameworkCard.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation 2 | // All rights reserved. 3 | // 4 | // MIT License: 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | import * as builder from "botbuilder"; 25 | import * as constants from "../constants"; 26 | import * as utils from "../utils"; 27 | import { cardTemplates, fetchTemplates, appRoot } from "./CardTemplates"; 28 | import { taskModuleLink } from "../utils/DeepLinks"; 29 | import { renderCard } from "../utils/CardUtils"; 30 | 31 | // Dialog for the Adaptive Card tester 32 | export class BotFrameworkCard extends builder.IntentDialog 33 | { 34 | constructor(private dialogId: string) { 35 | super({ recognizeMode: builder.RecognizeMode.onBegin }); 36 | } 37 | 38 | public register(bot: builder.UniversalBot, rootDialog: builder.IntentDialog): void { 39 | bot.dialog(this.dialogId, this); 40 | 41 | this.onBegin((session, args, next) => { this.onDialogBegin(session, args, next); }); 42 | this.onDefault((session) => { this.onMessageReceived(session); }); 43 | } 44 | 45 | // Handle start of dialog 46 | private async onDialogBegin(session: builder.Session, args: any, next: () => void): Promise { 47 | next(); 48 | } 49 | 50 | // Handle message 51 | private async onMessageReceived(session: builder.Session): Promise { 52 | // Message might contain @mentions which we would like to strip off in the response 53 | let text = utils.getTextWithoutMentions(session.message); 54 | 55 | let appInfo = { 56 | appId: process.env.MICROSOFT_APP_ID, 57 | }; 58 | let taskModuleUrls = { 59 | url1: taskModuleLink(appInfo.appId, constants.TaskModuleStrings.YouTubeTitle, constants.TaskModuleSizes.youtube.height, constants.TaskModuleSizes.youtube.width, `${appRoot()}/${constants.TaskModuleIds.YouTube}`), 60 | url2: taskModuleLink(appInfo.appId, constants.TaskModuleStrings.PowerAppTitle, constants.TaskModuleSizes.powerapp.height, constants.TaskModuleSizes.powerapp.width, `${appRoot()}/${constants.TaskModuleIds.PowerApp}`), 61 | url3: taskModuleLink(appInfo.appId, constants.TaskModuleStrings.CustomFormTitle, constants.TaskModuleSizes.customform.height, constants.TaskModuleSizes.customform.width, `${appRoot()}/${constants.TaskModuleIds.CustomForm}`), 62 | url4: taskModuleLink(appInfo.appId, constants.TaskModuleStrings.AdaptiveCardTitle, constants.TaskModuleSizes.adaptivecard.height, constants.TaskModuleSizes.adaptivecard.width, null, cardTemplates.adaptiveCard), 63 | url5: taskModuleLink(appInfo.appId, constants.TaskModuleStrings.AdaptiveCardTitle, constants.TaskModuleSizes.adaptivecard.height, constants.TaskModuleSizes.adaptivecard.width, null, cardTemplates.adaptiveCard), 64 | }; 65 | 66 | let cardData: any = { 67 | title: "Task Module - Bot Framework", 68 | subTitleDL: "Deep Links", 69 | instructionsDL: "Click on the buttons below below to open a task module via deep link.", 70 | subTitleTF: "Invoke: task/fetch", 71 | instructionsTF: "Click on the buttons below below to open a task module via task/fetch.", 72 | linkbutton1: constants.TaskModuleStrings.YouTubeName, 73 | url1: taskModuleUrls.url1, 74 | linkbutton2: constants.TaskModuleStrings.PowerAppName, 75 | url2: taskModuleUrls.url2, 76 | linkbutton3: constants.TaskModuleStrings.CustomFormName, 77 | url3: taskModuleUrls.url3, 78 | linkbutton4: constants.TaskModuleStrings.AdaptiveCardSingleName, 79 | url4: taskModuleUrls.url4, 80 | linkbutton5: constants.TaskModuleStrings.AdaptiveCardSequenceName, 81 | url5: taskModuleUrls.url5, 82 | fetchButtonId1: `${constants.TaskModuleIds.YouTube}`, 83 | fetchButtonId2: `${constants.TaskModuleIds.PowerApp}`, 84 | fetchButtonId3: `${constants.TaskModuleIds.CustomForm}`, 85 | fetchButtonId4: `${constants.TaskModuleIds.AdaptiveCard1}`, 86 | fetchButtonId5: `${constants.TaskModuleIds.AdaptiveCard2}`, 87 | fetchButtonTitle1: `${constants.TaskModuleStrings.YouTubeName}`, 88 | fetchButtonTitle2: `${constants.TaskModuleStrings.PowerAppName}`, 89 | fetchButtonTitle3: `${constants.TaskModuleStrings.CustomFormName}`, 90 | fetchButtonTitle4: `${constants.TaskModuleStrings.AdaptiveCardSingleName}`, 91 | fetchButtonTitle5: `${constants.TaskModuleStrings.AdaptiveCardSequenceName}`, 92 | }; 93 | 94 | if (text === constants.DialogId.BFCard) { 95 | // The user has typed "bfcard" - send two cards, one illustrating deep link buttons, and one with task/fetch 96 | session.send(new builder.Message(session).addAttachment( 97 | renderCard(cardTemplates.bfThumbnailDeepLink, cardData), 98 | )); 99 | session.send(new builder.Message(session).addAttachment( 100 | renderCard(cardTemplates.bfThumbnailTaskFetch, cardData), 101 | )); 102 | } 103 | session.endDialog(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation 2 | // All rights reserved. 3 | // 4 | // MIT License: 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | // 24 | // 25 | 26 | var gulp = require('gulp'); 27 | var ts = require('gulp-typescript'); 28 | var tslint = require('gulp-tslint'); 29 | var del = require('del'); 30 | var server = require('gulp-develop-server'); 31 | var mocha = require('gulp-spawn-mocha'); 32 | var sourcemaps = require('gulp-sourcemaps'); 33 | var zip = require('gulp-zip'); 34 | var browserify = require('browserify'); 35 | var source = require('vinyl-source-stream'); 36 | var path = require('path'); 37 | var minimist = require('minimist'); 38 | var fs = require('fs'); 39 | var _ = require('lodash'); 40 | 41 | var knownOptions = { 42 | string: 'packageName', 43 | string: 'packagePath', 44 | string: 'specFilter', 45 | default: {packageName: 'Package.zip', packagePath: path.join(__dirname, '_package'), specFilter: '*'} 46 | }; 47 | var options = minimist(process.argv.slice(2), knownOptions); 48 | 49 | var filesToWatch = ['**/*.ts', '!node_modules/**']; 50 | var filesToLint = ['**/*.ts', '!src/typings/**', '!node_modules/**']; 51 | var staticFiles = ['src/**/*.json', 'src/**/*.pug', '!src/manifest.json']; 52 | var clientJS = 'build/src/TaskModuleTab.js'; 53 | var bundledJS = 'bundle.js'; 54 | var msTeamsLib = './node_modules/@microsoft/teams-js/dist/MicrosoftTeams.min.js'; 55 | 56 | /** 57 | * Clean build output. 58 | */ 59 | gulp.task('clean', (done) => { 60 | del([ 61 | 'build/**/*', 62 | // Azure doesn't like it when we delete build/src 63 | '!build/src' 64 | // 'manifest/**/*' 65 | ]); 66 | done(); 67 | }); 68 | 69 | /** 70 | * Lint all TypeScript files. 71 | */ 72 | gulp.task('ts:lint', (done) => { 73 | if (!process.env.GLITCH_NO_LINT) { 74 | gulp.src(filesToLint) 75 | .pipe(tslint({ 76 | formatter: 'verbose' 77 | })) 78 | .pipe(tslint.report({ 79 | summarizeFailureOutput: true 80 | })); 81 | done(); 82 | } else { 83 | done(); 84 | } 85 | }); 86 | 87 | /** 88 | * Compile TypeScript and include references to library. 89 | */ 90 | gulp.task('ts', gulp.series("clean", (done) => { 91 | var tsProject = ts.createProject('./tsconfig.json', { 92 | // Point to the specific typescript package we pull in, not a machine-installed one 93 | typescript: require('typescript'), 94 | }); 95 | 96 | tsProject 97 | .src() 98 | .pipe(sourcemaps.init()) 99 | .pipe(tsProject()) 100 | .pipe(sourcemaps.write('.', { sourceRoot: function(file) { return file.cwd + '/build'; }})) 101 | .pipe(gulp.dest('build/src')); 102 | done(); 103 | })); 104 | 105 | /** 106 | * Copy statics to build directory. 107 | */ 108 | gulp.task('statics:copy', gulp.series("clean", (done) => { 109 | gulp.src(staticFiles, { base: '.' }) 110 | .pipe(gulp.dest('./build')); 111 | done(); 112 | })); 113 | 114 | /** 115 | * Copy (generated) client TypeScript files to the /scripts directory 116 | */ 117 | gulp.task('client-js', gulp.series("ts", (done) => { 118 | var bundler = browserify({ 119 | entries: clientJS, 120 | ignoreMissing: true, 121 | debug: false 122 | }); 123 | 124 | var bundle = function() { 125 | return bundler 126 | .bundle() 127 | .on('.error', function() {}) 128 | .pipe(source(bundledJS)) 129 | .pipe(gulp.dest('./public/scripts')); 130 | }; 131 | 132 | if (global.isWatching) { 133 | bundler = watchify(bundler); 134 | bundler.on('update', bundle) 135 | } 136 | 137 | bundle(); 138 | done(); 139 | })); 140 | 141 | /** 142 | * Build application. 143 | */ 144 | gulp.task('build', gulp.series("clean", "ts:lint", "ts", "client-js", "statics:copy")); 145 | 146 | /** 147 | * Build manifest 148 | */ 149 | gulp.task('generate-manifest', (done) => { 150 | gulp.src(['./public/images/*_icon.png', 'src/manifest.json']) 151 | .pipe(zip('TaskModule.zip')) 152 | .pipe(gulp.dest('manifest')); 153 | done(); 154 | }); 155 | 156 | /** 157 | * Build debug version of the manifest - 158 | */ 159 | gulp.task('generate-manifest-debug', (done) => { 160 | gulp.src(['./public/images/*_icon.png', 'manifest/debug/manifest.json']) 161 | .pipe(zip('TaskModuleDebug.zip')) 162 | .pipe(gulp.dest('manifest/debug')); 163 | done(); 164 | }); 165 | 166 | /** 167 | * Run tests. 168 | */ 169 | gulp.task('test', gulp.series("ts", "statics:copy", (done) => { 170 | gulp 171 | .src('build/test/' + options.specFilter + '.spec.js', {read: false}) 172 | .pipe(mocha({cwd: 'build/src'})) 173 | .once('error', function () { 174 | process.exit(1); 175 | }) 176 | .once('end', function () { 177 | process.exit(); 178 | }); 179 | done(); 180 | })); 181 | 182 | /** 183 | * Package up app into a ZIP file for Azure deployment. 184 | */ 185 | gulp.task('package', gulp.series("build", (done) => { 186 | var packagePaths = [ 187 | 'build/**/*', 188 | 'public/**/*', 189 | 'web.config', 190 | 'package.json', 191 | '**/node_modules/**', 192 | '!build/src/**/*.js.map', 193 | '!build/test/**/*', 194 | '!build/test', 195 | '!build/src/typings/**/*']; 196 | 197 | //add exclusion patterns for all dev dependencies 198 | var packageJSON = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8')); 199 | var devDeps = packageJSON.devDependencies; 200 | for (var propName in devDeps) { 201 | var excludePattern1 = '!**/node_modules/' + propName + '/**'; 202 | var excludePattern2 = '!**/node_modules/' + propName; 203 | packagePaths.push(excludePattern1); 204 | packagePaths.push(excludePattern2); 205 | } 206 | 207 | gulp.src(packagePaths, { base: '.' }) 208 | .pipe(zip(options.packageName)) 209 | .pipe(gulp.dest(options.packagePath)); 210 | done(); 211 | })); 212 | 213 | gulp.task('server:start', gulp.series("build", (done) => { 214 | server.listen({path: 'build/src/app.js'}, function(error) { 215 | console.log(error); 216 | }); 217 | done(); 218 | })); 219 | 220 | gulp.task('server:restart', gulp.series("build", (done) => { 221 | server.restart(); 222 | done(); 223 | })); 224 | 225 | gulp.task('default', gulp.series("server:start", (done) => { 226 | gulp.watch(filesToWatch, ['server:restart']); 227 | done(); 228 | })); 229 | 230 | gulp.task('default', gulp.series("clean", "generate-manifest", (done) => { 231 | console.log('Build completed. Output in manifest folder'); 232 | done(); 233 | })); -------------------------------------------------------------------------------- /src/dialogs/RootDialog.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation 2 | // All rights reserved. 3 | // 4 | // MIT License: 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | import * as builder from "botbuilder"; 25 | import * as constants from "../constants"; 26 | import * as utils from "../utils"; 27 | import * as logger from "winston"; 28 | import { ACGeneratorDialog } from "./ACGenerator"; 29 | import { renderACAttachment } from "../utils/CardUtils"; 30 | import { cardTemplates, fetchTemplates, appRoot } from "./CardTemplates"; 31 | import { taskModuleLink } from "../utils/DeepLinks"; 32 | import { BotFrameworkCard } from "./BotFrameworkCard"; 33 | // The Adaptive Card version is shown by default, which is why this lives in RootDialog 34 | export class RootDialog extends builder.IntentDialog 35 | { 36 | constructor() { 37 | super(); 38 | } 39 | 40 | // Register the dialogs with the bot 41 | public register(bot: builder.UniversalBot): void { 42 | bot.dialog(constants.DialogId.Root, this); 43 | 44 | this.onBegin((session, args, next) => { logger.verbose("onDialogBegin called"); this.onDialogBegin(session, args, next); }); 45 | this.onDefault((session) => { logger.verbose("onDefault called"); this.onMessageReceived(session); } ); 46 | new ACGeneratorDialog(constants.DialogId.ACTester).register(bot, this); 47 | this.matches(/actester/i, constants.DialogId.ACTester); 48 | new BotFrameworkCard(constants.DialogId.BFCard).register(bot, this); 49 | this.matches(/bfcard/i, constants.DialogId.BFCard); 50 | } 51 | 52 | // Handle start of dialog 53 | private async onDialogBegin(session: builder.Session, args: any, next: () => void): Promise { 54 | next(); 55 | } 56 | 57 | // Handle message 58 | private async onMessageReceived(session: builder.Session): Promise { 59 | console.log("Context: " + JSON.stringify(utils.getContext(null, session))); 60 | if (session.message.text === "") { 61 | console.log("Empty message received"); 62 | // This is a response from a generated AC card 63 | if (session.message.value !== undefined) { 64 | session.send("**Action.Submit results:**\n```" + JSON.stringify(session.message.value) + "```"); 65 | } 66 | } 67 | else { 68 | // If the user says anything, return a card to kick off Task Module flows 69 | // Message might contain @mentions which we would like to strip off in the response 70 | let text = utils.getTextWithoutMentions(session.message); 71 | 72 | let appInfo = { 73 | appId: process.env.MICROSOFT_APP_ID as string, 74 | }; 75 | let taskModuleUrls = { 76 | url1: taskModuleLink(appInfo.appId, constants.TaskModuleStrings.YouTubeTitle, constants.TaskModuleSizes.youtube.height, constants.TaskModuleSizes.youtube.width, `${appRoot()}/${constants.TaskModuleIds.YouTube}`, null, `${appRoot()}/${constants.TaskModuleIds.YouTube}`), 77 | url2: taskModuleLink(appInfo.appId, constants.TaskModuleStrings.PowerAppTitle, constants.TaskModuleSizes.powerapp.height, constants.TaskModuleSizes.powerapp.width, `${appRoot()}/${constants.TaskModuleIds.PowerApp}`, null, `${appRoot()}/${constants.TaskModuleIds.PowerApp}`), 78 | url3: taskModuleLink(appInfo.appId, constants.TaskModuleStrings.CustomFormTitle, constants.TaskModuleSizes.customform.height, constants.TaskModuleSizes.customform.width, `${appRoot()}/${constants.TaskModuleIds.CustomForm}`, null, `${appRoot()}/${constants.TaskModuleIds.CustomForm}`, appInfo.appId), 79 | url4: taskModuleLink(appInfo.appId, constants.TaskModuleStrings.AdaptiveCardTitle, constants.TaskModuleSizes.adaptivecard.height, constants.TaskModuleSizes.adaptivecard.width, null, cardTemplates.adaptiveCard, null, appInfo.appId), 80 | url5: taskModuleLink(appInfo.appId, constants.TaskModuleStrings.AdaptiveCardTitle, constants.TaskModuleSizes.adaptivecard.height, constants.TaskModuleSizes.adaptivecard.width, null, cardTemplates.adaptiveCard, null, appInfo.appId), 81 | }; 82 | 83 | let cardData: any = { 84 | title: "Task Module - Adaptive Card", 85 | subTitle: "Task Module Test Card", 86 | instructions: "Click on the buttons below below to open task modules in various ways. Type 'bfcard' to see the Bot Framework card version.", 87 | linkbutton1: constants.TaskModuleStrings.YouTubeName, 88 | url1: taskModuleUrls.url1, 89 | markdown1: `[${constants.TaskModuleStrings.YouTubeName}](${taskModuleUrls.url1})`, 90 | linkbutton2: constants.TaskModuleStrings.PowerAppName, 91 | url2: taskModuleUrls.url2, 92 | markdown2: `[${constants.TaskModuleStrings.PowerAppName}](${taskModuleUrls.url2})`, 93 | linkbutton3: constants.TaskModuleStrings.CustomFormName, 94 | url3: taskModuleUrls.url3, 95 | markdown3: `[${constants.TaskModuleStrings.CustomFormName}](${taskModuleUrls.url3})`, 96 | linkbutton4: constants.TaskModuleStrings.AdaptiveCardSingleName, 97 | url4: taskModuleUrls.url4, 98 | markdown4: `[${constants.TaskModuleStrings.AdaptiveCardSingleName}](${taskModuleUrls.url4})`, 99 | linkbutton5: constants.TaskModuleStrings.AdaptiveCardSequenceName, 100 | url5: taskModuleUrls.url5, 101 | markdown5: `[${constants.TaskModuleStrings.AdaptiveCardSequenceName}](${taskModuleUrls.url5})`, 102 | fetchButtonId1: `${constants.TaskModuleIds.YouTube}`, 103 | fetchButtonId2: `${constants.TaskModuleIds.PowerApp}`, 104 | fetchButtonId3: `${constants.TaskModuleIds.CustomForm}`, 105 | fetchButtonId4: `${constants.TaskModuleIds.AdaptiveCard1}`, 106 | fetchButtonId5: `${constants.TaskModuleIds.AdaptiveCard2}`, 107 | fetchButtonTitle1: `${constants.TaskModuleStrings.YouTubeName}`, 108 | fetchButtonTitle2: `${constants.TaskModuleStrings.PowerAppName}`, 109 | fetchButtonTitle3: `${constants.TaskModuleStrings.CustomFormName}`, 110 | fetchButtonTitle4: `${constants.TaskModuleStrings.AdaptiveCardSingleName}`, 111 | fetchButtonTitle5: `${constants.TaskModuleStrings.AdaptiveCardSequenceName}`, 112 | tfJsonTitle1: `${constants.TaskModuleStrings.YouTubeName}`, 113 | tfJson1: `${JSON.stringify(fetchTemplates[constants.TaskModuleIds.YouTube])}`, 114 | tfJsonTitle2: `${constants.TaskModuleStrings.PowerAppName}`, 115 | tfJson2: `${JSON.stringify(fetchTemplates[constants.TaskModuleIds.PowerApp])}`, 116 | tfJsonTitle3: `${constants.TaskModuleStrings.CustomFormName}`, 117 | tfJson3: `${JSON.stringify(fetchTemplates[constants.TaskModuleIds.CustomForm])}`, 118 | }; 119 | 120 | session.send(new builder.Message(session).addAttachment( 121 | renderACAttachment(cardTemplates.taskModule, cardData), 122 | )); 123 | // session.send("You said: %s", text); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | products: 4 | - office-teams 5 | - office 6 | - office-365 7 | languages: 8 | - typescript 9 | - nodejs 10 | description: "A task module allows you to create modal popup experiences in your Teams application." 11 | urlFragment: teams-module-node 12 | extensions: 13 | contentType: samples 14 | createdDate: 9/17/2018 6:53:22 PM 15 | --- 16 | 17 | # Microsoft Teams task module - Node.js/TypeScript sample 18 | 19 | A task module allows you to create modal popup experiences in your Teams application. Inside the popup, you can run your own custom HTML/JavaScript code, show an `