├── .gitignore ├── README.md ├── README.zh-cn.md ├── docs ├── assets │ ├── messagingextension_en_us │ │ ├── bot-reg.png │ │ ├── get-app-id.png │ │ ├── manifest-json.png │ │ ├── msg-ext-seattle.png │ │ ├── msg-ext-select-city.png │ │ ├── msg-ext.png │ │ ├── ngrok.png │ │ ├── upload-custom-app.png │ │ └── zip-manifest.png │ ├── readme_zh_cn │ │ ├── CreateAnOutgoingWebhook.jpg │ │ ├── SecurityToken.jpeg │ │ └── chat.png │ ├── tab_en_us │ │ ├── configTab.png │ │ ├── context_reactcomponent.png │ │ ├── manifest_json.png │ │ ├── manifest_zip.png │ │ ├── ngrok.png │ │ └── upload_manifest_zip.png │ └── webhook_en_us │ │ ├── CreateAnOutgoingWebhook.png │ │ ├── SecurityToken.png │ │ ├── appsettings.png │ │ ├── chat.png │ │ └── ngrok.png ├── messagingextension.md ├── messagingextension.zh-cn.md ├── tab.md ├── tab.zh_cn.md ├── webhook.md └── webhook.zh-cn.md ├── images ├── cbd_after_sunset.jpg ├── cities │ ├── beijing.jpg │ ├── dalian.jpg │ ├── guangzhou.jpg │ ├── kualalumpur.jpg │ ├── melbourne.jpg │ ├── osaka.jpg │ ├── seattle.jpg │ ├── shanghai.jpg │ ├── shenzhen.jpg │ ├── sydney.jpg │ ├── tokyo.jpg │ └── xiamen.jpg └── steak.jpg └── src ├── MessagingExtension ├── .template.config │ └── template.json ├── Controllers │ └── MessagingExtensionController.cs ├── Microsoft.Bot.Connector.Teams.cs ├── MicrosoftTeams.MessagingExtension.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── appsettings.Development.json ├── appsettings.json └── manifest │ ├── color.png │ ├── manifest.json │ └── outline.png ├── MicrosoftTeams.Templates.nuspec ├── OutgoingWebhook ├── .template.config │ └── template.json ├── Controllers │ └── MessagesController.cs ├── MicrosoftTeams.OutgoingWebhook.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── Services │ ├── ITeamsAuthProvider.cs │ ├── TeamsAuthProvider.cs │ └── TeamsAuthResponse.cs ├── Startup.cs ├── appsettings.Development.json └── appsettings.json └── Tab ├── .gitignore ├── .template.config └── template.json ├── Program.cs ├── Startup.cs ├── gulpfile.js ├── package-lock.json ├── package.json ├── src ├── MicrosoftTeams.d.ts ├── app │ ├── scripts │ │ ├── TeamsBaseComponent.tsx │ │ ├── client.ts │ │ ├── msTeamsTabConfig.tsx │ │ ├── msTeamsTabRemove.tsx │ │ └── msTeamsTabTab.tsx │ └── web │ │ ├── assets │ │ └── icon.png │ │ ├── index.html │ │ ├── msTeamsTabConfig.html │ │ ├── msTeamsTabRemove.html │ │ ├── msTeamsTabTab.html │ │ ├── privacy.html │ │ └── tou.html └── manifest │ ├── icon-color.png │ ├── icon-outline.png │ └── manifest.json ├── tsconfig-client.json ├── tsconfig.json ├── webpack.config.js └── website.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | [Xx]64/ 19 | [Xx]86/ 20 | [Bb]uild/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | 85 | # Visual Studio profiler 86 | *.psess 87 | *.vsp 88 | *.vspx 89 | *.sap 90 | 91 | # TFS 2012 Local Workspace 92 | $tf/ 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | *.DotSettings.user 101 | 102 | # JustCode is a .NET coding add-in 103 | .JustCode 104 | 105 | # TeamCity is a build add-in 106 | _TeamCity* 107 | 108 | # DotCover is a Code Coverage Tool 109 | *.dotCover 110 | 111 | # NCrunch 112 | _NCrunch_* 113 | .*crunch*.local.xml 114 | nCrunchTemp_* 115 | 116 | # MightyMoose 117 | *.mm.* 118 | AutoTest.Net/ 119 | 120 | # Web workbench (sass) 121 | .sass-cache/ 122 | 123 | # Installshield output folder 124 | [Ee]xpress/ 125 | 126 | # DocProject is a documentation generator add-in 127 | DocProject/buildhelp/ 128 | DocProject/Help/*.HxT 129 | DocProject/Help/*.HxC 130 | DocProject/Help/*.hhc 131 | DocProject/Help/*.hhk 132 | DocProject/Help/*.hhp 133 | DocProject/Help/Html2 134 | DocProject/Help/html 135 | 136 | # Click-Once directory 137 | publish/ 138 | 139 | # Publish Web Output 140 | *.[Pp]ublish.xml 141 | *.azurePubxml 142 | 143 | # TODO: Un-comment the next line if you do not want to checkin 144 | # your web deploy settings because they may include unencrypted 145 | # passwords 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # NuGet Packages 150 | *.nupkg 151 | # The packages folder can be ignored because of Package Restore 152 | **/packages/* 153 | # except build/, which is used as an MSBuild target. 154 | !**/packages/build/ 155 | # Uncomment if necessary however generally it will be regenerated when needed 156 | #!**/packages/repositories.config 157 | # NuGet v3's project.json files produces more ignoreable files 158 | *.nuget.props 159 | *.nuget.targets 160 | 161 | # Microsoft Azure Build Output 162 | csx/ 163 | *.build.csdef 164 | 165 | # Microsoft Azure Emulator 166 | ecf/ 167 | rcf/ 168 | 169 | # Windows Store app package directory 170 | AppPackages/ 171 | BundleArtifacts/ 172 | 173 | # Visual Studio cache files 174 | # files ending in .cache can be ignored 175 | *.[Cc]ache 176 | # but keep track of directories ending in .cache 177 | !*.[Cc]ache/ 178 | 179 | # Others 180 | ClientBin/ 181 | [Ss]tyle[Cc]op.* 182 | ~$* 183 | *~ 184 | *.dbmdl 185 | *.dbproj.schemaview 186 | *.pfx 187 | *.publishsettings 188 | node_modules/ 189 | orleans.codegen.cs 190 | 191 | # RIA/Silverlight projects 192 | Generated_Code/ 193 | 194 | # Backup & report files from converting an old project file 195 | # to a newer Visual Studio version. Backup files are not needed, 196 | # because we have git ;-) 197 | _UpgradeReport_Files/ 198 | Backup*/ 199 | UpgradeLog*.XML 200 | UpgradeLog*.htm 201 | 202 | # SQL Server files 203 | *.mdf 204 | *.ldf 205 | 206 | # Business Intelligence projects 207 | *.rdl.data 208 | *.bim.layout 209 | *.bim_*.settings 210 | 211 | # Microsoft Fakes 212 | FakesAssemblies/ 213 | 214 | # GhostDoc plugin setting file 215 | *.GhostDoc.xml 216 | 217 | # Node.js Tools for Visual Studio 218 | .ntvs_analysis.dat 219 | 220 | # Visual Studio 6 build log 221 | *.plg 222 | 223 | # Visual Studio 6 workspace options file 224 | *.opt 225 | 226 | # Visual Studio LightSwitch build output 227 | **/*.HTMLClient/GeneratedArtifacts 228 | **/*.DesktopClient/GeneratedArtifacts 229 | **/*.DesktopClient/ModelManifest.xml 230 | **/*.Server/GeneratedArtifacts 231 | **/*.Server/ModelManifest.xml 232 | _Pvt_Extensions 233 | 234 | # LightSwitch generated files 235 | GeneratedArtifacts/ 236 | ModelManifest.xml 237 | 238 | # Paket dependency manager 239 | .paket/paket.exe 240 | 241 | # FAKE - F# Make 242 | .fake/ 243 | 244 | .vscode/ 245 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microsoft Teams Templates 2 | 3 | ## Prerequisites 4 | 5 | * [dotnet SDK 3.1](https://dotnet.microsoft.com/download) together with dotnet CLI 6 | 7 | ## Using the template 8 | 9 | * Install or update the template: `dotnet new -i MicrosoftTeams.Templates` 10 | 11 | * [Develop an outgoing webhook](docs/webhook.md) 12 | * [Develop a messaging extension](docs/messagingextension.md) 13 | * [Develop a tab app](docs/tab.md) 14 | -------------------------------------------------------------------------------- /README.zh-cn.md: -------------------------------------------------------------------------------- 1 | # Microsoft Teams Templates 2 | 3 | ## 先决条件 4 | 5 | * [dotnet SDK 3.1](https://dotnet.microsoft.com/download) 6 | 7 | ## 使用模板 8 | 9 | * 安装或者更新模板: `dotnet new -i MicrosoftTeams.Templates` 10 | 11 | * [开发本地传出的webhook](docs/webhook.zh-cn.md) 12 | * [开发messaging extension](docs/messagingextension.zh-cn.md) 13 | * [开发 Teams Tab 应用程序](docs/tab.zh-cn.md) 14 | -------------------------------------------------------------------------------- /docs/assets/messagingextension_en_us/bot-reg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/messagingextension_en_us/bot-reg.png -------------------------------------------------------------------------------- /docs/assets/messagingextension_en_us/get-app-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/messagingextension_en_us/get-app-id.png -------------------------------------------------------------------------------- /docs/assets/messagingextension_en_us/manifest-json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/messagingextension_en_us/manifest-json.png -------------------------------------------------------------------------------- /docs/assets/messagingextension_en_us/msg-ext-seattle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/messagingextension_en_us/msg-ext-seattle.png -------------------------------------------------------------------------------- /docs/assets/messagingextension_en_us/msg-ext-select-city.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/messagingextension_en_us/msg-ext-select-city.png -------------------------------------------------------------------------------- /docs/assets/messagingextension_en_us/msg-ext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/messagingextension_en_us/msg-ext.png -------------------------------------------------------------------------------- /docs/assets/messagingextension_en_us/ngrok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/messagingextension_en_us/ngrok.png -------------------------------------------------------------------------------- /docs/assets/messagingextension_en_us/upload-custom-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/messagingextension_en_us/upload-custom-app.png -------------------------------------------------------------------------------- /docs/assets/messagingextension_en_us/zip-manifest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/messagingextension_en_us/zip-manifest.png -------------------------------------------------------------------------------- /docs/assets/readme_zh_cn/CreateAnOutgoingWebhook.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/readme_zh_cn/CreateAnOutgoingWebhook.jpg -------------------------------------------------------------------------------- /docs/assets/readme_zh_cn/SecurityToken.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/readme_zh_cn/SecurityToken.jpeg -------------------------------------------------------------------------------- /docs/assets/readme_zh_cn/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/readme_zh_cn/chat.png -------------------------------------------------------------------------------- /docs/assets/tab_en_us/configTab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/tab_en_us/configTab.png -------------------------------------------------------------------------------- /docs/assets/tab_en_us/context_reactcomponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/tab_en_us/context_reactcomponent.png -------------------------------------------------------------------------------- /docs/assets/tab_en_us/manifest_json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/tab_en_us/manifest_json.png -------------------------------------------------------------------------------- /docs/assets/tab_en_us/manifest_zip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/tab_en_us/manifest_zip.png -------------------------------------------------------------------------------- /docs/assets/tab_en_us/ngrok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/tab_en_us/ngrok.png -------------------------------------------------------------------------------- /docs/assets/tab_en_us/upload_manifest_zip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/tab_en_us/upload_manifest_zip.png -------------------------------------------------------------------------------- /docs/assets/webhook_en_us/CreateAnOutgoingWebhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/webhook_en_us/CreateAnOutgoingWebhook.png -------------------------------------------------------------------------------- /docs/assets/webhook_en_us/SecurityToken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/webhook_en_us/SecurityToken.png -------------------------------------------------------------------------------- /docs/assets/webhook_en_us/appsettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/webhook_en_us/appsettings.png -------------------------------------------------------------------------------- /docs/assets/webhook_en_us/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/webhook_en_us/chat.png -------------------------------------------------------------------------------- /docs/assets/webhook_en_us/ngrok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/docs/assets/webhook_en_us/ngrok.png -------------------------------------------------------------------------------- /docs/messagingextension.md: -------------------------------------------------------------------------------- 1 | # Develop a messaging extension 2 | 3 | * Run `ngrok.exe http 5000`, because our web api project will listen on port 5000. 4 | * Copy the ngrok url. 5 | ![ngrok](assets/messagingextension_en_us/ngrok.png) 6 | 7 | * Create a bot using Teams App Stdio or the BotFramework web [https://dev.botframework.com/bots/new](https://dev.botframework.com/bots/new). Greater detail can be found [here](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/bots/bots-create). Regards to the `Messaging endpoint`, input the ngrok url with the postfix `/api/extension` 8 | 9 | ![bot-reg](assets/messagingextension_en_us/bot-reg.png) 10 | 11 | * When a bot is created, remember the `Microsoft App ID` which will be used in the next step. 12 | 13 | ![get-app-id](assets/messagingextension_en_us/get-app-id.png) 14 | 15 | * Run `dotnet new teamsmsgext --name SampleApp` to generate the project. 16 | 17 | * Open `SampleApp/manifest/manifest.json` file, paste `Microsoft App ID` you got in previous step. 18 | 19 | ![manifest-json](assets/messagingextension_en_us/manifest-json.png) 20 | 21 | * Zip `SampleApp/manifest` folder into manifest.zip file. 22 | 23 | ![zip-manifest](assets/messagingextension_en_us/zip-manifest.png) 24 | 25 | * Upload manifest.zip file into your Teams. `Manage Team -> Apps`, click the bottom-right link `Upload a custom app` 26 | 27 | ![upload-custom-app](assets/messagingextension_en_us/upload-custom-app.png) 28 | 29 | * Start the project by running `dotnet run`. 30 | 31 | * All done, you can switch to Microsoft Teams, go to a team's channel into which you have uploaded the custom app(the zip file), click the `...` button, you can find your messaging extension. 32 | 33 | ![msg-ext](assets/messagingextension_en_us/msg-ext.png) 34 | 35 | ![msg-ext-select-city](assets/messagingextension_en_us/msg-ext-select-city.png) 36 | 37 | ![msg-ext-seattle](assets/messagingextension_en_us/msg-ext-seattle.png) 38 | 39 | -------------------------------------------------------------------------------- /docs/messagingextension.zh-cn.md: -------------------------------------------------------------------------------- 1 | # 开发一个messaging extension 2 | 3 | * 因为我们的web api会监听5000端口,运行ngrok,讲请求转发到此端口 `ngrok.exe http 5000` 4 | * 拷贝ngrok生成的url. 5 | ![ngrok](assets/messagingextension_en_us/ngrok.png) 6 | 7 | * 使用Teams App Studio或者在Bot Framework网站上创建一个bot 8 | [https://dev.botframework.com/bots/new](https://dev.botframework.com/bots/new). 进一步的详细信息在 [这里](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/bots/bots-create). 对于 `Messaging endpoint`, 填入我们前一步拷贝的ngrok的url,并且在url后加上这个:`/api/extension` 9 | 10 | ![bot-reg](assets/messagingextension_en_us/bot-reg.png) 11 | 12 | * 在bot被成功创建后,记住生成的 `Microsoft App ID` ,我们在下一步会用到这个。 13 | 14 | ![get-app-id](assets/messagingextension_en_us/get-app-id.png) 15 | 16 | * 运行 `dotnet new teamsmsgext --name SampleApp` 来生成一个项目的框架. 17 | 18 | * 用你最喜爱的编辑工具打开 `SampleApp/manifest/manifest.json` 文件, 用上一步骤获取的 `Microsoft App ID` 进行替换. 19 | 20 | ![manifest-json](assets/messagingextension_en_us/manifest-json.png) 21 | 22 | * 打包 `SampleApp/manifest` 这个目录,生成 manifest.zip 文件. 23 | 24 | ![zip-manifest](assets/messagingextension_en_us/zip-manifest.png) 25 | 26 | * 上传 manifest.zip 文件到你的Teams里. `Manage Team -> Apps`, 点击右下方的链接 `Upload a custom app` 27 | 28 | ![upload-custom-app](assets/messagingextension_en_us/upload-custom-app.png) 29 | 30 | * 运行你的项目,执行 `dotnet run`. 31 | 32 | * 搞定了!你现在可以切换到 Microsoft Teams,选择刚才上传zip文件的那个团队,进入这个团队的任意一个聊天频道(channel),点击输入框下方的 `...` 按钮,你就可以看到你自己的 messaging extension了! 33 | 34 | ![msg-ext](assets/messagingextension_en_us/msg-ext.png) 35 | 36 | ![msg-ext-select-city](assets/messagingextension_en_us/msg-ext-select-city.png) 37 | 38 | ![msg-ext-seattle](assets/messagingextension_en_us/msg-ext-seattle.png) 39 | 40 | -------------------------------------------------------------------------------- /docs/tab.md: -------------------------------------------------------------------------------- 1 | # Developing and debugging a tab locally 2 | 3 | ## Prerequisites 4 | 5 | The following tools should be installed. 6 | 7 | 1. [dotnet](https://dotnet.microsoft.com/download) 8 | 2. [node](https://nodejs.org/en/) 9 | 3. [npm](https://www.npmjs.com/) 10 | 11 | ## Steps 12 | 13 | - Install ngrok from [https://ngrok.com/](https://ngrok.com/) 14 | - Run `ngrok.exe http 5000`, because our project will listen on port 5000. 15 | - Copy the ngrok url(https). 16 | ![ngrok](assets/tab_en_us/ngrok.png) 17 | 18 | - Open the manifest.json file, and update it accordingly using copied url. 19 | ![manifest.json](assets/tab_en_us/manifest_json.png) 20 | 21 | - Compress files 'manifest.json' & 'icon-outline.png' & 'icon-color.png' into a file named manifest.zip. The zip file will be uploaded to Teams. 22 | ![manifest.zip](assets/tab_en_us/manifest_zip.png) 23 | 24 | - Run `dotnet new teamstabs` to generate the project. 25 | 26 | - Run command to start the server. 27 | 28 | 1. > dotnet build 29 | 2. > dotnet run 30 | 31 | - Open Microsoft Teams, create a Tab. 32 | 33 | 1. Navigate to 'Apps' tab in a certain team. 34 | 2. Click 'Upload a custom app' at the right bottom corner of the page. 35 | 3. Select manifest.zip and click Open. 36 | ![UploadACustomApp](assets/tab_en_us/upload_manifest_zip.png) 37 | 38 | - Click "MSTeamsTab" app uploaded just now, And set it up for a certain channel. 39 | ![Tab](assets/tab_en_us/context_reactcomponent.png) 40 | -------------------------------------------------------------------------------- /docs/tab.zh_cn.md: -------------------------------------------------------------------------------- 1 | # 本地开发调试 Teams Tab 应用程序 2 | 3 | ## 准备 4 | 5 | 需提前安装: 6 | 7 | 1. [dotnet](https://dotnet.microsoft.com/download) 8 | 2. [node](https://nodejs.org/en/) 9 | 3. [npm](https://www.npmjs.com/) 10 | 11 | ## 步骤 12 | 13 | - 下载 [ngrok](https://ngrok.com/) 14 | - 运行`ngrok.exe http 5000`。因为我们项目启动会监听 5000 端口. 15 | - 拷贝 ngrok 的 url (注意是:http**s**). 16 | ![ngrok](assets/tab_en_us/ngrok.png) 17 | 18 | - 打开 manifest.json 文件, 将相关 url 替换为上一步拷贝的 url。 19 | ![manifest.json](assets/tab_en_us/manifest_json.png) 20 | 21 | - 将 manifest.json、icon-outline.png 和 icon-color.png 三个文件打包为 manifest.zip. 这个 zip 包就是我们要上传到 teams 的应用包。 22 | 23 | ![manifest.zip](assets/tab_en_us/manifest_zip.png) 24 | 25 | - 运行`dotnet new teamstabs`命令生成项目。 26 | 27 | - 运行以下命令. 28 | 29 | 1. > dotnet build 30 | 2. > dotnet run 31 | 32 | - 打开 Microsoft Teams, 创建一个 Tab. 33 | 34 | 1. 找到某个 team 并进入,导航到'Apps'标签页. 35 | 2. 点击页面右下角的'Upload a custom app'(上传自定义应用)按钮. 36 | 3. 选择并上传刚才的压缩包 manifest.zip. 37 | ![UploadACustomApp](assets/tab_en_us/upload_manifest_zip.png) 38 | 39 | - 点击刚才上传的"MSTeamsTab"应用并且为需要的频道进行设置. 40 | ![Tab](assets/tab_en_us/context_reactcomponent.png) 41 | -------------------------------------------------------------------------------- /docs/webhook.md: -------------------------------------------------------------------------------- 1 | # Developing and debugging an outgoing web hook locally 2 | 3 | * Install ngrok from [https://ngrok.com/](https://ngrok.com/) 4 | * Run `ngrok.exe http 5000`, because our web api project will listen on port 5000. 5 | * Copy the ngrok url. 6 | ![ngrok](assets/webhook_en_us/ngrok.png) 7 | * Open Microsoft Teams, create an Outgoing webhook. Input the app name and app description. Paste the ngrok url into "Callback URL" field. You need to append the `api/message` to your url, because this is the endpoint which handles the requests from Microsoft Teams. 8 | ![CreateAnOutgoingwebhook](assets/webhook_en_us/CreateAnOutgoingWebhook.png) 9 | * Click "Create" button. Copy and save the security token for future use. 10 | ![SecurityToken](assets/webhook_en_us/SecurityToken.png) 11 | * Run `dotnet new teamswebhook --name SampleApp` to generate the project. 12 | * Open `SampleApp/appsettings.json` file, replace `[Teams app security token]` with the real security token you just saved. 13 | ![appsettings](assets/webhook_en_us/appsettings.png) 14 | * Start the project by running `dotnet run`. 15 | * All done, you can switch to Microsoft Teams, @ the bot name, your sample project will send the meesage back to you. 16 | ![chat](assets/webhook_en_us/chat.png) 17 | -------------------------------------------------------------------------------- /docs/webhook.zh-cn.md: -------------------------------------------------------------------------------- 1 | ## 开发和调试一个本地传出的web hook 2 | 3 | * 安装ngrok,官网地址: [https://ngrok.com/](https://ngrok.com/) 4 | * 运行 `ngrok.exe http 5000`, 因为我们本地的web api项目将会监听5000端口. 5 | * 复制ngrok中的地址. 6 | ![ngrok](assets/webhook_en_us/ngrok.png) 7 | * 打开Microsoft Teams, 创建一个传出的webhook. 输入应用名称和应用的描述。粘贴ngrok中的url到回调Url框中。还应该加上 `api/message` 到你的回调地址中, 这个才是一个完整的终结点地址处理来自Microsoft Teams的请求。 8 | ![CreateAnOutgoingwebhook](assets/readme_zh_cn/CreateAnOutgoingWebhook.jpg) 9 | * 点击 "创建" 按钮. 复制和保存安全令牌,留着后面备用. 10 | ![SecurityToken](assets/readme_zh_cn/SecurityToken.jpeg) 11 | * 运行命令 `dotnet new teamswebhook --name SampleApp` 来生成项目。 12 | * 打开 `SampleApp/appsettings.json` 文件, 用刚刚复制保存的安全令牌替换这里的 `[Teams app security token]`。 13 | ![appsettings](assets/webhook_en_us/appsettings.png) 14 | * 输入命令 `dotnet run` 运行项目。 15 | * 以上全部完成, 切换到中Microsoft Teams, @机器人应用的名称, 你的应用将会受到请求,响应信息回来. 16 | ![chat](assets/readme_zh_cn/chat.png) 17 | -------------------------------------------------------------------------------- /images/cbd_after_sunset.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/images/cbd_after_sunset.jpg -------------------------------------------------------------------------------- /images/cities/beijing.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/images/cities/beijing.jpg -------------------------------------------------------------------------------- /images/cities/dalian.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/images/cities/dalian.jpg -------------------------------------------------------------------------------- /images/cities/guangzhou.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/images/cities/guangzhou.jpg -------------------------------------------------------------------------------- /images/cities/kualalumpur.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/images/cities/kualalumpur.jpg -------------------------------------------------------------------------------- /images/cities/melbourne.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/images/cities/melbourne.jpg -------------------------------------------------------------------------------- /images/cities/osaka.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/images/cities/osaka.jpg -------------------------------------------------------------------------------- /images/cities/seattle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/images/cities/seattle.jpg -------------------------------------------------------------------------------- /images/cities/shanghai.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/images/cities/shanghai.jpg -------------------------------------------------------------------------------- /images/cities/shenzhen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/images/cities/shenzhen.jpg -------------------------------------------------------------------------------- /images/cities/sydney.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/images/cities/sydney.jpg -------------------------------------------------------------------------------- /images/cities/tokyo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/images/cities/tokyo.jpg -------------------------------------------------------------------------------- /images/cities/xiamen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/images/cities/xiamen.jpg -------------------------------------------------------------------------------- /images/steak.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/images/steak.jpg -------------------------------------------------------------------------------- /src/MessagingExtension/.template.config/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/template", 3 | "author": "Tony Xia", 4 | "classifications": [ "Teams", "MesssagingExtension", "WebAPI" ], 5 | "identity": "MicrosoftTeams.MessagingExtension", 6 | "name": "Microsoft Teams Messaging Extension Application", 7 | "shortName": "teamsmsgext", 8 | "tags": { 9 | "language": "C#", 10 | "type":"project" 11 | }, 12 | "sourceName": "MicrosoftTeams.MessagingExtension", 13 | "preferNameDirectory" : true 14 | } 15 | -------------------------------------------------------------------------------- /src/MessagingExtension/Controllers/MessagingExtensionController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Bot.Connector.Teams; 7 | using Microsoft.Bot.Connector.Teams.Models; 8 | using Microsoft.Bot.Schema; 9 | 10 | namespace MicrosoftTeams.MessagingExtension.Controllers 11 | { 12 | [ApiController] 13 | public class MessagingExtensionController : ControllerBase 14 | { 15 | private static readonly string[] s_sampleCities = new string[] { "Seattle", "Shanghai", "ShenZhen", "Sydney", "Melbourne", "Tokyo", "Osaka", "KualaLumpur" }; 16 | 17 | [HttpPost] 18 | [Route("api/extension")] 19 | public IActionResult Post([FromBody]Activity activity) 20 | { 21 | if (activity.Type == ActivityTypes.Invoke) 22 | { 23 | if (activity.IsComposeExtensionQuery()) 24 | { 25 | // This is the response object that will get sent back to the messaging extension request. 26 | var invokeResponse = new ComposeExtensionResponse(); 27 | 28 | // This helper method gets the query as an object. 29 | var query = activity.GetComposeExtensionQueryData(); 30 | 31 | if (query.CommandId != null && query.Parameters != null && query.Parameters.Count > 0) 32 | { 33 | string[] cities; 34 | if (query.Parameters[0].Name == "initialRun") 35 | { 36 | cities = s_sampleCities; 37 | } 38 | else 39 | { 40 | var keyword = query.Parameters[0].Value.ToString(); 41 | cities = s_sampleCities 42 | .Where(c => c.Contains(keyword, StringComparison.InvariantCultureIgnoreCase)) 43 | .ToArray(); 44 | } 45 | 46 | var results = new ComposeExtensionResult() 47 | { 48 | AttachmentLayout = "list", 49 | Type = "result", 50 | Attachments = BuildAttachments(cities) 51 | }; 52 | invokeResponse.ComposeExtension = results; 53 | } 54 | 55 | // Return the response 56 | return Ok(invokeResponse); 57 | } 58 | } 59 | 60 | // Failure case catch-all. 61 | return BadRequest("Invalid request! This API supports only messaging extension requests. Check your query and try again"); 62 | } 63 | 64 | private List BuildAttachments(IList cities) 65 | { 66 | var attachments = new List(); 67 | foreach (var city in cities) 68 | { 69 | var attachment = new ComposeExtensionAttachment 70 | { 71 | ContentType = ThumbnailCard.ContentType, 72 | Content = CreateSampleThumbnailCard(city, false), 73 | Preview = new Attachment() 74 | { 75 | ContentType = ThumbnailCard.ContentType, 76 | Content = CreateSampleThumbnailCard(city, true), 77 | } 78 | }; 79 | attachments.Add(attachment); 80 | } 81 | return attachments; 82 | } 83 | 84 | private ThumbnailCard CreateSampleThumbnailCard(string city, bool preview) 85 | { 86 | return new ThumbnailCard() 87 | { 88 | Title = city, 89 | Images = new List() 90 | { 91 | new CardImage() 92 | { 93 | Url = "https://github.com/tony-xia/microsoft-teams-templates/raw/master/images/cities/" + city.ToLowerInvariant() + ".jpg" 94 | } 95 | }, 96 | Tap = preview ? null : new CardAction() 97 | { 98 | Type = "openUrl", 99 | Title = "Bing this city", 100 | Value = "https://www.bing.com/images/search?q=" + city 101 | }, 102 | }; 103 | } 104 | 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/MessagingExtension/Microsoft.Bot.Connector.Teams.cs: -------------------------------------------------------------------------------- 1 | // The source code in this file is copied from https://github.com/OfficeDev/BotBuilder-MicrosoftTeams 2 | // because the package "Microsoft.Bot.Connector.Teams" v0.9.0 does not support .NET Standard at the moment. 3 | // When "Microsoft.Bot.Connector.Teams" is ready in future, the code in this file is not needed. 4 | 5 | namespace Microsoft.Bot.Connector.Teams.Models 6 | { 7 | using System.Linq; 8 | using Microsoft.Bot.Schema; 9 | 10 | /// 11 | /// Compose extension attachment. 12 | /// 13 | public partial class ComposeExtensionAttachment : Attachment 14 | { 15 | /// 16 | /// Initializes a new instance of the ComposeExtensionAttachment class. 17 | /// 18 | public ComposeExtensionAttachment() { } 19 | 20 | /// 21 | /// Initializes a new instance of the ComposeExtensionAttachment class. 22 | /// 23 | /// mimetype/Contenttype for the file 24 | /// Content Url 25 | /// Embedded content 26 | /// (OPTIONAL) The name of the attachment 27 | /// (OPTIONAL) Thumbnail associated with 28 | /// attachment 29 | /// Specifies how the result should be displayed 30 | /// in the preview window 31 | public ComposeExtensionAttachment(string contentType = default(string), string contentUrl = default(string), object content = default(object), string name = default(string), string thumbnailUrl = default(string), Attachment preview = default(Attachment)) 32 | : base(contentType, contentUrl, content, name, thumbnailUrl) 33 | { 34 | Preview = preview; 35 | } 36 | 37 | /// 38 | /// Gets or sets specifies how the result should be displayed in the 39 | /// preview window 40 | /// 41 | [Newtonsoft.Json.JsonProperty(PropertyName = "preview")] 42 | public Attachment Preview { get; set; } 43 | 44 | } 45 | 46 | /// 47 | /// Compose extension query parameters 48 | /// 49 | public partial class ComposeExtensionParameter 50 | { 51 | /// 52 | /// Initializes a new instance of the ComposeExtensionParameter class. 53 | /// 54 | public ComposeExtensionParameter() { } 55 | 56 | /// 57 | /// Initializes a new instance of the ComposeExtensionParameter class. 58 | /// 59 | /// Name of the parameter 60 | /// Value of the parameter 61 | public ComposeExtensionParameter(string name = default(string), object value = default(object)) 62 | { 63 | Name = name; 64 | Value = value; 65 | } 66 | 67 | /// 68 | /// Gets or sets name of the parameter 69 | /// 70 | [Newtonsoft.Json.JsonProperty(PropertyName = "name")] 71 | public string Name { get; set; } 72 | 73 | /// 74 | /// Gets or sets value of the parameter 75 | /// 76 | [Newtonsoft.Json.JsonProperty(PropertyName = "value")] 77 | public object Value { get; set; } 78 | 79 | } 80 | 81 | /// 82 | /// Compose extension query 83 | /// 84 | public partial class ComposeExtensionQuery 85 | { 86 | /// 87 | /// Initializes a new instance of the ComposeExtensionQuery class. 88 | /// 89 | public ComposeExtensionQuery() { } 90 | 91 | /// 92 | /// Initializes a new instance of the ComposeExtensionQuery class. 93 | /// 94 | /// Id of the command assigned by Bot 95 | /// Parameters for the query 96 | /// Options for the query 97 | /// State parameter passed back to the bot after 98 | /// authentication/configuration flow 99 | public ComposeExtensionQuery(string commandId = default(string), System.Collections.Generic.IList parameters = default(System.Collections.Generic.IList), ComposeExtensionQueryOptions queryOptions = default(ComposeExtensionQueryOptions), string state = default(string)) 100 | { 101 | CommandId = commandId; 102 | Parameters = parameters; 103 | QueryOptions = queryOptions; 104 | State = state; 105 | } 106 | 107 | /// 108 | /// Gets or sets id of the command assigned by Bot 109 | /// 110 | [Newtonsoft.Json.JsonProperty(PropertyName = "commandId")] 111 | public string CommandId { get; set; } 112 | 113 | /// 114 | /// Gets or sets parameters for the query 115 | /// 116 | [Newtonsoft.Json.JsonProperty(PropertyName = "parameters")] 117 | public System.Collections.Generic.IList Parameters { get; set; } 118 | 119 | /// 120 | /// Gets or sets options for the query 121 | /// 122 | [Newtonsoft.Json.JsonProperty(PropertyName = "queryOptions")] 123 | public ComposeExtensionQueryOptions QueryOptions { get; set; } 124 | 125 | /// 126 | /// Gets or sets state parameter passed back to the bot after 127 | /// authentication/configuration flow 128 | /// 129 | [Newtonsoft.Json.JsonProperty(PropertyName = "state")] 130 | public string State { get; set; } 131 | 132 | } 133 | 134 | /// 135 | /// Compose extensions query options 136 | /// 137 | public partial class ComposeExtensionQueryOptions 138 | { 139 | /// 140 | /// Initializes a new instance of the ComposeExtensionQueryOptions 141 | /// class. 142 | /// 143 | public ComposeExtensionQueryOptions() { } 144 | 145 | /// 146 | /// Initializes a new instance of the ComposeExtensionQueryOptions 147 | /// class. 148 | /// 149 | /// Number of entities to skip 150 | /// Number of entities to fetch 151 | public ComposeExtensionQueryOptions(int? skip = default(int?), int? count = default(int?)) 152 | { 153 | Skip = skip; 154 | Count = count; 155 | } 156 | 157 | /// 158 | /// Gets or sets number of entities to skip 159 | /// 160 | [Newtonsoft.Json.JsonProperty(PropertyName = "skip")] 161 | public int? Skip { get; set; } 162 | 163 | /// 164 | /// Gets or sets number of entities to fetch 165 | /// 166 | [Newtonsoft.Json.JsonProperty(PropertyName = "count")] 167 | public int? Count { get; set; } 168 | 169 | } 170 | 171 | 172 | /// 173 | /// Compose extension response 174 | /// 175 | public partial class ComposeExtensionResponse 176 | { 177 | /// 178 | /// Initializes a new instance of the ComposeExtensionResponse class. 179 | /// 180 | public ComposeExtensionResponse() { } 181 | 182 | /// 183 | /// Initializes a new instance of the ComposeExtensionResponse class. 184 | /// 185 | public ComposeExtensionResponse(ComposeExtensionResult composeExtension = default(ComposeExtensionResult)) 186 | { 187 | ComposeExtension = composeExtension; 188 | } 189 | 190 | /// 191 | /// 192 | [Newtonsoft.Json.JsonProperty(PropertyName = "composeExtension")] 193 | public ComposeExtensionResult ComposeExtension { get; set; } 194 | 195 | } 196 | 197 | 198 | /// 199 | /// Compose extension result 200 | /// 201 | public partial class ComposeExtensionResult 202 | { 203 | /// 204 | /// Initializes a new instance of the ComposeExtensionResult class. 205 | /// 206 | public ComposeExtensionResult() { } 207 | 208 | /// 209 | /// Initializes a new instance of the ComposeExtensionResult class. 210 | /// 211 | /// Hint for how to display multiple 212 | /// attachments. 213 | /// The type of the result 214 | /// (Only when type is result) 215 | /// Attachments 216 | /// (Only when type of auth or config) 217 | /// Suggested actions 218 | /// (Only when type is message) Text 219 | public ComposeExtensionResult(string attachmentLayout = default(string), string type = default(string), System.Collections.Generic.IList attachments = default(System.Collections.Generic.IList), ComposeExtensionSuggestedAction suggestedActions = default(ComposeExtensionSuggestedAction), string text = default(string)) 220 | { 221 | AttachmentLayout = attachmentLayout; 222 | Type = type; 223 | Attachments = attachments; 224 | SuggestedActions = suggestedActions; 225 | Text = text; 226 | } 227 | 228 | /// 229 | /// Gets or sets hint for how to display multiple attachments. 230 | /// 231 | [Newtonsoft.Json.JsonProperty(PropertyName = "attachmentLayout")] 232 | public string AttachmentLayout { get; set; } 233 | 234 | /// 235 | /// Gets or sets the type of the result 236 | /// 237 | [Newtonsoft.Json.JsonProperty(PropertyName = "type")] 238 | public string Type { get; set; } 239 | 240 | /// 241 | /// Gets or sets (Only when type is result) Attachments 242 | /// 243 | [Newtonsoft.Json.JsonProperty(PropertyName = "attachments")] 244 | public System.Collections.Generic.IList Attachments { get; set; } 245 | 246 | /// 247 | /// Gets or sets (Only when type of auth or config) Suggested actions 248 | /// 249 | [Newtonsoft.Json.JsonProperty(PropertyName = "suggestedActions")] 250 | public ComposeExtensionSuggestedAction SuggestedActions { get; set; } 251 | 252 | /// 253 | /// Gets or sets (Only when type is message) Text 254 | /// 255 | [Newtonsoft.Json.JsonProperty(PropertyName = "text")] 256 | public string Text { get; set; } 257 | 258 | } 259 | 260 | 261 | /// 262 | /// Compose extension Actions (Only when type is auth or config) 263 | /// 264 | public partial class ComposeExtensionSuggestedAction 265 | { 266 | /// 267 | /// Initializes a new instance of the ComposeExtensionSuggestedAction 268 | /// class. 269 | /// 270 | public ComposeExtensionSuggestedAction() { } 271 | 272 | /// 273 | /// Initializes a new instance of the ComposeExtensionSuggestedAction 274 | /// class. 275 | /// 276 | /// Actions 277 | public ComposeExtensionSuggestedAction(System.Collections.Generic.IList actions = default(System.Collections.Generic.IList)) 278 | { 279 | Actions = actions; 280 | } 281 | 282 | /// 283 | /// Gets or sets actions 284 | /// 285 | [Newtonsoft.Json.JsonProperty(PropertyName = "actions")] 286 | public System.Collections.Generic.IList Actions { get; set; } 287 | 288 | } 289 | } 290 | 291 | 292 | 293 | namespace Microsoft.Bot.Connector.Teams 294 | { 295 | using System; 296 | using System.Collections.Generic; 297 | using System.Linq; 298 | using Microsoft.Bot.Schema; 299 | using Models; 300 | using Newtonsoft.Json.Linq; 301 | 302 | /// 303 | /// Activity extensions. 304 | /// 305 | public static class ActivityExtensions 306 | { 307 | /// 308 | /// Checks if the activity is a O365 connector card action query. 309 | /// 310 | /// Incoming activity. 311 | /// True is activity is a actionable card query, false otherwise. 312 | public static bool IsO365ConnectorCardActionQuery(this IInvokeActivity activity) 313 | { 314 | return activity.Type == ActivityTypes.Invoke && 315 | !string.IsNullOrEmpty(activity.Name) && 316 | activity.Name.StartsWith("actionableMessage/executeAction", StringComparison.OrdinalIgnoreCase); 317 | } 318 | 319 | /// 320 | /// Checks if the activity is a compose extension query. 321 | /// 322 | /// Incoming activity. 323 | /// True is activity is a compose extension query, false otherwise. 324 | public static bool IsComposeExtensionQuery(this IInvokeActivity activity) 325 | { 326 | return activity.Type == ActivityTypes.Invoke && 327 | !string.IsNullOrEmpty(activity.Name) && 328 | activity.Name.StartsWith("composeExtension", StringComparison.OrdinalIgnoreCase); 329 | } 330 | 331 | /// 332 | /// Gets the compose extension query data. 333 | /// 334 | /// The activity. 335 | /// Compose extension query data. 336 | public static ComposeExtensionQuery GetComposeExtensionQueryData(this IInvokeActivity activity) 337 | { 338 | return JObject.FromObject(activity.Value).ToObject(); 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/MessagingExtension/MicrosoftTeams.MessagingExtension.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/MessagingExtension/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace MicrosoftTeams.MessagingExtension 5 | { 6 | public static class Program 7 | { 8 | public static void Main() 9 | { 10 | CreateHostBuilder(null).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => 16 | { 17 | webBuilder.UseStartup(); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/MessagingExtension/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:17495", 8 | "sslPort": 44324 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "api/values", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "MicrosoftTeams.MessagingExtension": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "api/values", 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/MessagingExtension/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | 7 | namespace MicrosoftTeams.MessagingExtension 8 | { 9 | public class Startup 10 | { 11 | public Startup(IConfiguration configuration) 12 | { 13 | Configuration = configuration; 14 | } 15 | 16 | public IConfiguration Configuration { get; } 17 | 18 | // This method gets called by the runtime. Use this method to add services to the container. 19 | public void ConfigureServices(IServiceCollection services) 20 | { 21 | services.AddControllers(); 22 | } 23 | 24 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 25 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 26 | { 27 | if (env.IsDevelopment()) 28 | { 29 | app.UseDeveloperExceptionPage(); 30 | } 31 | 32 | app.UseRouting(); 33 | 34 | app.UseEndpoints(endpoints => 35 | { 36 | endpoints.MapControllers(); 37 | }); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/MessagingExtension/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/MessagingExtension/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*" 8 | } 9 | -------------------------------------------------------------------------------- /src/MessagingExtension/manifest/color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/src/MessagingExtension/manifest/color.png -------------------------------------------------------------------------------- /src/MessagingExtension/manifest/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.7/MicrosoftTeams.schema.json", 3 | "manifestVersion": "1.7", 4 | "version": "1.0", 5 | "id": "<>", 6 | "packageName": "com.microsoft.teams.yourprojectname", 7 | "developer": { 8 | "name": "YourName", 9 | "websiteUrl": "http://www.yourwebsite.com", 10 | "privacyUrl": "https://privacy.yourwebsite.com", 11 | "termsOfUseUrl": "https://terms.yourwebsite.com" 12 | }, 13 | "icons": { 14 | "color": "color.png", 15 | "outline": "outline.png" 16 | }, 17 | "name": { 18 | "short": "SampleMessagingExtension", 19 | "full": "The full name of this messaging extension" 20 | }, 21 | "description": { 22 | "short": "The short description", 23 | "full": "The full description for this messaging extension" 24 | }, 25 | "accentColor": "#FFFFFF", 26 | "composeExtensions": [ 27 | { 28 | "botId": "<>", 29 | "canUpdateConfiguration": true, 30 | "commands": [ 31 | { 32 | "id": "searchCmd", 33 | "title": "Search", 34 | "description": "Search cities", 35 | "initialRun": true, 36 | "parameters": [ 37 | { 38 | "name": "searchKeyword", 39 | "title": "Keywords", 40 | "description": "Enter your search keywords" 41 | } 42 | ] 43 | } 44 | ] 45 | } 46 | ], 47 | "permissions": [ 48 | "identity", 49 | "messageTeamMembers" 50 | ], 51 | "validDomains": [] 52 | } -------------------------------------------------------------------------------- /src/MessagingExtension/manifest/outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony-xia/microsoft-teams-templates/582ee05acce72bb97f7c5c922259d48880d90457/src/MessagingExtension/manifest/outline.png -------------------------------------------------------------------------------- /src/MicrosoftTeams.Templates.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MicrosoftTeams.Templates 5 | 0.9.6 6 | 7 | Templates for Microsoft Teams apps. 8 | Install or update the template: dotnet new -i MicrosoftTeams.Templates 9 | Develop an outgoing webhook: https://github.com/tony-xia/microsoft-teams-templates/blob/master/docs/webhook.md 10 | Develop a messaging extension: https://github.com/tony-xia/microsoft-teams-templates/blob/master/docs/messagingextension.md 11 | 12 | Microsoft Teams Template 13 | Tony Xia 14 | en-US 15 | https://github.com/tony-xia/microsoft-teams-templates 16 | https://c.s-microsoft.com/en-au/CMSImages/Icon_MicrosoftTeamsTile_85x85.jpg?version=107e4762-aeef-86ae-9f77-c931d9a0ac8c 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/OutgoingWebhook/.template.config/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/template", 3 | "author": "Tony Xia", 4 | "classifications": [ "Teams", "Webhook", "WebAPI" ], 5 | "identity": "MicrosoftTeams.OutgoingWebhook", 6 | "name": "Microsoft Teams Outgoing Webhook Application", 7 | "shortName": "teamswebhook", 8 | "tags": { 9 | "language": "C#", 10 | "type":"project" 11 | }, 12 | "sourceName": "MicrosoftTeams.OutgoingWebhook", 13 | "preferNameDirectory" : true 14 | } 15 | -------------------------------------------------------------------------------- /src/OutgoingWebhook/Controllers/MessagesController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Bot.Schema; 5 | using MicrosoftTeams.OutgoingWebhook.Services; 6 | 7 | namespace MicrosoftTeams.OutgoingWebhook.Controllers; 8 | 9 | [ApiController] 10 | public class MessagesController : ControllerBase 11 | { 12 | private readonly ITeamsAuthProvider _teamsAuth; 13 | 14 | public MessagesController(ITeamsAuthProvider teamsAuth) 15 | { 16 | _teamsAuth = teamsAuth; 17 | } 18 | 19 | [HttpPost] 20 | [Route("api/message")] 21 | public Activity GetMessage([FromBody]Activity activity) 22 | { 23 | var authResult = _teamsAuth.Validate(this.Request); 24 | if (!authResult.AuthSuccessful) 25 | { 26 | return new Activity() 27 | { 28 | Text = "You are not authorized to call into this endpoint." 29 | }; 30 | } 31 | 32 | Attachment attachment = null; 33 | if (activity.Text.Contains("hero", StringComparison.InvariantCultureIgnoreCase)) 34 | { 35 | var card = CreateSampleHeroCard(); 36 | attachment = new Attachment() 37 | { 38 | ContentType = HeroCard.ContentType, 39 | Content = card 40 | }; 41 | } 42 | else if (activity.Text.Contains("thumbnail", StringComparison.InvariantCultureIgnoreCase)) 43 | { 44 | var card = CreateSampleThumbnailCard(); 45 | attachment = new Attachment() 46 | { 47 | ContentType = ThumbnailCard.ContentType, 48 | Content = card 49 | }; 50 | } 51 | 52 | if (attachment != null) 53 | { 54 | return new Activity() 55 | { 56 | Attachments = new List() { attachment } 57 | }; 58 | } 59 | 60 | return new Activity() 61 | { 62 | Text = "Try to type hero or thumbnail." 63 | }; 64 | } 65 | 66 | private HeroCard CreateSampleHeroCard() 67 | { 68 | return new HeroCard() 69 | { 70 | Title = "Superhero", 71 | Subtitle = "An incredible hero", 72 | Text = "Microsoft Teams", 73 | Images = new List() 74 | { 75 | new CardImage() 76 | { 77 | Url = "https://github.com/tony-xia/microsoft-teams-templates/raw/master/images/cbd_after_sunset.jpg" 78 | } 79 | }, 80 | Buttons = new List() 81 | { 82 | new CardAction() 83 | { 84 | Type = "openUrl", 85 | Title = "Visit", 86 | Value = "http://www.microsoft.com" 87 | } 88 | } 89 | }; 90 | } 91 | 92 | private ThumbnailCard CreateSampleThumbnailCard() 93 | { 94 | return new ThumbnailCard() 95 | { 96 | Title = "Teams Sample", 97 | Subtitle = "Outgoing Webhook sample", 98 | Images = new List() 99 | { 100 | new CardImage() 101 | { 102 | Url = "https://github.com/tony-xia/microsoft-teams-templates/raw/master/images/steak.jpg" 103 | } 104 | }, 105 | Buttons = new List() 106 | { 107 | new CardAction() 108 | { 109 | Type = "openUrl", 110 | Title = "Visit", 111 | Value = "http://www.bing.com" 112 | } 113 | } 114 | }; 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/OutgoingWebhook/MicrosoftTeams.OutgoingWebhook.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/OutgoingWebhook/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace MicrosoftTeams.OutgoingWebhook; 5 | 6 | public static class Program 7 | { 8 | public static void Main() 9 | { 10 | CreateHostBuilder(null).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => 16 | { 17 | webBuilder.UseStartup(); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/OutgoingWebhook/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:52326", 8 | "sslPort": 44368 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "api/message", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "MicrosoftTeams.OutgoingWebhook": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "api/message", 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/OutgoingWebhook/Services/ITeamsAuthProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace MicrosoftTeams.OutgoingWebhook.Services; 4 | 5 | public interface ITeamsAuthProvider 6 | { 7 | /// 8 | /// Validates the specified authentication header value. 9 | /// 10 | /// The HTTP request message. 11 | /// 12 | /// Response containing result of validation. 13 | /// 14 | TeamsAuthResponse Validate(HttpRequest request); 15 | } 16 | -------------------------------------------------------------------------------- /src/OutgoingWebhook/Services/TeamsAuthProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Security.Cryptography; 4 | using System.Text; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace MicrosoftTeams.OutgoingWebhook.Services; 10 | 11 | /// 12 | /// Provides authentication results. 13 | /// 14 | public class TeamsAuthProvider : ITeamsAuthProvider 15 | { 16 | private readonly ILogger _logger; 17 | private readonly string _securityToken; 18 | 19 | public TeamsAuthProvider(ILogger logger, IConfiguration configuration) 20 | { 21 | _logger = logger; 22 | _securityToken = configuration.GetValue("TeamsAppSecurityToken"); 23 | } 24 | 25 | /// 26 | /// Validates the specified authentication header value. 27 | /// 28 | /// The HTTP request message. 29 | /// 30 | /// Response containing result of validation. 31 | /// 32 | public TeamsAuthResponse Validate(HttpRequest request) 33 | { 34 | request.Body.Seek(0, SeekOrigin.Begin); 35 | string messageContent = new StreamReader(request.Body).ReadToEnd(); 36 | var authenticationHeaderValue = request.Headers["Authorization"]; 37 | 38 | if (authenticationHeaderValue.Count <= 0) 39 | { 40 | return new TeamsAuthResponse(false, "Authentication header not present on request."); 41 | } 42 | 43 | if (!authenticationHeaderValue[0].StartsWith("HMAC")) 44 | { 45 | return new TeamsAuthResponse(false, "Incorrect authorization header scheme."); 46 | } 47 | 48 | // Reject all empty messages 49 | if (string.IsNullOrEmpty(messageContent)) 50 | { 51 | return new TeamsAuthResponse(false, "Unable to validate authentication header for messages with empty body."); 52 | } 53 | 54 | string providedHmacValue = authenticationHeaderValue[0].Substring("HMAC".Length).Trim(); 55 | string calculatedHmacValue = null; 56 | try 57 | { 58 | byte[] serializedPayloadBytes = Encoding.UTF8.GetBytes(messageContent); 59 | 60 | byte[] keyBytes = Convert.FromBase64String(_securityToken); 61 | using (HMACSHA256 hmacSHA256 = new HMACSHA256(keyBytes)) 62 | { 63 | byte[] hashBytes = hmacSHA256.ComputeHash(serializedPayloadBytes); 64 | calculatedHmacValue = Convert.ToBase64String(hashBytes); 65 | } 66 | 67 | if (string.Equals(providedHmacValue, calculatedHmacValue)) 68 | { 69 | return new TeamsAuthResponse(true, null); 70 | } 71 | else 72 | { 73 | string errorMessage = string.Format( 74 | "AuthHeaderValueMismatch. Expected:'{0}' Provided:'{1}'", 75 | calculatedHmacValue, 76 | providedHmacValue); 77 | return new TeamsAuthResponse(false, errorMessage); 78 | } 79 | } 80 | catch (Exception ex) 81 | { 82 | _logger.LogWarning(ex, "Exception occured while verifying HMAC on the incoming request."); 83 | return new TeamsAuthResponse(false, "Exception thrown while verifying MAC on incoming request."); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/OutgoingWebhook/Services/TeamsAuthResponse.cs: -------------------------------------------------------------------------------- 1 | namespace MicrosoftTeams.OutgoingWebhook.Services; 2 | 3 | /// 4 | /// Encapsulates auth results. 5 | /// 6 | public class TeamsAuthResponse 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// if set to true then [authentication was successful]. 12 | /// The error message. 13 | public TeamsAuthResponse(bool authSuccessful, string errorMessage) 14 | { 15 | this.AuthSuccessful = authSuccessful; 16 | this.ErrorMessage = errorMessage; 17 | } 18 | 19 | /// 20 | /// Gets a value indicating whether [authentication successful]. 21 | /// 22 | /// 23 | /// true if [authentication successful]; otherwise, false. 24 | /// 25 | public bool AuthSuccessful { get; private set; } 26 | 27 | /// 28 | /// Gets the error message. 29 | /// 30 | /// 31 | /// The error message. 32 | /// 33 | public string ErrorMessage { get; private set; } 34 | } 35 | -------------------------------------------------------------------------------- /src/OutgoingWebhook/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using MicrosoftTeams.OutgoingWebhook.Services; 7 | 8 | namespace MicrosoftTeams.OutgoingWebhook; 9 | 10 | public class Startup 11 | { 12 | public Startup(IConfiguration configuration) 13 | { 14 | Configuration = configuration; 15 | } 16 | 17 | public IConfiguration Configuration { get; } 18 | 19 | // This method gets called by the runtime. Use this method to add services to the container. 20 | public void ConfigureServices(IServiceCollection services) 21 | { 22 | services.AddControllers(); 23 | 24 | services.AddSingleton(); 25 | } 26 | 27 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 28 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 29 | { 30 | if (env.IsDevelopment()) 31 | { 32 | app.UseDeveloperExceptionPage(); 33 | } 34 | 35 | app.UseRouting(); 36 | 37 | app.UseEndpoints(endpoints => 38 | { 39 | endpoints.MapControllers(); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/OutgoingWebhook/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/OutgoingWebhook/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*", 8 | "TeamsAppSecurityToken": "[Teams app security token]" 9 | } 10 | -------------------------------------------------------------------------------- /src/Tab/.gitignore: -------------------------------------------------------------------------------- 1 | # do not include the node modules in Git 2 | node_modules 3 | 4 | # do not include the package in Git 5 | package 6 | 7 | # do not include the local environment files 8 | .env 9 | 10 | # do not include the Connectors Json Db file 11 | connectors.json 12 | 13 | # do not include dist folder. 14 | dist -------------------------------------------------------------------------------- /src/Tab/.template.config/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/template", 3 | "author": "Tony Xia", 4 | "classifications": [ "Teams", "Tabs" ], 5 | "identity": "MicrosoftTeams.Tabs", 6 | "name": "Microsoft Teams Tabs", 7 | "shortName": "teamstabs", 8 | "tags": { 9 | "language": "C#", 10 | "type":"project" 11 | }, 12 | "sourceName": "MicrosoftTeams.Tabs", 13 | "preferNameDirectory" : true 14 | } 15 | -------------------------------------------------------------------------------- /src/Tab/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace website 5 | { 6 | public static class Program 7 | { 8 | public static void Main() 9 | { 10 | CreateHostBuilder(null).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => 16 | { 17 | webBuilder.UseStartup(); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Tab/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Hosting; 5 | using Microsoft.Extensions.FileProviders; 6 | using System.IO; 7 | using Microsoft.AspNetCore.Http; 8 | 9 | namespace website 10 | { 11 | public class Startup 12 | { 13 | // This method gets called by the runtime. Use this method to add services to the container. 14 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 15 | public void ConfigureServices(IServiceCollection services) 16 | { 17 | } 18 | 19 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 20 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 21 | { 22 | if (env.IsDevelopment()) 23 | { 24 | app.UseDeveloperExceptionPage(); 25 | } 26 | 27 | app.UseStaticFiles(new StaticFileOptions() 28 | { 29 | FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, @"dist/web")), 30 | RequestPath = new PathString() 31 | }); 32 | 33 | app.UseRouting(); 34 | 35 | app.UseEndpoints(endpoints => 36 | { 37 | endpoints.MapControllers(); 38 | }); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Tab/gulpfile.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Wictor Wilén. All rights reserved. 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // Licensed under the MIT license. 4 | 5 | var gulp = require('gulp'); 6 | var webpack = require('webpack'); 7 | var inject = require('gulp-inject'); 8 | var runSequence = require('run-sequence'); 9 | const zip = require('gulp-zip'); 10 | var nodemon = require('nodemon'); 11 | var argv = require('yargs').argv; 12 | var PluginError = require('plugin-error'); 13 | var log = require('fancy-log'); 14 | var fs = require('fs'); 15 | var ZSchema = require('z-schema'); 16 | var request = require('request'); 17 | var path = require('path'); 18 | 19 | var injectSources = ["./dist/web/scripts/**/*.js", './dist/web/assets/**/*.css'] 20 | var typeScriptFiles = ["./src/**/*.ts?"] 21 | var staticFiles = ["./src/app/**/*.html", "./src/app/**/*.ejs", "./src/app/web/assets/**/*"] 22 | var htmlFiles = ["./src/app/**/*.html", "./src/app/**/*.ejs"] 23 | var watcherfiles = ["./src/**/*.*"] 24 | var manifestFiles = ["./src/manifest/**/*.*"] 25 | 26 | 27 | /** 28 | * Watches source files and invokes the build task 29 | */ 30 | gulp.task('watch', function () { 31 | gulp.watch('./src/**/*.*', ['build']); 32 | }); 33 | 34 | 35 | /** 36 | * Creates the tab manifest 37 | */ 38 | gulp.task('manifest', ['validate-manifest'], () => { 39 | // TODO: add version injection here 40 | gulp.src(manifestFiles) 41 | .pipe(zip('microsoftteamstab.zip')) 42 | .pipe(gulp.dest('package')); 43 | }); 44 | 45 | /** 46 | * Webpack bundling 47 | */ 48 | gulp.task('webpack', function (callback) { 49 | var webpackConfig = require(process.cwd() + '/webpack.config') 50 | webpack(webpackConfig, function (err, stats) { 51 | if (err) throw new PluginError("webpack", err); 52 | 53 | var jsonStats = stats.toJson(); 54 | if (jsonStats.errors.length > 0) { 55 | jsonStats.errors.map(function (e) { 56 | log('[Webpack error] ' + e); 57 | }); 58 | throw new PluginError("webpack", "Webpack errors, see log"); 59 | } 60 | if (jsonStats.warnings.length > 0) { 61 | jsonStats.warnings.map(function (e) { 62 | log('[Webpack warning] ' + e); 63 | }); 64 | } 65 | callback(); 66 | }); 67 | }); 68 | 69 | /** 70 | * Copies static files 71 | */ 72 | gulp.task('static:copy', function () { 73 | return gulp.src(staticFiles, { 74 | base: "./src/app" 75 | }) 76 | .pipe(gulp.dest('./dist/')); 77 | }) 78 | 79 | /** 80 | * Injects script into pages 81 | */ 82 | gulp.task('static:inject', ['static:copy'], function () { 83 | 84 | var injectSrc = gulp.src(injectSources); 85 | 86 | var injectOptions = { 87 | relative: false, 88 | ignorePath: 'dist/web', 89 | addRootSlash: true 90 | }; 91 | 92 | return gulp.src(htmlFiles) 93 | .pipe(inject(injectSrc, injectOptions)) // inserts custom sources 94 | .pipe(gulp.dest('./dist')); 95 | }); 96 | 97 | /** 98 | * Build task, that uses webpack and injects scripts into pages 99 | */ 100 | gulp.task('build', function () { 101 | runSequence('webpack', 'static:inject') 102 | }); 103 | 104 | /** 105 | * Schema validation 106 | */ 107 | gulp.task('validate-manifest', (callback) => { 108 | 109 | var filePath = path.join(__dirname, 'src/manifest/manifest.json'); 110 | fs.readFile(filePath, { 111 | encoding: 'utf-8' 112 | }, function (err, data) { 113 | if (!err) { 114 | var requiredUrl = "https://statics.teams.microsoft.com/sdk/v1.2/manifest/MicrosoftTeams.schema.json"; 115 | var validator = new ZSchema(); 116 | var json = JSON.parse(data); 117 | var schema = { 118 | "$ref": requiredUrl 119 | }; 120 | request(requiredUrl, { 121 | gzip: true 122 | }, (err, res, body) => { 123 | validator.setRemoteReference(requiredUrl, JSON.parse(body)); 124 | 125 | var valid = validator.validate(json, schema); 126 | var errors = validator.getLastErrors(); 127 | if (!valid) { 128 | callback(new PluginError("validate-manifest", errors.map((e) => { 129 | return e.message; 130 | }).join('\n'))); 131 | } else { 132 | callback(); 133 | } 134 | }) 135 | 136 | } else { 137 | callback(PluginError("validate-manifest", err)); 138 | } 139 | }); 140 | }); 141 | 142 | /** 143 | * Task for local debugging 144 | */ 145 | gulp.task('serve', ['build', 'watch'], function (cb) { 146 | var started = false; 147 | var debug = argv.debug !== undefined; 148 | 149 | return nodemon({ 150 | script: 'dist/server.js', 151 | watch: ['dist/server.js'], 152 | nodeArgs: debug ? ['--debug'] : [] 153 | }).on('start', function () { 154 | if (!started) { 155 | cb(); 156 | started = true; 157 | } 158 | }); 159 | }); -------------------------------------------------------------------------------- /src/Tab/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microsoftteamstab", 3 | "version": "0.0.1", 4 | "description": "Generate a Microsoft Teams application.", 5 | "scripts": { 6 | "start": "node dist/server.js", 7 | "build": "gulp build", 8 | "debug": "gulp serve --debug" 9 | }, 10 | "dependencies": { 11 | "@types/react": "16.0.38", 12 | "browser-request": "0.3.3", 13 | "cryptiles": "^4.1.3", 14 | "dotenv": "4.0.0", 15 | "express": "4.16.2", 16 | "express-session": "1.15.6", 17 | "fancy-log": "1.3.2", 18 | "file-loader": "1.1.6", 19 | "lodash": "^4.17.20", 20 | "minimatch": "^3.0.4", 21 | "morgan": "^1.10.0", 22 | "msteams-ui-components-react": "^0.7.3", 23 | "react": "^16.13.1", 24 | "react-dom": "^16.13.1", 25 | "request": "2.83.0", 26 | "typestyle": "1.5.1" 27 | }, 28 | "devDependencies": { 29 | "@types/express": "4.0.39", 30 | "@types/express-session": "1.15.5", 31 | "@types/morgan": "1.7.35", 32 | "@types/request": "2.0.7", 33 | "gulp": "^4.0.2", 34 | "gulp-inject": "4.3.0", 35 | "gulp-zip": "4.0.0", 36 | "nodemon": "^2.0.20", 37 | "plugin-error": "1.0.1", 38 | "run-sequence": "^2.2.1", 39 | "ts-loader": "3.1.1", 40 | "typescript": "2.6.1", 41 | "webpack": "^5.0.0", 42 | "yargs": "^16.0.3", 43 | "z-schema": "^3.19.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Tab/src/MicrosoftTeams.d.ts: -------------------------------------------------------------------------------- 1 | interface MessageEvent { 2 | originalEvent: MessageEvent; 3 | } 4 | /** 5 | * This is the root namespace for the JavaScript SDK. 6 | */ 7 | declare namespace microsoftTeams { 8 | /** 9 | * Initializes the library. This must be called before any other SDK calls. 10 | * The caller should only call this once the frame is loaded successfully. 11 | */ 12 | function initialize(): void; 13 | /** 14 | * Retrieves the current context the frame is running in. 15 | * @param callback The callback to invoke when the {@link Context} object is retrieved. 16 | */ 17 | function getContext(callback: (context: Context) => void): void; 18 | /** 19 | * Registers a handler for when the user changes their theme. 20 | * Only one handler may be registered at a time. Subsequent registrations will override the first. 21 | * @param handler The handler to invoke when the user changes their theme. 22 | */ 23 | function registerOnThemeChangeHandler(handler: (theme: string) => void): void; 24 | /** 25 | * Navigates the frame to a new cross-domain URL. The domain of this URL must match at least one of the 26 | * valid domains specified in the tab manifest; otherwise, an exception will be thrown. This function only 27 | * needs to be used when navigating the frame to a URL in a different domain than the current one in 28 | * a way that keeps the app informed of the change and allows the SDK to continue working. 29 | * @param url The url to navigate the frame to. 30 | */ 31 | function navigateCrossDomain(url: string): void; 32 | /** 33 | * Shares a deep link a user can use to navigate back to a specific state in this page. 34 | */ 35 | function shareDeepLink(deepLinkParameters: DeepLinkParameters): void; 36 | /** 37 | * Namespace to interact with the settings-specific part of the SDK. 38 | * This object is only usable on the settings frame. 39 | */ 40 | namespace settings { 41 | /** 42 | * Sets the validity state for the settings. 43 | * The inital value is false so the user will not be able to save the settings until this is called with true. 44 | * @param validityState A value indicating whether the save or remove button is enabled for the user. 45 | */ 46 | function setValidityState(validityState: boolean): void; 47 | /** 48 | * Gets the settings for the current instance. 49 | * @param callback The callback to invoke when the {@link Settings} object is retrieved. 50 | */ 51 | function getSettings(callback: (settings: Settings) => void): void; 52 | /** 53 | * Sets the settings for the current instance. 54 | * Note that this is an asynchronous operation so there are no guarentees as to when calls 55 | * to getSettings will reflect the changed state. 56 | * @param settings The desired settings for this current instance. 57 | */ 58 | function setSettings(settings: Settings): void; 59 | /** 60 | * Registers a handler for when the user attempts to save the settings. This handler should be used 61 | * to create or update the underlying resource powering the content. 62 | * The object passed to the handler must be used to notify whether to proceed with the save. 63 | * Only one handler may be registered at a time. Subsequent registrations will override the first. 64 | * @param handler The handler to invoke when the user selects the save button. 65 | */ 66 | function registerOnSaveHandler(handler: (evt: SaveEvent) => void): void; 67 | /** 68 | * Registers a handler for when the user attempts to remove the content. This handler should be used 69 | * to remove the underlying resource powering the content. 70 | * The object passed to the handler must be used to notify whether to proceed with the remove 71 | * Only one handler may be registered at a time. Subsequent registrations will override the first. 72 | * @param handler The handler to invoke when the user selects the remove button. 73 | */ 74 | function registerOnRemoveHandler(handler: (evt: RemoveEvent) => void): void; 75 | interface Settings { 76 | /** 77 | * A suggested display name for the new content. 78 | * In the settings for an existing instance being updated, this call has no effect. 79 | */ 80 | suggestedDisplayName?: string; 81 | /** 82 | * Sets the url to use for the content of this instance. 83 | */ 84 | contentUrl: string; 85 | /** 86 | * Sets the remove URL for the remove config experience 87 | */ 88 | removeUrl?: string; 89 | /** 90 | * Sets the url to use for the external link to view the underlying resource in a browser. 91 | */ 92 | websiteUrl?: string; 93 | /** 94 | * The developer-defined unique id for the entity this content points to. 95 | */ 96 | entityId: string; 97 | } 98 | interface SaveEvent { 99 | /** 100 | * Notifies that the underlying resource has been created and the settings may be saved. 101 | */ 102 | notifySuccess(): void; 103 | /** 104 | * Notifies that the underlying resource creation failed and that the settings may not be saved. 105 | * @param reason Specifies a reason for the failure. If provided, this string is displayed to the user. Otherwise a generic error is displayed. 106 | */ 107 | notifyFailure(reason?: string): void; 108 | } 109 | interface RemoveEvent { 110 | /** 111 | * Notifies that the underlying resource has been removed and the content may be removed. 112 | */ 113 | notifySuccess(): void; 114 | /** 115 | * Notifies that the underlying resource removal failed and that the content may not be removed. 116 | * @param reason Specifies a reason for the failure. If provided, this string is displayed to the user. Otherwise a generic error is displayed. 117 | */ 118 | notifyFailure(reason?: string): void; 119 | } 120 | } 121 | /** 122 | * Namespace to interact with the authentication-specific part of the SDK. 123 | * This object is used for starting or completing authentication flows. 124 | */ 125 | namespace authentication { 126 | /** 127 | * Initiates an authentication request which pops up a new windows with the specified settings. 128 | * @param authenticateParameters A set of values that configure the authentication popup. 129 | */ 130 | function authenticate(authenticateParameters: AuthenticateParameters): void; 131 | /** 132 | * Requests an AAD token to be issued on behalf of the app. The token is acquired from the cache 133 | * if it is not expired. Otherwise a request will be sent to AAD to obtain a new token. 134 | * @param authTokenRequest A set of values that configure the token request. 135 | */ 136 | function getAuthToken(authTokenRequest: AuthTokenRequest): void; 137 | /** 138 | * Requests the decoded AAD user identity on behalf of the app. 139 | */ 140 | function getUser(userRequest: UserRequest): void; 141 | /** 142 | * Notifies the frame that initiated this authentication request that the request was successful. 143 | * This function is only usable on the authentication window. 144 | * This call causes the authentication window to be closed. 145 | * @param result Specifies a result for the authentication. If specified, the frame which initiated the authentication popup will recieve this value in their callback. 146 | */ 147 | function notifySuccess(result?: string): void; 148 | /** 149 | * Notifies the frame that initiated this authentication request that the request failed. 150 | * This function is only usable on the authentication window. 151 | * This call causes the authentication window to be closed. 152 | * @param reason Specifies a reason for the authentication failure. If specified, the frame which initiated the authentication popup will recieve this value in their callback. 153 | */ 154 | function notifyFailure(reason?: string): void; 155 | interface AuthenticateParameters { 156 | /** 157 | * The url for the authentication popup 158 | */ 159 | url: string; 160 | /** 161 | * The preferred width for the popup. Note that this value may be ignored if outside the acceptable bounds. 162 | */ 163 | width?: number; 164 | /** 165 | * The preferred height for the popup. Note that this value may be ignored if outside the acceptable bounds. 166 | */ 167 | height?: number; 168 | /** 169 | * A function which is called if the authentication succeeds with the result returned from the authentication popup. 170 | */ 171 | successCallback?: (result?: string) => void; 172 | /** 173 | * A function which is called if the authentication fails with the reason for the failure returned from the authentication popup. 174 | */ 175 | failureCallback?: (reason?: string) => void; 176 | } 177 | interface AuthTokenRequest { 178 | /** 179 | * An array of resource URIs identifying the target resources for which the token should be requested. 180 | */ 181 | resources: string[]; 182 | /** 183 | * A function which is called if the token request succeeds with the resulting token. 184 | */ 185 | successCallback?: (token: string) => void; 186 | /** 187 | * A function which is called if the token request fails with the reason for the failure. 188 | */ 189 | failureCallback?: (reason: string) => void; 190 | } 191 | interface UserRequest { 192 | /** 193 | * A function which is called if the token request succeeds with the resulting token. 194 | */ 195 | successCallback?: (user: UserProfile) => void; 196 | /** 197 | * A function which is called if the token request fails with the reason for the failure. 198 | */ 199 | failureCallback?: (reason: string) => void; 200 | } 201 | interface UserProfile { 202 | /** 203 | * The intended recipient of the token. The application that receives the token must verify that the audience 204 | * value is correct and reject any tokens intended for a different audience. 205 | */ 206 | aud: string; 207 | /** 208 | * Identifies how the subject of the token was authenticated. 209 | */ 210 | amr: string[]; 211 | /** 212 | * Stores the time at which the token was issued. It is often used to measure token freshness. 213 | */ 214 | iat: number; 215 | /** 216 | * Identifies the security token service (STS) that constructs and returns the token. In the tokens that Azure AD 217 | * returns, the issuer is sts.windows.net. The GUID in the Issuer claim value is the tenant ID of the Azure AD 218 | * directory. The tenant ID is an immutable and reliable identifier of the directory. 219 | */ 220 | iss: string; 221 | /** 222 | * Provides the last name, surname, or family name of the user as defined in the Azure AD user object. 223 | */ 224 | family_name: string; 225 | /** 226 | * Provides the first or "given" name of the user, as set on the Azure AD user object. 227 | */ 228 | given_name: string; 229 | /** 230 | * Provides a human readable value that identifies the subject of the token. This value is not guaranteed to 231 | * be unique within a tenant and is designed to be used only for display purposes. 232 | */ 233 | unique_name: string; 234 | /** 235 | * Contains a unique identifier of an object in Azure AD. This value is immutable and cannot be reassigned or 236 | * reused. Use the object ID to identify an object in queries to Azure AD. 237 | */ 238 | oid: string; 239 | /** 240 | * Identifies the principal about which the token asserts information, such as the user of an application. 241 | * This value is immutable and cannot be reassigned or reused, so it can be used to perform authorization 242 | * checks safely. Because the subject is always present in the tokens the Azure AD issues, we recommended 243 | * using this value in a general purpose authorization system. 244 | */ 245 | sub: string; 246 | /** 247 | * An immutable, non-reusable identifier that identifies the directory tenant that issued the token. You can 248 | * use this value to access tenant-specific directory resources in a multi-tenant application. For example, 249 | * you can use this value to identify the tenant in a call to the Graph API. 250 | */ 251 | tid: string; 252 | /** 253 | * Defines the time interval within which a token is valid. The service that validates the token should verify 254 | * that the current date is within the token lifetime, else it should reject the token. The service might allow 255 | * for up to five minutes beyond the token lifetime range to account for any differences in clock time ("time 256 | * skew") between Azure AD and the service. 257 | */ 258 | exp: number; 259 | nbf: number; 260 | /** 261 | * Stores the user name of the user principal. 262 | */ 263 | upn: string; 264 | /** 265 | * Stores the version number of the token. 266 | */ 267 | ver: string; 268 | } 269 | } 270 | interface Context { 271 | /** 272 | * The O365 group id for the team with which the content is associated. 273 | * This field is only available when the identity permission is requested in the manifest. 274 | */ 275 | groupId?: string; 276 | /** 277 | * The Microsoft Teams id for the team with which the content is associated. 278 | */ 279 | teamId?: string; 280 | /** 281 | * The Microsoft Teams id for the channel with which the content is associated. 282 | */ 283 | channelId?: string; 284 | /** 285 | * The developer-defined unique id for the entity this content points to. 286 | */ 287 | entityId: string; 288 | /** 289 | * The developer-defined unique id for the sub-entity this content points to. 290 | * This field should be used to restore to a specific state within an entity, for example scrolling to or activating a specific piece of content. 291 | */ 292 | subEntityId?: string; 293 | /** 294 | * The current locale that the user has configured for the app formatted as 295 | * languageId-countryId (e.g. en-us). 296 | */ 297 | locale: string; 298 | /** 299 | * The current user's upn. 300 | * As a malicious party can host content in a malicious browser, this value should only 301 | * be used as a hint as to who the user is and never as proof of identity. 302 | * This field is only available when the identity permission is requested in the manifest. 303 | */ 304 | upn?: string; 305 | /** 306 | * The current user's AAD tenant id. 307 | * As a malicious party can host content in a malicious browser, this value should only 308 | * be used as a hint as to who the user is and never as proof of identity. 309 | * This field is only available when the identity permission is requested in the manifest. 310 | */ 311 | tid?: string; 312 | /** 313 | * The current UI theme the user is using. 314 | */ 315 | theme?: string; 316 | } 317 | interface DeepLinkParameters { 318 | /** 319 | * The developer-defined unique id for the sub-entity this deep link points to within the current entity. 320 | * This field should be used to restore to a specific state within an entity, for example scrolling to or activating a specific piece of content. 321 | */ 322 | subEntityId: string; 323 | /** 324 | * The label for the sub-entity which should be displayed when the deep link is rendered in a client. 325 | */ 326 | subEntityLabel: string; 327 | /** 328 | * The fallback url to navigate the user to if there is no support for rendering the page inside the client. 329 | * This url should lead directly to the sub-entity. 330 | */ 331 | subEntityWebUrl?: string; 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /src/Tab/src/app/scripts/TeamsBaseComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { ThemeStyle, ITeamsComponentProps, ITeamsComponentState } from 'msteams-ui-components-react'; 4 | 5 | /** 6 | * State interface for the Teams Base user interface React component 7 | */ 8 | export interface ITeamsBaseComponentState extends ITeamsComponentState { 9 | fontSize: number; 10 | theme: ThemeStyle; 11 | } 12 | 13 | /** 14 | * Properties interface for the Teams Base user interface React component 15 | */ 16 | export interface ITeamsBaseComponentProps extends ITeamsComponentProps { 17 | 18 | } 19 | 20 | 21 | /** 22 | * Base implementation of the React based interface for the Microsoft Teams app 23 | */ 24 | export class TeamsBaseComponent

25 | extends React.Component { 26 | 27 | constructor(props: P, state: S) { 28 | super(props, state); 29 | } 30 | 31 | public static render

(element: HTMLElement, props: P) { 32 | render(React.createElement(this, props), element); 33 | } 34 | 35 | public setValidityState(val: boolean) { 36 | microsoftTeams.settings.setValidityState(val); 37 | } 38 | 39 | 40 | protected pageFontSize = () => { 41 | let sizeStr = window.getComputedStyle(document.getElementsByTagName('html')[0]).getPropertyValue('font-size'); 42 | sizeStr = sizeStr.replace('px', ''); 43 | let fontSize = parseInt(sizeStr, 10); 44 | if (!fontSize) { 45 | fontSize = 16; 46 | } 47 | return fontSize; 48 | } 49 | protected inTeams = () => { 50 | try { 51 | return window.self !== window.top; 52 | } catch (e) { 53 | return true; 54 | } 55 | } 56 | 57 | protected updateTheme = (themeStr) => { 58 | let theme; 59 | switch (themeStr) { 60 | case 'dark': 61 | theme = ThemeStyle.Dark; 62 | break; 63 | case 'contrast': 64 | theme = ThemeStyle.HighContrast; 65 | break; 66 | case 'default': 67 | default: 68 | theme = ThemeStyle.Light; 69 | } 70 | this.setState({ theme }); 71 | } 72 | 73 | protected getQueryVariable = (variable) => { 74 | const query = window.location.search.substring(1); 75 | const vars = query.split('&'); 76 | for (const varPairs of vars) { 77 | const pair = varPairs.split('='); 78 | if (decodeURIComponent(pair[0]) === variable) { 79 | return decodeURIComponent(pair[1]); 80 | } 81 | } 82 | return null; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Tab/src/app/scripts/client.ts: -------------------------------------------------------------------------------- 1 | // Default entry point for client scripts 2 | // Automatically generated 3 | // Please avoid from modifying to much... 4 | 5 | 6 | // Added by generator-teams 7 | export * from './msTeamsTabConfig'; 8 | export * from './msTeamsTabTab'; 9 | export * from './msTeamsTabRemove'; 10 | -------------------------------------------------------------------------------- /src/Tab/src/app/scripts/msTeamsTabConfig.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | PrimaryButton, 4 | TeamsComponentContext, 5 | ConnectedComponent, 6 | Panel, 7 | PanelBody, 8 | PanelHeader, 9 | PanelFooter, 10 | Input, 11 | Surface 12 | } from 'msteams-ui-components-react'; 13 | import { render } from 'react-dom'; 14 | import { TeamsBaseComponent, ITeamsBaseComponentProps, ITeamsBaseComponentState } from './TeamsBaseComponent' 15 | 16 | export interface ImsTeamsTabConfigState extends ITeamsBaseComponentState { 17 | value: string; 18 | } 19 | 20 | export interface ImsTeamsTabConfigProps extends ITeamsBaseComponentProps { 21 | 22 | } 23 | 24 | /** 25 | * Implementation of MSTeams.Tab configuration page 26 | */ 27 | export class msTeamsTabConfig extends TeamsBaseComponent { 28 | 29 | public componentWillMount() { 30 | this.updateTheme(this.getQueryVariable('theme')); 31 | this.setState({ 32 | fontSize: this.pageFontSize() 33 | }); 34 | 35 | if (this.inTeams()) { 36 | microsoftTeams.initialize(); 37 | 38 | microsoftTeams.getContext((context: microsoftTeams.Context) => { 39 | this.setState({ 40 | value: context.entityId 41 | }); 42 | this.setValidityState(true); 43 | }); 44 | 45 | microsoftTeams.settings.registerOnSaveHandler((saveEvent: microsoftTeams.settings.SaveEvent) => { 46 | // Calculate host dynamically to enable local debugging 47 | let host = "https://" + window.location.host; 48 | microsoftTeams.settings.setSettings({ 49 | contentUrl: host + "/msTeamsTabTab.html?data=", 50 | suggestedDisplayName: 'MSTeams.Tab', 51 | removeUrl: host + "/msTeamsTabRemove.html", 52 | entityId: this.state.value 53 | }); 54 | saveEvent.notifySuccess(); 55 | }); 56 | } 57 | } 58 | 59 | public render() { 60 | return ( 61 | 65 | 66 | { 67 | const { context } = props; 68 | const { rem, font } = context; 69 | const { sizes, weights } = font; 70 | const styles = { 71 | header: { ...sizes.title, ...weights.semibold }, 72 | section: { ...sizes.base, marginTop: rem(1.4), marginBottom: rem(1.4) }, 73 | input: {}, 74 | } 75 | 76 | return ( 77 | 78 | 79 | 80 |

Configure your tab
81 | 82 | 83 |
84 | { 92 | this.setState({ 93 | value: e.target.value 94 | }) 95 | }} 96 | required /> 97 |
98 | 99 |
100 | 101 | 102 | 103 | 104 | ); 105 | }}> 106 | 107 | 108 | ); 109 | } 110 | } -------------------------------------------------------------------------------- /src/Tab/src/app/scripts/msTeamsTabRemove.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | PrimaryButton, 4 | TeamsComponentContext, 5 | ConnectedComponent, 6 | Panel, 7 | PanelBody, 8 | PanelHeader, 9 | PanelFooter, 10 | Input, 11 | Surface 12 | } from 'msteams-ui-components-react'; 13 | import { TeamsBaseComponent, ITeamsBaseComponentProps, ITeamsBaseComponentState } from './TeamsBaseComponent' 14 | 15 | export interface ImsTeamsTabRemoveState extends ITeamsBaseComponentState { 16 | value: string; 17 | } 18 | export interface ImsTeamsTabRemoveProps extends ITeamsBaseComponentProps { 19 | 20 | } 21 | 22 | /** 23 | * Implementation of MSTeams.Tab remove page 24 | */ 25 | export class msTeamsTabRemove extends TeamsBaseComponent { 26 | 27 | public componentWillMount() { 28 | this.updateTheme(this.getQueryVariable('theme')); 29 | this.setState({ 30 | fontSize: this.pageFontSize() 31 | }); 32 | 33 | if (this.inTeams()) { 34 | microsoftTeams.initialize(); 35 | } 36 | } 37 | 38 | public render() { 39 | return ( 40 | 44 | 45 | { 46 | const { context } = props; 47 | const { rem, font } = context; 48 | const { sizes, weights } = font; 49 | const styles = { 50 | header: { ...sizes.title, ...weights.semibold }, 51 | section: { ...sizes.base, marginTop: rem(1.4), marginBottom: rem(1.4) }, 52 | input: {}, 53 | } 54 | 55 | return ( 56 | 57 | 58 | 59 |
You're about to remove your tab...
60 |
61 | 62 |
63 | You can just add stuff here if you want to clean up when removing the tab. For instance, if you have stored data in an external repository, you can delete or archive it here. If you don't need this remove page you can remove it. 64 |
65 | 66 |
67 | 68 | 69 |
70 |
71 | ); 72 | }}> 73 |
74 |
75 | ); 76 | } 77 | } -------------------------------------------------------------------------------- /src/Tab/src/app/scripts/msTeamsTabTab.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | PrimaryButton, 4 | TeamsComponentContext, 5 | ConnectedComponent, 6 | Panel, 7 | PanelBody, 8 | PanelHeader, 9 | PanelFooter, 10 | Surface, 11 | 12 | Radiobutton, 13 | RadiobuttonGroup, 14 | Checkbox, 15 | CheckboxGroup, 16 | Anchor, 17 | Dropdown, 18 | Input, 19 | Tab, 20 | TextArea, 21 | Toggle, 22 | ThemeStyle, 23 | IContext, 24 | IDropdownItemProps, 25 | } from 'msteams-ui-components-react'; 26 | import { render } from 'react-dom'; 27 | import { TeamsBaseComponent, ITeamsBaseComponentProps, ITeamsBaseComponentState } from './TeamsBaseComponent'; 28 | 29 | /** 30 | * State for the msTeamsTabTab React component 31 | */ 32 | export interface ImsTeamsTabTabState extends ITeamsBaseComponentState { 33 | // MS Team SDK 'context' related state. 34 | groupId?: string | undefined; 35 | teamId?: string; 36 | channelId?: string; 37 | entityId?: string; 38 | subEntityId?: string; 39 | locale?: string; 40 | upn?: string; 41 | tid?: string; 42 | theme: ThemeStyle; 43 | 44 | // react component related state. 45 | toggledRadioButton?: boolean; 46 | selectedTab?: string; 47 | selectedChoice?: string; 48 | checkboxChecked?: boolean; 49 | notifySuccessResult?: any; 50 | } 51 | 52 | /** 53 | * Properties for the msTeamsTabTab React component 54 | */ 55 | export interface ImsTeamsTabTabProps extends ITeamsBaseComponentProps { 56 | 57 | } 58 | 59 | /** 60 | * Implementation of the MSTeams.Tab content page 61 | */ 62 | export class msTeamsTabTab extends TeamsBaseComponent { 63 | 64 | public componentWillMount() { 65 | this.updateTheme(this.getQueryVariable('theme')); 66 | this.setState({ 67 | selectedTab: 'tabA', 68 | toggledRadioButton: false, 69 | fontSize: 12, 70 | theme: ThemeStyle.Light, 71 | context: {} as IContext, 72 | selectedChoice: 'select member to display mail', 73 | checkboxChecked: false, 74 | }); 75 | 76 | if (this.inTeams()) { 77 | microsoftTeams.initialize(); 78 | microsoftTeams.registerOnThemeChangeHandler(this.updateTheme); 79 | microsoftTeams.getContext(context => { 80 | this.setState({ 81 | groupId: context.groupId, 82 | teamId: context.teamId, 83 | channelId: context.channelId, 84 | entityId: context.entityId, 85 | subEntityId: context.subEntityId, 86 | locale: context.locale, 87 | upn: context.upn, 88 | tid: context.tid, 89 | }); 90 | }); 91 | } else { 92 | this.setState({ 93 | entityId: "This is not hosted in Microsoft Teams" 94 | }); 95 | } 96 | } 97 | 98 | /** 99 | * The render() method to create the UI of the tab 100 | */ 101 | public render() { 102 | return ( 103 | 107 | 108 | { 109 | const { context } = props; 110 | const { rem, font } = context; 111 | const { sizes, weights } = font; 112 | const styles = { 113 | header: { ...sizes.title, ...weights.semibold }, 114 | section: { ...sizes.base, marginTop: rem(1.4), marginBottom: rem(1.4) }, 115 | footer: { ...sizes.xsmall } 116 | } 117 | 118 | return ( 119 | 120 | 121 | 122 |
This is your tab
123 |
124 | 125 |
Information from JS SDK context:
126 |
127 |
groupId: {this.state.groupId}
128 |
teamId: {this.state.teamId}
129 |
channelId: {this.state.channelId}
130 |
entityId: {this.state.entityId}
131 |
locale: {this.state.locale}
132 |
upn: {this.state.upn}
133 |
tid: {this.state.tid}
134 |
135 | 136 |
React UI Components:
137 |
138 | { 143 | alert(`checked:${checked}, value:${value}`); 144 | this.setState({ toggledRadioButton: !this.state.toggledRadioButton }) 145 | }} /> 146 | { 151 | alert(`checked:${checked}, value:${value}`); 152 | this.setState({ toggledRadioButton: this.state.toggledRadioButton }) 153 | }} /> 154 | 155 | { alert(`checked:${checked}, value:${value}`) }} /> 156 | { alert(JSON.stringify(values)) }} /> 157 | 158 | this.setState({ selectedChoice: 'CA' }) }, 163 | { text: 'NY', onClick: () => this.setState({ selectedChoice: 'NY' }) }, 164 | ]} 165 | /> 166 | 167 | this.setState({ selectedTab: 'tabA' }), id: 'tabA' }, 169 | { text: 'tabB', onSelect: () => this.setState({ selectedTab: 'tabB' }), id: 'tabB' } 170 | ]} 171 | selectedTabId={this.state.selectedTab} 172 | autoFocus={false} /> 173 |