├── .deployment ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Deployment └── azuredeploy.json ├── LICENSE ├── Manifest ├── ar.json ├── color.png ├── de.json ├── en.json ├── es.json ├── fr.json ├── he.json ├── ja.json ├── ko.json ├── manifest.json ├── outline.png ├── pt-BR.json ├── ru.json ├── zh-CN.json └── zn-TW.json ├── README.md ├── SECURITY.md ├── Source ├── ExpertFinder │ ├── AdapterWithErrorHandler.cs │ ├── Bots │ │ ├── BotLocalizationCultureProvider.cs │ │ └── ExpertFinderBot.cs │ ├── Cards │ │ ├── HelpCard.cs │ │ ├── MessagingExtensionUserProfileCard.cs │ │ ├── MyProfileCard.cs │ │ ├── SearchCard.cs │ │ └── WelcomeCard.cs │ ├── ClientApp │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── public │ │ │ └── index.html │ │ ├── src │ │ │ ├── api │ │ │ │ ├── axiosDecorator.ts │ │ │ │ └── profileSearchApi.ts │ │ │ ├── app.tsx │ │ │ ├── components │ │ │ │ ├── emptySearchResultMessage.tsx │ │ │ │ ├── errorPage.tsx │ │ │ │ ├── filterCheckboxGroup.tsx │ │ │ │ ├── filterNamesComponent.tsx │ │ │ │ ├── filterPopUp.tsx │ │ │ │ ├── initialResultMessage.tsx │ │ │ │ ├── profileSearchTextBox.tsx │ │ │ │ ├── searchResultInitialMessage.tsx │ │ │ │ ├── searchUserWrapperPage.tsx │ │ │ │ └── userProfilesList.tsx │ │ │ ├── constants │ │ │ │ └── resources.ts │ │ │ ├── index.tsx │ │ │ ├── react-app-env.d.ts │ │ │ ├── router │ │ │ │ └── router.tsx │ │ │ └── styles │ │ │ │ ├── site.css │ │ │ │ └── userProfile.css │ │ ├── tsconfig.json │ │ └── tslint.json │ ├── Common │ │ ├── Constants.cs │ │ ├── Extensions │ │ │ └── SharePointSearchCellsResultExtension.cs │ │ ├── GraphApiHelper.cs │ │ ├── Interfaces │ │ │ ├── ICustomTokenHelper.cs │ │ │ ├── IGraphApiHelper.cs │ │ │ ├── ISharePointApiHelper.cs │ │ │ ├── ITokenHelper.cs │ │ │ └── IUserProfileActivityStorageHelper.cs │ │ ├── SharePointApiHelper.cs │ │ ├── TokenHelper.cs │ │ └── UserProfileActivityStorageHelper.cs │ ├── Controllers │ │ ├── BotController.cs │ │ ├── ResourceController.cs │ │ └── UserProfileController.cs │ ├── Dialogs │ │ ├── LogoutDialog.cs │ │ └── MainDialog.cs │ ├── Microsoft.Teams.Apps.ExpertFinder.csproj │ ├── Models │ │ ├── AdaptiveCardAction.cs │ │ ├── Configuration │ │ │ ├── AADSettings.cs │ │ │ ├── BotSettings.cs │ │ │ ├── SharePointSettings.cs │ │ │ ├── StorageSettings.cs │ │ │ └── TokenSettings.cs │ │ ├── ConversationData.cs │ │ ├── EditProfileCardAction.cs │ │ ├── SearchSubmitAction.cs │ │ ├── SharePoint │ │ │ ├── SearchPropertiesResult.cs │ │ │ ├── SearchRelevantResult.cs │ │ │ ├── SearchResponse.cs │ │ │ ├── SearchRowResult.cs │ │ │ ├── SearchTableResult.cs │ │ │ ├── UserProfileDetail.cs │ │ │ └── UserSearch.cs │ │ ├── UserData.cs │ │ ├── UserProfileActivityInfo.cs │ │ ├── UserProfileDetail.cs │ │ └── UserProfileDetailBase.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Resources │ │ ├── Strings.Designer.cs │ │ ├── Strings.ar.resx │ │ ├── Strings.de.resx │ │ ├── Strings.es.resx │ │ ├── Strings.fr.resx │ │ ├── Strings.he.resx │ │ ├── Strings.ja.resx │ │ ├── Strings.ko.resx │ │ ├── Strings.pt-BR.resx │ │ ├── Strings.resx │ │ ├── Strings.ru.resx │ │ ├── Strings.zh-CN.resx │ │ └── Strings.zh-TW.resx │ ├── Startup.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── stylecop.json │ └── wwwroot │ │ └── Artifacts │ │ ├── appLogo.png │ │ └── validationIcon.png └── Microsoft.Teams.Apps.ExpertFinder.sln ├── deploy.bot.cmd ├── deploy.cmd └── global.json /.deployment: -------------------------------------------------------------------------------- 1 | [config] 2 | command = deploy.cmd 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | 352 | # compiled source # 353 | ################### 354 | *.com 355 | *.class 356 | *.dll 357 | *.exe 358 | *.pdb 359 | *.dll.config 360 | *.cache 361 | *.suo 362 | # Include dlls if they’re in the NuGet packages directory 363 | !/packages/*/lib/*.dll 364 | !/packages/*/lib/*/*.dll 365 | # Include dlls if they're in the CommonReferences directory 366 | !*CommonReferences/*.dll 367 | #################### 368 | # VS Upgrade stuff # 369 | #################### 370 | UpgradeLog.XML 371 | _UpgradeReport_Files/ 372 | ############### 373 | # Directories # 374 | ############### 375 | bin/ 376 | obj/ 377 | TestResults/ 378 | ################### 379 | # Web publish log # 380 | ################### 381 | *.Publish.xml 382 | ############# 383 | # Resharper # 384 | ############# 385 | /_ReSharper.* 386 | *.ReSharper.* 387 | ############ 388 | # Packages # 389 | ############ 390 | # it’s better to unpack these files and commit the raw source 391 | # git has its own built in compression methods 392 | *.7z 393 | *.dmg 394 | *.gz 395 | *.iso 396 | *.jar 397 | *.rar 398 | *.tar 399 | *.zip 400 | ###################### 401 | # Logs and databases # 402 | ###################### 403 | *.log 404 | *.sqlite 405 | # OS generated files # 406 | ###################### 407 | .DS_Store? 408 | ehthumbs.db 409 | Icon? 410 | Thumbs.db 411 | [Bb]in 412 | [Oo]bj 413 | [Tt]est[Rr]esults 414 | *.suo 415 | *.user 416 | *.[Cc]ache 417 | *[Rr]esharper* 418 | packages 419 | NuGet.exe 420 | _[Ss]cripts 421 | *.exe 422 | *.dll 423 | *.nupkg 424 | *.ncrunchsolution 425 | *.dot[Cc]over 426 | 427 | /Source/.vs 428 | /Source/Microsoft.Teams.Apps.ExpertFinder/ClientApp/node_modules 429 | /Source/Microsoft.Teams.Apps.ExpertFinder/wwwroot/dist 430 | /Source/Microsoft.Teams.Apps.ExpertFinder/ClientApp/build/ 431 | /Source/Microsoft.Teams.Apps.ExpertFinder/.config 432 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /Deployment/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "baseResourceName": { 6 | "type": "string", 7 | "minLength": 1, 8 | "metadata": { 9 | "description": "The base name to use for the resources that will be provisioned." 10 | } 11 | }, 12 | "botClientId": { 13 | "type": "string", 14 | "minLength": 36, 15 | "maxLength": 36, 16 | "metadata": { 17 | "description": "The client ID of the bot Azure AD app, e.g., 123e4567-e89b-12d3-a456-426655440000." 18 | } 19 | }, 20 | "botClientSecret": { 21 | "type": "securestring", 22 | "minLength": 1, 23 | "metadata": { 24 | "description": "The client secret of the bot Azure AD app." 25 | } 26 | }, 27 | "appDisplayName": { 28 | "type": "string", 29 | "minLength": 1, 30 | "defaultValue": "Expert Finder", 31 | "metadata": { 32 | "description": "App display name." 33 | } 34 | }, 35 | "appDescription": { 36 | "type": "string", 37 | "minLength": 1, 38 | "defaultValue": "ExpertFinder bot allows users to search for experts based on some attributes.", 39 | "metadata": { 40 | "description": "App description." 41 | } 42 | }, 43 | "appIconUrl": { 44 | "type": "string", 45 | "minLength": 1, 46 | "defaultValue": "https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-expertfinder/master/Manifest/color.png", 47 | "metadata": { 48 | "description": "The link to the icon for the app. It must resolve to a PNG file." 49 | } 50 | }, 51 | "tenantId": { 52 | "type": "string", 53 | "defaultValue": "[subscription().tenantId]", 54 | "minLength": 1, 55 | "maxLength": 36, 56 | "metadata": { 57 | "description": "The ID of the tenant to which the app will be deployed." 58 | } 59 | }, 60 | "sharePointSiteUrl": { 61 | "type": "string", 62 | "minLength": 1, 63 | "metadata": { 64 | "description": "SharePoint site URL." 65 | } 66 | }, 67 | "tokenSigningKey": { 68 | "type": "string", 69 | "minLength": 13, 70 | "defaultValue": "[concat(uniqueString(newGuid()), uniqueString(newGuid()))]", 71 | "metadata": { 72 | "description": "A secret used to sign the JWT authenticating the task module." 73 | } 74 | }, 75 | "sku": { 76 | "type": "string", 77 | "allowedValues": [ 78 | "Basic", 79 | "Standard", 80 | "Premium" 81 | ], 82 | "defaultValue": "Standard", 83 | "metadata": { 84 | "description": "The pricing tier for the hosting plan." 85 | } 86 | }, 87 | "planSize": { 88 | "type": "string", 89 | "allowedValues": [ 90 | "1", 91 | "2", 92 | "3" 93 | ], 94 | "defaultValue": "1", 95 | "metadata": { 96 | "description": "The size of the hosting plan (small, medium, or large)." 97 | } 98 | }, 99 | "location": { 100 | "type": "string", 101 | "defaultValue": "[resourceGroup().location]", 102 | "metadata": { 103 | "description": "Location for all resources." 104 | } 105 | }, 106 | "gitRepoUrl": { 107 | "type": "string", 108 | "metadata": { 109 | "description": "The URL to the GitHub repository to deploy." 110 | }, 111 | "defaultValue": "https://github.com/OfficeDev/microsoft-teams-apps-expertfinder.git" 112 | }, 113 | "gitBranch": { 114 | "type": "string", 115 | "metadata": { 116 | "description": "The branch of the GitHub repository to deploy." 117 | }, 118 | "defaultValue": "master" 119 | }, 120 | 121 | "defaultCulture": { 122 | "type": "string", 123 | "allowedValues": [ 124 | "en", 125 | "ar", 126 | "de", 127 | "es", 128 | "fr", 129 | "he", 130 | "ja", 131 | "ko", 132 | "pt-BR", 133 | "ru", 134 | "zh-CN", 135 | "zh-TW" 136 | ], 137 | "defaultValue": "en", 138 | "metadata": { 139 | "description": "Default localization for app." 140 | } 141 | } 142 | }, 143 | "variables": { 144 | "uniqueString": "[uniquestring(subscription().subscriptionId, resourceGroup().id, parameters('baseResourceName'))]", 145 | "botName": "[parameters('baseResourceName')]", 146 | "botAppName": "[parameters('baseResourceName')]", 147 | "botAppDomain": "[concat(variables('botAppName'), '.azurewebsites.net')]", 148 | "botAppUrl": "[concat('https://', variables('botAppDomain'))]", 149 | "hostingPlanName": "[parameters('baseResourceName')]", 150 | "storageAccountName": "[variables('uniqueString')]", 151 | "botAppInsightsName": "[parameters('baseResourceName')]", 152 | "sharedSkus": [ 153 | "Free", 154 | "Shared" 155 | ], 156 | "isSharedPlan": "[contains(variables('sharedSkus'), parameters('sku'))]", 157 | "skuFamily": "[if(equals(parameters('sku'), 'Shared'), 'D', take(parameters('sku'), 1))]" 158 | }, 159 | "resources": [ 160 | { 161 | "apiVersion": "2018-02-01", 162 | "kind": "Storage", 163 | "location": "[parameters('location')]", 164 | "name": "[variables('storageAccountName')]", 165 | "sku": { 166 | "name": "Standard_LRS" 167 | }, 168 | "type": "Microsoft.Storage/storageAccounts" 169 | }, 170 | { 171 | "apiVersion": "2016-09-01", 172 | "location": "[parameters('location')]", 173 | "name": "[variables('hostingPlanName')]", 174 | "properties": { 175 | "name": "[variables('hostingPlanName')]", 176 | "hostingEnvironment": "", 177 | "numberOfWorkers": 1 178 | }, 179 | "sku": { 180 | "name": "[if(variables('isSharedPlan'), concat(variables('skuFamily'),'1'), concat(variables('skuFamily'),parameters('planSize')))]", 181 | "tier": "[parameters('sku')]", 182 | "size": "[concat(variables('skuFamily'), parameters('planSize'))]", 183 | "family": "[variables('skuFamily')]", 184 | "capacity": 0 185 | }, 186 | "type": "Microsoft.Web/serverfarms" 187 | }, 188 | { 189 | "apiVersion": "2016-08-01", 190 | "dependsOn": [ 191 | "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", 192 | "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", 193 | "[resourceId('Microsoft.Insights/components/', variables('botAppInsightsName'))]" 194 | ], 195 | "kind": "app", 196 | "location": "[parameters('location')]", 197 | "name": "[variables('botAppName')]", 198 | "properties": { 199 | "name": "[variables('botAppName')]", 200 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", 201 | "enabled": true, 202 | "reserved": false, 203 | "clientAffinityEnabled": true, 204 | "clientCertEnabled": false, 205 | "hostNamesDisabled": false, 206 | "containerSize": 0, 207 | "dailyMemoryTimeQuota": 0, 208 | "httpsOnly": true, 209 | "siteConfig": { 210 | "alwaysOn": true, 211 | "appSettings": [ 212 | { 213 | "name": "SITE_ROLE", 214 | "value": "bot" 215 | }, 216 | { 217 | "name": "MicrosoftAppId", 218 | "value": "[parameters('botClientId')]" 219 | }, 220 | { 221 | "name": "MicrosoftAppPassword", 222 | "value": "[parameters('botClientSecret')]" 223 | }, 224 | { 225 | "name": "OAuthConnectionName", 226 | "value": "ExpertFinderAuth" 227 | }, 228 | { 229 | "name": "StorageConnectionString", 230 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')),'2015-05-01-preview').key1)]" 231 | }, 232 | { 233 | "name": "AppBaseUri", 234 | "value": "[concat('https://', variables('botAppDomain'))]" 235 | }, 236 | { 237 | "name": "TenantId", 238 | "value": "[parameters('tenantId')]" 239 | }, 240 | { 241 | "name": "APPINSIGHTS_INSTRUMENTATIONKEY", 242 | "value": "[reference(resourceId('Microsoft.Insights/components/', variables('botAppInsightsName')), '2015-05-01').InstrumentationKey]" 243 | }, 244 | { 245 | "name": "SharePointSiteUrl", 246 | "value": "[parameters('sharePointSiteUrl')]" 247 | }, 248 | { 249 | "name": "TokenSigningKey", 250 | "value": "[parameters('tokenSigningKey')]" 251 | }, 252 | { 253 | "name": "WEBSITE_NODE_DEFAULT_VERSION", 254 | "value": "10.15.2" 255 | }, 256 | { 257 | "name": "i18n:DefaultCulture", 258 | "value": "[parameters('defaultCulture')]" 259 | }, 260 | { 261 | "name": "i18n:SupportedCultures", 262 | "value": "en,ar,de,es,fr,he,ja,ko,pt-BR,ru,zh-CN,zh-TW" 263 | } 264 | ] 265 | } 266 | }, 267 | "resources": [ 268 | { 269 | "apiVersion": "2016-08-01", 270 | "name": "web", 271 | "type": "sourcecontrols", 272 | "condition": "[not(empty(parameters('gitRepoUrl')))]", 273 | "dependsOn": [ 274 | "[resourceId('Microsoft.Web/sites', variables('botAppName'))]" 275 | ], 276 | "properties": { 277 | "RepoUrl": "[parameters('gitRepoUrl')]", 278 | "branch": "[parameters('gitBranch')]", 279 | "IsManualIntegration": true 280 | } 281 | } 282 | ], 283 | "type": "Microsoft.Web/sites" 284 | }, 285 | { 286 | "apiVersion": "2015-05-01", 287 | "name": "[variables('botAppInsightsName')]", 288 | "type": "Microsoft.Insights/components", 289 | "location": "[parameters('location')]", 290 | "tags": { 291 | "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', variables('botAppName'))]": "Resource" 292 | }, 293 | "properties": { 294 | "Application_Type": "web", 295 | "Request_Source": "rest" 296 | } 297 | }, 298 | { 299 | "apiVersion": "2018-07-12", 300 | "kind": "sdk", 301 | "location": "global", 302 | "name": "[variables('botName')]", 303 | "dependsOn": [ 304 | "[resourceId('Microsoft.Web/sites', variables('botAppName'))]" 305 | ], 306 | "properties": { 307 | "displayName": "[parameters('appDisplayName')]", 308 | "description": "[parameters('appDescription')]", 309 | "iconUrl": "[parameters('appIconUrl')]", 310 | "msaAppId": "[parameters('botClientId')]", 311 | "endpoint": "[concat(variables('botAppUrl'), '/api/messages')]", 312 | "developerAppInsightKey": "[reference(resourceId('Microsoft.Insights/components', variables('botAppInsightsName')), '2015-05-01').InstrumentationKey]" 313 | }, 314 | "resources": [ 315 | { 316 | "name": "[concat(variables('botName'), '/MsTeamsChannel')]", 317 | "type": "Microsoft.BotService/botServices/channels", 318 | "apiVersion": "2018-07-12", 319 | "location": "global", 320 | "sku": { 321 | "name": "F0" 322 | }, 323 | "properties": { 324 | "channelName": "MsTeamsChannel", 325 | "location": "global", 326 | "properties": { 327 | "isEnabled": true 328 | } 329 | }, 330 | "dependsOn": [ 331 | "[concat('Microsoft.BotService/botServices/', variables('botName'))]" 332 | ] 333 | } 334 | ], 335 | "sku": { 336 | "name": "F0" 337 | }, 338 | "type": "Microsoft.BotService/botServices" 339 | } 340 | ], 341 | "outputs": { 342 | "botId": { 343 | "type": "string", 344 | "value": "[parameters('botClientId')]" 345 | }, 346 | "appDomain": { 347 | "type": "string", 348 | "value": "[variables('botAppDomain')]" 349 | } 350 | } 351 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /Manifest/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", 3 | "name.short": "الباحث عن الخبير", 4 | "name.full": "الباحث عن الخبير", 5 | "description.short": "يسمح 'الباحث عن الخبير' للمستخدمين بالبحث عن الخبراء استنادا إلى بعض السمات", 6 | "description.full": "يسمح 'الباحث عن الخبير' للمستخدمين بالبحث عن الخبراء في مؤسسة استنادا إلى مهاراتهم أو اهتماماتهم أو المدارس التي تم حضورها. بالإضافة إلى ذلك، يوفر للمستخدمين أيضًا إمكانية تحديث معلومات ملفات التعريف الخاصة بهم وتحديثها.", 7 | "bots[0].commandLists[0].commands[0].title": "ملف التعريف الخاص بي", 8 | "bots[0].commandLists[0].commands[0].description": "ملف التعريف الخاص بي", 9 | "bots[0].commandLists[0].commands[1].title": "بحث", 10 | "bots[0].commandLists[0].commands[1].description": "البحث عن الأفراد", 11 | "bots[0].commandLists[0].commands[2].title": "تسجيل الخروج", 12 | "bots[0].commandLists[0].commands[2].description": "تسجيل الخروج من Expert Finder", 13 | "composeExtensions[0].commands[0].title": "المهارات", 14 | "composeExtensions[0].commands[0].description": "البحث عن خبراء على أساس المهارات", 15 | "composeExtensions[0].commands[0].parameters[0].title": "المهارات", 16 | "composeExtensions[0].commands[0].parameters[0].description": "البحث عن خبراء على أساس المهارات", 17 | "composeExtensions[0].commands[1].title": "الاهتمامات", 18 | "composeExtensions[0].commands[1].description": "البحث عن خبراء على أساس الاهتمام", 19 | "composeExtensions[0].commands[1].parameters[0].title": "الاهتمامات", 20 | "composeExtensions[0].commands[1].parameters[0].description": "البحث عن خبراء على أساس الاهتمام", 21 | "composeExtensions[0].commands[2].title": "المؤسسات التعليمية", 22 | "composeExtensions[0].commands[2].description": "البحث عن خبراء على أساس المدارس", 23 | "composeExtensions[0].commands[2].parameters[0].title": "المؤسسات التعليمية", 24 | "composeExtensions[0].commands[2].parameters[0].description": "البحث عن خبراء على أساس المدارس" 25 | } -------------------------------------------------------------------------------- /Manifest/color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-expertfinder/d75c1fc8022c31c696bf7dac069b21d58382709f/Manifest/color.png -------------------------------------------------------------------------------- /Manifest/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", 3 | "name.short": "Expert Finder", 4 | "name.full": "Expert Finder", 5 | "description.short": "Mit Expert Finder können Benutzer nach Experten basierend auf Attributen suchen", 6 | "description.full": "Mithilfe von Expert Finder können Benutzer in einer Organisation basierend auf Qualifikationen, Interessen oder besuchten Bildungseinrichtungen nach Experten suchen. Darüber hinaus bietet Expert Finder Benutzern auch die Möglichkeit, Ihre Profilinformationen zu aktualisieren und auf dem neuesten Stand zu halten.", 7 | "bots[0].commandLists[0].commands[0].title": "Mein Profil", 8 | "bots[0].commandLists[0].commands[0].description": "Mein Profil", 9 | "bots[0].commandLists[0].commands[1].title": "Suchen", 10 | "bots[0].commandLists[0].commands[1].description": "Personen suchen", 11 | "bots[0].commandLists[0].commands[2].title": "Abmelden", 12 | "bots[0].commandLists[0].commands[2].description": "Von Expert Finder abmelden", 13 | "composeExtensions[0].commands[0].title": "Qualifikationen", 14 | "composeExtensions[0].commands[0].description": "Experten basierend auf Fähigkeiten suchen", 15 | "composeExtensions[0].commands[0].parameters[0].title": "Qualifikationen", 16 | "composeExtensions[0].commands[0].parameters[0].description": "Experten basierend auf Fähigkeiten suchen", 17 | "composeExtensions[0].commands[1].title": "Interessen", 18 | "composeExtensions[0].commands[1].description": "Experten basierend auf Interessen suchen", 19 | "composeExtensions[0].commands[1].parameters[0].title": "Interessen", 20 | "composeExtensions[0].commands[1].parameters[0].description": "Experten basierend auf Interessen suchen", 21 | "composeExtensions[0].commands[2].title": "Bildungseinrichtungen", 22 | "composeExtensions[0].commands[2].description": "Experten basierend auf Bildungseinrichtungen suchen", 23 | "composeExtensions[0].commands[2].parameters[0].title": "Bildungseinrichtungen", 24 | "composeExtensions[0].commands[2].parameters[0].description": "Experten basierend auf Bildungseinrichtungen suchen" 25 | } -------------------------------------------------------------------------------- /Manifest/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", 3 | "name.short": "Expert Finder", 4 | "name.full": "Expert Finder", 5 | "description.short": "Expert Finder lets users search for experts based on some attributes", 6 | "description.full": "Expert Finder lets users search for experts in an organization based on their skills, interests or schools attended. In addition, it also provides users the ability to update their profile information and keep it up to date.", 7 | "bots[0].commandLists[0].commands[0].title": "My profile", 8 | "bots[0].commandLists[0].commands[0].description": "My profile", 9 | "bots[0].commandLists[0].commands[1].title": "Search", 10 | "bots[0].commandLists[0].commands[1].description": "Search individuals", 11 | "bots[0].commandLists[0].commands[2].title": "Logout", 12 | "bots[0].commandLists[0].commands[2].description": "Sign out of Expert Finder", 13 | "composeExtensions[0].commands[0].title": "Skills", 14 | "composeExtensions[0].commands[0].description": "Search experts on basis of skills", 15 | "composeExtensions[0].commands[0].parameters[0].title": "Skills", 16 | "composeExtensions[0].commands[0].parameters[0].description": "Search experts on basis of skills", 17 | "composeExtensions[0].commands[1].title": "Interests", 18 | "composeExtensions[0].commands[1].description": "Search experts on basis of interest", 19 | "composeExtensions[0].commands[1].parameters[0].title": "Interests", 20 | "composeExtensions[0].commands[1].parameters[0].description": "Search experts on basis of interest", 21 | "composeExtensions[0].commands[2].title": "Schools", 22 | "composeExtensions[0].commands[2].description": "Search experts on basis of schools", 23 | "composeExtensions[0].commands[2].parameters[0].title": "Schools", 24 | "composeExtensions[0].commands[2].parameters[0].description": "Search experts on basis of schools" 25 | } -------------------------------------------------------------------------------- /Manifest/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", 3 | "name.short": "Buscador de expertos", 4 | "name.full": "Buscador de expertos", 5 | "description.short": "El buscador de expertos permite buscar expertos según ciertos atributos", 6 | "description.full": "Expert Finder permite a los usuarios buscar expertos en una organización en función de sus aptitudes, intereses o escuelas atendidas. Además, también proporciona a los usuarios la capacidad de actualizar su información de perfil y mantenerla actualizada.", 7 | "bots[0].commandLists[0].commands[0].title": "Mi perfil", 8 | "bots[0].commandLists[0].commands[0].description": "Mi perfil", 9 | "bots[0].commandLists[0].commands[1].title": "Buscar", 10 | "bots[0].commandLists[0].commands[1].description": "Buscar usuarios particulares", 11 | "bots[0].commandLists[0].commands[2].title": "Cerrar sesión", 12 | "bots[0].commandLists[0].commands[2].description": "Cerrar la sesión de Expert Finder", 13 | "composeExtensions[0].commands[0].title": "Aptitudes", 14 | "composeExtensions[0].commands[0].description": "Buscar expertos según sus aptitudes", 15 | "composeExtensions[0].commands[0].parameters[0].title": "Aptitudes", 16 | "composeExtensions[0].commands[0].parameters[0].description": "Buscar expertos según sus aptitudes", 17 | "composeExtensions[0].commands[1].title": "Intereses", 18 | "composeExtensions[0].commands[1].description": "Buscar expertos según sus intereses", 19 | "composeExtensions[0].commands[1].parameters[0].title": "Intereses", 20 | "composeExtensions[0].commands[1].parameters[0].description": "Buscar expertos según sus intereses", 21 | "composeExtensions[0].commands[2].title": "Escuelas", 22 | "composeExtensions[0].commands[2].description": "Buscar expertos según las escuelas", 23 | "composeExtensions[0].commands[2].parameters[0].title": "Escuelas", 24 | "composeExtensions[0].commands[2].parameters[0].description": "Buscar expertos según las escuelas" 25 | } -------------------------------------------------------------------------------- /Manifest/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", 3 | "name.short": "Recherche d’experts", 4 | "name.full": "Recherche d’experts", 5 | "description.short": "Recherchez des experts en fonction des attributs avec le localisateur d’experts.", 6 | "description.full": "L’expert permet aux utilisateurs de rechercher des experts au sein d’une organisation en fonction de leurs compétences, centres d’intérêt ou écoles qui ont participé. En outre, il permet également aux utilisateurs de mettre à jour leurs informations de profil et de les tenir à jour.", 7 | "bots[0].commandLists[0].commands[0].title": "Mon profil", 8 | "bots[0].commandLists[0].commands[0].description": "Mon profil", 9 | "bots[0].commandLists[0].commands[1].title": "Rechercher", 10 | "bots[0].commandLists[0].commands[1].description": "Rechercher des particuliers", 11 | "bots[0].commandLists[0].commands[2].title": "Déconnexion", 12 | "bots[0].commandLists[0].commands[2].description": "Se déconnecter de la recherche d’experts", 13 | "composeExtensions[0].commands[0].title": "Compétences", 14 | "composeExtensions[0].commands[0].description": "Rechercher des experts sur la base des compétences", 15 | "composeExtensions[0].commands[0].parameters[0].title": "Compétences", 16 | "composeExtensions[0].commands[0].parameters[0].description": "Rechercher des experts sur la base des compétences", 17 | "composeExtensions[0].commands[1].title": "Centres d'intérêts", 18 | "composeExtensions[0].commands[1].description": "Rechercher des experts sur la base des intérêts", 19 | "composeExtensions[0].commands[1].parameters[0].title": "Centres d'intérêts", 20 | "composeExtensions[0].commands[1].parameters[0].description": "Rechercher des experts sur la base des intérêts", 21 | "composeExtensions[0].commands[2].title": "Établissements scolaires", 22 | "composeExtensions[0].commands[2].description": "Rechercher des experts sur la base des écoles", 23 | "composeExtensions[0].commands[2].parameters[0].title": "Établissements scolaires", 24 | "composeExtensions[0].commands[2].parameters[0].description": "Rechercher des experts sur la base des écoles" 25 | } -------------------------------------------------------------------------------- /Manifest/he.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", 3 | "name.short": "מאתר מומחה", 4 | "name.full": "מאתר מומחה", 5 | "description.short": "'מאתר מומחה' מאפשר למשתמשים לחפש אחר מומחים בהתבסס על תכונות מסוימות", 6 | "description.full": "'מאתר מומחה' מאפשר למשתמשים לחפש אחר מומחים בארגון על בסיס הכישורים שלהם, תחומי העניין או בתי הספר בהם למדו. בנוסף, הוא מספק למשתמשים גם את היכולת לעדכן את מידע הפרופיל שלהם ולהשאיר אותו עדכני.", 7 | "bots[0].commandLists[0].commands[0].title": "הפרופיל שלי", 8 | "bots[0].commandLists[0].commands[0].description": "הפרופיל שלי", 9 | "bots[0].commandLists[0].commands[1].title": "חפש", 10 | "bots[0].commandLists[0].commands[1].description": "חפש אנשים", 11 | "bots[0].commandLists[0].commands[2].title": "צא", 12 | "bots[0].commandLists[0].commands[2].description": "צא מ'מאתר מומחה'", 13 | "composeExtensions[0].commands[0].title": "כישורים", 14 | "composeExtensions[0].commands[0].description": "חפש מומחים על בסיס כישורים", 15 | "composeExtensions[0].commands[0].parameters[0].title": "כישורים", 16 | "composeExtensions[0].commands[0].parameters[0].description": "חפש מומחים על בסיס כישורים", 17 | "composeExtensions[0].commands[1].title": "תחומי עניין", 18 | "composeExtensions[0].commands[1].description": "חפש מומחים על בסיס תחום עניין", 19 | "composeExtensions[0].commands[1].parameters[0].title": "תחומי עניין", 20 | "composeExtensions[0].commands[1].parameters[0].description": "חפש מומחים על בסיס תחום עניין", 21 | "composeExtensions[0].commands[2].title": "בתי ספר", 22 | "composeExtensions[0].commands[2].description": "חפש מומחים על בסיס בתי ספר", 23 | "composeExtensions[0].commands[2].parameters[0].title": "בתי ספר", 24 | "composeExtensions[0].commands[2].parameters[0].description": "חפש מומחים על בסיס בתי ספר" 25 | } -------------------------------------------------------------------------------- /Manifest/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", 3 | "name.short": "エキスパートの検索", 4 | "name.full": "エキスパートの検索", 5 | "description.short": "エキスパートの検索では、ユーザーがいくつかの属性に基づいてエキスパートを検索できます", 6 | "description.full": "エキスパートの検索では、スキル、興味の対象、または通っていた学校に基づいてユーザーが組織内のエキスパートを検索できます。また、ユーザーはプロフィール情報を更新して、最新の状態にしておくことができます。", 7 | "bots[0].commandLists[0].commands[0].title": "マイ プロフィール", 8 | "bots[0].commandLists[0].commands[0].description": "マイ プロフィール", 9 | "bots[0].commandLists[0].commands[1].title": "検索", 10 | "bots[0].commandLists[0].commands[1].description": "個人を検索", 11 | "bots[0].commandLists[0].commands[2].title": "ログアウト", 12 | "bots[0].commandLists[0].commands[2].description": "エキスパートの検索からサインアウト", 13 | "composeExtensions[0].commands[0].title": "スキル", 14 | "composeExtensions[0].commands[0].description": "スキルに基づいてエキスパートを検索", 15 | "composeExtensions[0].commands[0].parameters[0].title": "スキル", 16 | "composeExtensions[0].commands[0].parameters[0].description": "スキルに基づいてエキスパートを検索", 17 | "composeExtensions[0].commands[1].title": "興味の対象", 18 | "composeExtensions[0].commands[1].description": "興味の対象に基づいてエキスパートを検索", 19 | "composeExtensions[0].commands[1].parameters[0].title": "興味の対象", 20 | "composeExtensions[0].commands[1].parameters[0].description": "興味の対象に基づいてエキスパートを検索", 21 | "composeExtensions[0].commands[2].title": "学校", 22 | "composeExtensions[0].commands[2].description": "学校に基づいてエキスパートを検索", 23 | "composeExtensions[0].commands[2].parameters[0].title": "学校", 24 | "composeExtensions[0].commands[2].parameters[0].description": "学校に基づいてエキスパートを検索" 25 | } -------------------------------------------------------------------------------- /Manifest/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", 3 | "name.short": "전문가 파인더", 4 | "name.full": "전문가 파인더", 5 | "description.short": "전문가 파인더를 사용하면 사용자가 몇 가지 특성을 기반으로 전문가를 검색할 수 있습니다.", 6 | "description.full": "Expert Finder를 사용하면 사용자는 스킬, 흥미, 교육 기술 책임을 기준으로 조직 내 전문가를 검색할 수 있습니다. 또한 사용자의 프로필 정보를 업데이트하고 최신 상태로 유지할 수 있는 기능을 제공합니다.", 7 | "bots[0].commandLists[0].commands[0].title": "내 프로필", 8 | "bots[0].commandLists[0].commands[0].description": "내 프로필", 9 | "bots[0].commandLists[0].commands[1].title": "검색", 10 | "bots[0].commandLists[0].commands[1].description": "개인 검색", 11 | "bots[0].commandLists[0].commands[2].title": "로그아웃", 12 | "bots[0].commandLists[0].commands[2].description": "Expert Finder 로그아웃", 13 | "composeExtensions[0].commands[0].title": "기술", 14 | "composeExtensions[0].commands[0].description": "기술을 기반으로 전문가 검색", 15 | "composeExtensions[0].commands[0].parameters[0].title": "기술", 16 | "composeExtensions[0].commands[0].parameters[0].description": "기술을 기반으로 전문가 검색", 17 | "composeExtensions[0].commands[1].title": "관심사", 18 | "composeExtensions[0].commands[1].description": "관심사를 기반으로 전문가 검색", 19 | "composeExtensions[0].commands[1].parameters[0].title": "관심사", 20 | "composeExtensions[0].commands[1].parameters[0].description": "관심사를 기반으로 전문가 검색", 21 | "composeExtensions[0].commands[2].title": "학교", 22 | "composeExtensions[0].commands[2].description": "학교를 기반으로 전문가 검색", 23 | "composeExtensions[0].commands[2].parameters[0].title": "학교", 24 | "composeExtensions[0].commands[2].parameters[0].description": "학교를 기반으로 전문가 검색" 25 | } -------------------------------------------------------------------------------- /Manifest/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", 3 | "manifestVersion": "1.5", 4 | "version": "1.1.0", 5 | "id": "38e34b2e-5fcf-40e0-8e8c-90163aef3061", 6 | "packageName": "com.microsoft.teams.expertfinder", 7 | "developer": { 8 | "name": "<>", 9 | "websiteUrl": "<>", 10 | "privacyUrl": "<>", 11 | "termsOfUseUrl": "<>" 12 | }, 13 | "localizationInfo": { 14 | "defaultLanguageTag": "en", 15 | "additionalLanguages": [ 16 | { 17 | "languageTag": "de", 18 | "file": "de.json" 19 | }, 20 | { 21 | "languageTag": "en", 22 | "file": "en.json" 23 | }, 24 | { 25 | "languageTag": "fr", 26 | "file": "fr.json" 27 | }, 28 | { 29 | "languageTag": "ar", 30 | "file": "ar.json" 31 | }, 32 | { 33 | "languageTag": "ja", 34 | "file": "ja.json" 35 | }, 36 | { 37 | "languageTag": "es", 38 | "file": "es.json" 39 | }, 40 | { 41 | "languageTag": "he", 42 | "file": "he.json" 43 | }, 44 | { 45 | "languageTag": "ko", 46 | "file": "ko.json" 47 | }, 48 | { 49 | "languageTag": "pt-BR", 50 | "file": "pt-BR.json" 51 | }, 52 | { 53 | "languageTag": "ru", 54 | "file": "ru.json" 55 | }, 56 | { 57 | "languageTag": "zh-CN", 58 | "file": "zh-CN.json" 59 | }, 60 | { 61 | "languageTag": "zh-TW", 62 | "file": "zh-TW.json" 63 | } 64 | ] 65 | }, 66 | "icons": { 67 | "color": "color.png", 68 | "outline": "outline.png" 69 | }, 70 | "name": { 71 | "short": "Expert Finder", 72 | "full": "Expert Finder" 73 | }, 74 | "description": { 75 | "short": "Expert Finder lets users search for experts based on some attributes", 76 | "full": "Expert Finder lets users search for experts in an organization based on their skills, interests or schools attended. In addition, it also provides users the ability to update their profile information and keep it up to date. " 77 | }, 78 | "accentColor": "#FEAE25", 79 | "bots": [ 80 | { 81 | "botId": "<>", 82 | "scopes": [ 83 | "personal" 84 | ], 85 | "commandLists": [ 86 | { 87 | "scopes": [ 88 | "personal" 89 | ], 90 | "commands": [ 91 | { 92 | "title": "My profile", 93 | "description": "My profile" 94 | }, 95 | { 96 | "title": "Search", 97 | "description": "Search individuals" 98 | }, 99 | { 100 | "title": "Logout", 101 | "description": "Sign out of Expert Finder" 102 | } 103 | ] 104 | } 105 | ], 106 | "supportsFiles": false, 107 | "isNotificationOnly": false 108 | } 109 | ], 110 | "composeExtensions": [ 111 | { 112 | "botId": "<>", 113 | "canUpdateConfiguration": true, 114 | "commands": [ 115 | { 116 | "id": "skills", 117 | "type": "query", 118 | "title": "Skills", 119 | "description": "Search experts on basis of skills", 120 | "initialRun": true, 121 | "fetchTask": false, 122 | "context": [ 123 | "commandBox", 124 | "compose" 125 | ], 126 | "parameters": [ 127 | { 128 | "name": "skills", 129 | "title": "Skills", 130 | "description": "Search experts on basis of skills", 131 | "inputType": "text" 132 | } 133 | ] 134 | }, 135 | { 136 | "id": "interests", 137 | "type": "query", 138 | "title": "Interests", 139 | "description": "Search experts on basis of interest", 140 | "initialRun": true, 141 | "fetchTask": false, 142 | "context": [ 143 | "commandBox", 144 | "compose" 145 | ], 146 | "parameters": [ 147 | { 148 | "name": "interests", 149 | "title": "Interests", 150 | "description": "Search experts on basis of interest", 151 | "inputType": "text" 152 | } 153 | ] 154 | }, 155 | { 156 | "id": "schools", 157 | "type": "query", 158 | "title": "Schools", 159 | "description": "Search experts on basis of schools", 160 | "initialRun": true, 161 | "fetchTask": false, 162 | "context": [ 163 | "commandBox", 164 | "compose" 165 | ], 166 | "parameters": [ 167 | { 168 | "name": "schools", 169 | "title": "Schools", 170 | "description": "Search experts on basis of schools", 171 | "inputType": "text" 172 | } 173 | ] 174 | } 175 | ] 176 | } 177 | ], 178 | "permissions": [ 179 | "identity", 180 | "messageTeamMembers" 181 | ], 182 | "validDomains": [ 183 | "token.botframework.com", 184 | "<>" 185 | ] 186 | } -------------------------------------------------------------------------------- /Manifest/outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-expertfinder/d75c1fc8022c31c696bf7dac069b21d58382709f/Manifest/outline.png -------------------------------------------------------------------------------- /Manifest/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", 3 | "name.short": "Localizador de especialistas", 4 | "name.full": "Localizador de especialistas", 5 | "description.short": "O Expert Finder deixa os usuários pesquisarem especialistas baseado em atributos", 6 | "description.full": "O expert Finder permite que os usuários pesquisem especialistas em uma organização com base em suas habilidades, interesses ou escolas freqüentados. Além disso, ela também permite que os usuários atualizem suas informações de perfil e as mantenham atualizadas.", 7 | "bots[0].commandLists[0].commands[0].title": "Meu perfil", 8 | "bots[0].commandLists[0].commands[0].description": "Meu perfil", 9 | "bots[0].commandLists[0].commands[1].title": "Pesquisar", 10 | "bots[0].commandLists[0].commands[1].description": "Pesquisar indivíduos", 11 | "bots[0].commandLists[0].commands[2].title": "Sair", 12 | "bots[0].commandLists[0].commands[2].description": "Saia do Expert Finder", 13 | "composeExtensions[0].commands[0].title": "Habilidades", 14 | "composeExtensions[0].commands[0].description": "Pesquisar especialistas com base em habilidades", 15 | "composeExtensions[0].commands[0].parameters[0].title": "Habilidades", 16 | "composeExtensions[0].commands[0].parameters[0].description": "Pesquisar especialistas com base em habilidades", 17 | "composeExtensions[0].commands[1].title": "Interesses", 18 | "composeExtensions[0].commands[1].description": "Pesquisar especialistas com base em interesse", 19 | "composeExtensions[0].commands[1].parameters[0].title": "Interesses", 20 | "composeExtensions[0].commands[1].parameters[0].description": "Pesquisar especialistas com base em interesse", 21 | "composeExtensions[0].commands[2].title": "Escolas", 22 | "composeExtensions[0].commands[2].description": "Pesquisar especialistas com base em escolas", 23 | "composeExtensions[0].commands[2].parameters[0].title": "Escolas", 24 | "composeExtensions[0].commands[2].parameters[0].description": "Pesquisar especialistas com base em escolas" 25 | } -------------------------------------------------------------------------------- /Manifest/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", 3 | "name.short": "Expert Finder", 4 | "name.full": "Expert Finder", 5 | "description.short": "Expert Finder позволяет искать экспертов на основе некоторых атрибутов", 6 | "description.full": "Expert Finder позволяет пользователям искать экспертов в организации на основе их навыков, интересов или учебных заведений, которые они посещали. Кроме того, эта функция предоставляет пользователям возможность обновить свои данные профиля и поддерживать их в актуальном состоянии.", 7 | "bots[0].commandLists[0].commands[0].title": "Мой профиль", 8 | "bots[0].commandLists[0].commands[0].description": "Мой профиль", 9 | "bots[0].commandLists[0].commands[1].title": "Поиск", 10 | "bots[0].commandLists[0].commands[1].description": "Поиск отдельных пользователей", 11 | "bots[0].commandLists[0].commands[2].title": "Выйти", 12 | "bots[0].commandLists[0].commands[2].description": "Выйти из Expert Finder", 13 | "composeExtensions[0].commands[0].title": "Навыки", 14 | "composeExtensions[0].commands[0].description": "Поиск экспертов на основе навыков", 15 | "composeExtensions[0].commands[0].parameters[0].title": "Навыки", 16 | "composeExtensions[0].commands[0].parameters[0].description": "Поиск экспертов на основе навыков", 17 | "composeExtensions[0].commands[1].title": "Увлечения", 18 | "composeExtensions[0].commands[1].description": "Поиск экспертов на основе интересов", 19 | "composeExtensions[0].commands[1].parameters[0].title": "Увлечения", 20 | "composeExtensions[0].commands[1].parameters[0].description": "Поиск экспертов на основе интересов", 21 | "composeExtensions[0].commands[2].title": "Учебные заведения", 22 | "composeExtensions[0].commands[2].description": "Поиск экспертов на основе учебных заведений", 23 | "composeExtensions[0].commands[2].parameters[0].title": "Учебные заведения", 24 | "composeExtensions[0].commands[2].parameters[0].description": "Поиск экспертов на основе учебных заведений" 25 | } -------------------------------------------------------------------------------- /Manifest/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", 3 | "name.short": "专家查找工具", 4 | "name.full": "专家查找工具", 5 | "description.short": "'专家查找工具'允许用户根据一些属性搜索专家", 6 | "description.full": "“专业查找工具”允许用户根据技能、兴趣或就读学校搜索组织中的专家。此外,它还为用户提供了更新其个人资料信息并保持其更新的能力。", 7 | "bots[0].commandLists[0].commands[0].title": "我的个人资料", 8 | "bots[0].commandLists[0].commands[0].description": "我的个人资料", 9 | "bots[0].commandLists[0].commands[1].title": "搜索", 10 | "bots[0].commandLists[0].commands[1].description": "搜索个人", 11 | "bots[0].commandLists[0].commands[2].title": "注销", 12 | "bots[0].commandLists[0].commands[2].description": "注销专家查找程序", 13 | "composeExtensions[0].commands[0].title": "技能", 14 | "composeExtensions[0].commands[0].description": "基于技能搜索专家", 15 | "composeExtensions[0].commands[0].parameters[0].title": "技能", 16 | "composeExtensions[0].commands[0].parameters[0].description": "基于技能搜索专家", 17 | "composeExtensions[0].commands[1].title": "兴趣", 18 | "composeExtensions[0].commands[1].description": "基于兴趣搜索专家", 19 | "composeExtensions[0].commands[1].parameters[0].title": "兴趣", 20 | "composeExtensions[0].commands[1].parameters[0].description": "基于兴趣搜索专家", 21 | "composeExtensions[0].commands[2].title": "学校", 22 | "composeExtensions[0].commands[2].description": "基于学校搜索专家", 23 | "composeExtensions[0].commands[2].parameters[0].title": "学校", 24 | "composeExtensions[0].commands[2].parameters[0].description": "基于学校搜索专家" 25 | } -------------------------------------------------------------------------------- /Manifest/zn-TW.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", 3 | "name.short": "專家尋找工具", 4 | "name.full": "專家尋找工具", 5 | "description.short": "專家尋找工具可讓使用者根據某些屬性搜尋專家", 6 | "description.full": "專家尋找工具可讓使用者根據技能、興趣或就讀的學校,搜尋組織中的專家。此外,它也能讓使用者更新其設定檔資訊,並讓它保持在最新狀態。", 7 | "bots[0].commandLists[0].commands[0].title": "我的設定檔", 8 | "bots[0].commandLists[0].commands[0].description": "我的設定檔", 9 | "bots[0].commandLists[0].commands[1].title": "搜尋", 10 | "bots[0].commandLists[0].commands[1].description": "搜尋個人", 11 | "bots[0].commandLists[0].commands[2].title": "登出", 12 | "bots[0].commandLists[0].commands[2].description": "登出專家尋找工具", 13 | "composeExtensions[0].commands[0].title": "技能", 14 | "composeExtensions[0].commands[0].description": "根據技能搜尋專家", 15 | "composeExtensions[0].commands[0].parameters[0].title": "技能", 16 | "composeExtensions[0].commands[0].parameters[0].description": "根據技能搜尋專家", 17 | "composeExtensions[0].commands[1].title": "興趣", 18 | "composeExtensions[0].commands[1].description": "根據興趣搜尋專家", 19 | "composeExtensions[0].commands[1].parameters[0].title": "興趣", 20 | "composeExtensions[0].commands[1].parameters[0].description": "根據興趣搜尋專家", 21 | "composeExtensions[0].commands[2].title": "學校", 22 | "composeExtensions[0].commands[2].description": "根據學校搜尋專家", 23 | "composeExtensions[0].commands[2].parameters[0].title": "學校", 24 | "composeExtensions[0].commands[2].parameters[0].description": "根據學校搜尋專家" 25 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - csharp 5 | products: 6 | - office-teams 7 | description: Expert Finder bot allows users to search for experts based on certain attributes 8 | urlFragment: microsoft-teams-apps-expertfinder 9 | --- 10 | 11 | # Expert Finder Bot App Template 12 | | [Documentation](https://github.com/OfficeDev/microsoft-teams-apps-expertfinder/wiki) | [Deployment guide](https://github.com/OfficeDev/microsoft-teams-apps-expertfinder/wiki/Deployment-Guide)| [Architecture](https://github.com/OfficeDev/microsoft-teams-apps-expertfinder/wiki/Solution-Overview) 13 | |--|--|--| 14 | 15 | Expert Finder bot allows employees to search for other individuals in an organization based on their skills, interests and schools. In addition, it also provides users the ability to update their profile information and keep it up to date. 16 | 17 | **Expert Finder bot** 18 | - **My Profile**: Using this command users will be able to view their Azure Active Directory profile information in a card. The card will have call to action buttons that will let them add or modify information in their Azure Active Directory profile or view details about other attributes. 19 | 20 | ![MyProfileCard](https://github.com/OfficeDev/microsoft-teams-apps-expertfinder/wiki/Images/MyProfileCard.png) 21 | 22 | ![EditProfile](https://github.com/OfficeDev/microsoft-teams-apps-expertfinder/wiki/Images/EditProfile.png) 23 | 24 | - **Search**: This command allows the users to search for experts within the organization whose attributes match with the search keyword. They will be able to select a max of 5 user profiles and view details pertaining to them. 25 | 26 | ![SearchCard](https://github.com/OfficeDev/microsoft-teams-apps-expertfinder/wiki/Images/SearchFeature.png) 27 | 28 | ![SearchTaskModule](https://github.com/OfficeDev/microsoft-teams-apps-expertfinder/wiki/Images/SearchTaskModule.PNG) 29 | 30 | **Expert Finder messaging extension** 31 | Users can search for individuals within the organization whose attributes match the user search keyword using the messaging extension. 32 | 33 | ![MessagingExtension](https://github.com/OfficeDev/microsoft-teams-apps-expertfinder/wiki/Images/MessagingExtension.PNG) 34 | 35 | ## Legal Notices 36 | 37 | This app template is provided under the [MIT License](https://github.com/OfficeDev/microsoft-teams-apps-expertfinder/blob/master/LICENSE) terms. In addition to these terms, by using this app template you agree to the following: 38 | 39 | - You, not Microsoft, will license the use of your app to users or organization. 40 | 41 | - This app template is not intended to substitute your own regulatory due diligence or make you or your app compliant with respect to any applicable regulations, including but not limited to privacy, healthcare, employment, or financial regulations. 42 | 43 | - You are responsible for complying with all applicable privacy and security regulations including those related to use, collection and handling of any personal data by your app. This includes complying with all internal privacy and security policies of your organization if your app is developed to be sideloaded internally within your organization. Where applicable, you may be responsible for data related incidents or data subject requests for data collected through your app. 44 | 45 | - Any trademarks or registered trademarks of Microsoft in the United States and/or other countries and logos included in this repository are the property of Microsoft, and the license for this project does not grant you rights to use any Microsoft names, logos or trademarks outside of this repository. Microsoft’s general trademark guidelines can be found [here](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general.aspx). 46 | 47 | - If the app template enables access to any Microsoft Internet-based services (e.g., Office365), use of those services will be subject to the separately-provided terms of use. In such cases, Microsoft may collect telemetry data related to app template usage and operation. Use and handling of telemetry data will be performed in accordance with such terms of use. 48 | 49 | - Use of this template does not guarantee acceptance of your app to the Teams app store. To make this app available in the Teams app store, you will have to comply with the [submission and validation process](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/appsource/publish), and all associated requirements such as including your own privacy statement and terms of use for your app. 50 | 51 | ## Getting Started 52 | Begin with the [Solution overview](https://github.com/OfficeDev/microsoft-teams-apps-expertfinder/wiki/Solution-Overview) to read about what the app does and how it works. 53 | 54 | When you're ready to try out Expert Finder bot, or to use it in your own organization, follow the steps in the [Deployment guide](https://github.com/OfficeDev/microsoft-teams-apps-expertfinder/wiki/Deployment-Guide). 55 | 56 | ## Feedback 57 | Thoughts? Questions? Ideas? Share them with us on [Teams UserVoice](https://microsoftteams.uservoice.com/forums/555103-public)! 58 | 59 | Please report bugs and other code issues [here](https://github.com/OfficeDev/microsoft-teams-apps-expertfinder/issues/new). 60 | 61 | ## Contributing 62 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 63 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 64 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 65 | 66 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 67 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 68 | provided by the bot. You will only need to do this once across all repos using our CLA. 69 | 70 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 71 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 72 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 73 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /Source/ExpertFinder/AdapterWithErrorHandler.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder 6 | { 7 | using System; 8 | using Microsoft.Bot.Builder; 9 | using Microsoft.Bot.Builder.Integration.AspNet.Core; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.Logging; 12 | using Microsoft.Teams.Apps.ExpertFinder.Resources; 13 | 14 | /// 15 | /// Log any leaked exception from the application. 16 | /// 17 | public class AdapterWithErrorHandler : BotFrameworkHttpAdapter 18 | { 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | /// Object that passes the application configuration key-values. 23 | /// Instance to send logs to the Application Insights service. 24 | /// State management object for maintaining conversation state. 25 | public AdapterWithErrorHandler(IConfiguration configuration, ILogger logger, ConversationState conversationState) 26 | : base(configuration) 27 | { 28 | this.OnTurnError = async (turnContext, exception) => 29 | { 30 | // Log any leaked exception from the application. 31 | logger.LogError(exception, $"Exception caught : {exception.Message}"); 32 | 33 | // Send a catch-all apology to the user. 34 | await turnContext.SendActivityAsync(Strings.ErrorMessage).ConfigureAwait(false); 35 | 36 | if (conversationState != null) 37 | { 38 | logger.LogTrace($"Clearing conversation state for {turnContext.Activity?.Conversation?.Id}"); 39 | try 40 | { 41 | // Delete the conversationState for the current conversation to prevent the 42 | // bot from getting stuck in a error-loop caused by being in a bad state. 43 | // ConversationState should be thought of as similar to "cookie-state" in a Web pages. 44 | await conversationState.DeleteAsync(turnContext).ConfigureAwait(false); 45 | } 46 | catch (Exception ex) 47 | { 48 | logger.LogError(ex, $"Exception caught on attempting to Delete ConversationState : {ex.Message}"); 49 | } 50 | } 51 | }; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Bots/BotLocalizationCultureProvider.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Bots 6 | { 7 | using System; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | using Microsoft.AspNetCore.Http; 13 | using Microsoft.AspNetCore.Localization; 14 | using Microsoft.Bot.Schema; 15 | using Newtonsoft.Json; 16 | using Newtonsoft.Json.Linq; 17 | 18 | /// 19 | /// This class is responsible for implementing the for Bot Activities 20 | /// received from BotFramework. 21 | /// 22 | public class BotLocalizationCultureProvider : IRequestCultureProvider 23 | { 24 | /// 25 | /// Get the culture of current request. 26 | /// 27 | /// The current HTTP request. 28 | /// A Task resolving to the culture info if found, null otherwise. 29 | #pragma warning disable UseAsyncSuffix // Interface method doesn't have Async suffix. 30 | public async Task DetermineProviderCultureResult(HttpContext httpContext) 31 | #pragma warning restore UseAsyncSuffix 32 | { 33 | if (httpContext?.Request?.Body?.CanRead != true) 34 | { 35 | return null; 36 | } 37 | 38 | string locale = string.Empty; 39 | var isBotFrameworkUserAgent = 40 | httpContext.Request.Headers["User-Agent"] 41 | .Any(userAgent => userAgent.Contains("Microsoft-BotFramework", StringComparison.OrdinalIgnoreCase)); 42 | 43 | if (!isBotFrameworkUserAgent) 44 | { 45 | locale = httpContext.Request.Headers["Accept-Language"].FirstOrDefault(); 46 | locale = locale?.Split(",")?.FirstOrDefault(); 47 | if (string.IsNullOrEmpty(locale)) 48 | { 49 | return null; 50 | } 51 | } 52 | 53 | try 54 | { 55 | if (isBotFrameworkUserAgent) 56 | { 57 | // Wrap the request stream so that we can rewind it back to the start for regular request processing. 58 | httpContext.Request.EnableBuffering(); 59 | 60 | // Read the request body, parse out the activity object, and set the parsed culture information. 61 | var streamReader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, leaveOpen: true); 62 | using (var jsonReader = new JsonTextReader(streamReader)) 63 | { 64 | var obj = await JObject.LoadAsync(jsonReader); 65 | var activity = obj.ToObject(); 66 | 67 | var result = new ProviderCultureResult(activity.Locale); 68 | httpContext.Request.Body.Seek(0, SeekOrigin.Begin); 69 | return result; 70 | } 71 | } 72 | else 73 | { 74 | var result = new ProviderCultureResult(locale); 75 | return result; 76 | } 77 | } 78 | #pragma warning disable CA1031 // part of the middle ware pipeline, better to use default local then fail the request. 79 | catch (Exception) 80 | #pragma warning restore CA1031 81 | { 82 | return null; 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Cards/HelpCard.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Cards 6 | { 7 | using System.Collections.Generic; 8 | using AdaptiveCards; 9 | using Microsoft.Bot.Schema; 10 | using Microsoft.Teams.Apps.ExpertFinder.Common; 11 | using Microsoft.Teams.Apps.ExpertFinder.Models; 12 | using Microsoft.Teams.Apps.ExpertFinder.Resources; 13 | 14 | /// 15 | /// Class that contains method for help card attachment. 16 | /// 17 | public static class HelpCard 18 | { 19 | /// 20 | /// Get help card attchment that will give available commands to user if user has provided invalid command. 21 | /// 22 | /// Help adaptive card attachment. 23 | public static Attachment GetHelpCard() 24 | { 25 | AdaptiveCard helpCard = new AdaptiveCard(new AdaptiveSchemaVersion(1, 0)) 26 | { 27 | Body = new List 28 | { 29 | new AdaptiveTextBlock 30 | { 31 | HorizontalAlignment = AdaptiveHorizontalAlignment.Left, 32 | Text = Strings.HelpMessage, 33 | Wrap = true, 34 | }, 35 | }, 36 | Actions = new List 37 | { 38 | new AdaptiveSubmitAction 39 | { 40 | Title = Strings.SearchTitle, 41 | Data = new AdaptiveCardAction 42 | { 43 | MsteamsCardAction = new CardAction 44 | { 45 | Type = ActionTypes.MessageBack, 46 | DisplayText = Strings.SearchTitle, 47 | }, 48 | Command = Constants.Search, 49 | }, 50 | }, 51 | new AdaptiveSubmitAction 52 | { 53 | Title = Strings.MyProfileTitle, 54 | Data = new AdaptiveCardAction 55 | { 56 | MsteamsCardAction = new CardAction 57 | { 58 | Type = ActionTypes.MessageBack, 59 | DisplayText = Strings.MyProfileTitle, 60 | }, 61 | Command = Constants.MyProfile, 62 | }, 63 | }, 64 | }, 65 | }; 66 | return new Attachment 67 | { 68 | ContentType = AdaptiveCard.ContentType, 69 | Content = helpCard, 70 | }; 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Cards/MessagingExtensionUserProfileCard.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Cards 6 | { 7 | using System.Collections.Generic; 8 | using System.Globalization; 9 | using AdaptiveCards; 10 | using Microsoft.Bot.Schema; 11 | using Microsoft.Bot.Schema.Teams; 12 | using Microsoft.Teams.Apps.ExpertFinder.Models.SharePoint; 13 | using Microsoft.Teams.Apps.ExpertFinder.Resources; 14 | 15 | /// 16 | /// Class having method to return messaging extension user profile details attachments. 17 | /// 18 | public static class MessagingExtensionUserProfileCard 19 | { 20 | /// 21 | /// Message extension command id for skills. 22 | /// 23 | private const string SkillsCommandId = "skills"; 24 | 25 | /// 26 | /// Message extension command id for interests. 27 | /// 28 | private const string InterestCommandId = "interests"; 29 | 30 | /// 31 | /// Message extension command id for schools. 32 | /// 33 | private const string SchoolsCommandId = "schools"; 34 | 35 | /// 36 | /// Get user profile details messaging extension attachments for given user profiles and messaging extension command. 37 | /// 38 | /// Collection of user profile details. 39 | /// Messaging extension command name. 40 | /// List of user details messaging extension attachment. 41 | public static List GetUserDetailsCards(IList userProfiles, string commandId) 42 | { 43 | var messagingExtensionAttachments = new List(); 44 | var cardContent = string.Empty; 45 | 46 | foreach (var userProfile in userProfiles) 47 | { 48 | switch (commandId) 49 | { 50 | case SkillsCommandId: 51 | cardContent = userProfile.Skills; 52 | break; 53 | 54 | case InterestCommandId: 55 | cardContent = userProfile.Interests; 56 | break; 57 | 58 | case SchoolsCommandId: 59 | cardContent = userProfile.Schools; 60 | break; 61 | } 62 | 63 | var userCard = new AdaptiveCard(new AdaptiveSchemaVersion(1, 0)) 64 | { 65 | Body = new List 66 | { 67 | new AdaptiveTextBlock 68 | { 69 | Text = userProfile.PreferredName, 70 | Weight = AdaptiveTextWeight.Bolder, 71 | Wrap = true, 72 | }, 73 | new AdaptiveTextBlock 74 | { 75 | Text = userProfile.JobTitle, 76 | Wrap = true, 77 | Spacing = AdaptiveSpacing.None, 78 | }, 79 | new AdaptiveTextBlock 80 | { 81 | Text = Strings.AboutMeTitle, 82 | Wrap = true, 83 | }, 84 | new AdaptiveTextBlock 85 | { 86 | Text = userProfile.AboutMe, 87 | IsSubtle = true, 88 | Wrap = true, 89 | Spacing = AdaptiveSpacing.None, 90 | }, 91 | }, 92 | }; 93 | ThumbnailCard previewCard = new ThumbnailCard 94 | { 95 | Title = $"{userProfile.PreferredName}", 96 | Subtitle = userProfile.JobTitle, 97 | Text = cardContent, 98 | }; 99 | messagingExtensionAttachments.Add(new Attachment 100 | { 101 | ContentType = AdaptiveCard.ContentType, 102 | Content = userCard, 103 | }.ToMessagingExtensionAttachment(previewCard.ToAttachment())); 104 | } 105 | 106 | return messagingExtensionAttachments; 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Cards/SearchCard.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Cards 6 | { 7 | using System.Collections.Generic; 8 | using AdaptiveCards; 9 | using Microsoft.Bot.Schema; 10 | using Microsoft.Teams.Apps.ExpertFinder.Common; 11 | using Microsoft.Teams.Apps.ExpertFinder.Models; 12 | using Microsoft.Teams.Apps.ExpertFinder.Resources; 13 | 14 | /// 15 | /// Class having methods related to user search card attachment and user profile details card attachment. 16 | /// 17 | public static class SearchCard 18 | { 19 | /// 20 | /// Url to initiate teams 1:1 chat with user. 21 | /// 22 | private const string InitiateChatUrl = "https://teams.microsoft.com/l/chat/0/0?users="; 23 | 24 | /// 25 | /// Card attachment to show on search command. 26 | /// 27 | /// Fetch action user search card attachment. 28 | public static Attachment GetSearchCard() 29 | { 30 | var searchCard = new AdaptiveCard(new AdaptiveSchemaVersion(1, 0)) 31 | { 32 | Body = new List 33 | { 34 | new AdaptiveTextBlock 35 | { 36 | Text = Strings.SearchCardContent, 37 | Wrap = true, 38 | }, 39 | }, 40 | Actions = new List 41 | { 42 | new AdaptiveSubmitAction 43 | { 44 | Title = Strings.SearchTitle, 45 | Data = new AdaptiveCardAction 46 | { 47 | MsteamsCardAction = new CardAction 48 | { 49 | Type = Constants.FetchActionType, 50 | }, 51 | Command = Constants.Search, 52 | }, 53 | }, 54 | }, 55 | }; 56 | return new Attachment 57 | { 58 | ContentType = AdaptiveCard.ContentType, 59 | Content = searchCard, 60 | }; 61 | } 62 | 63 | /// 64 | /// User detail card attachment for given user profile. 65 | /// 66 | /// User profile details. 67 | /// User profile details card attachment. 68 | public static Attachment GetUserCard(Models.SharePoint.UserProfileDetail userDetail) 69 | { 70 | var skills = string.IsNullOrEmpty(userDetail.Skills) ? Strings.NoneText : userDetail.Skills; 71 | var interests = string.IsNullOrEmpty(userDetail.Interests) ? Strings.NoneText : userDetail.Interests; 72 | var schools = string.IsNullOrEmpty(userDetail.Schools) ? Strings.NoneText : userDetail.Schools; 73 | 74 | var userDetailCard = new AdaptiveCard(new AdaptiveSchemaVersion(1, 0)) 75 | { 76 | Body = new List 77 | { 78 | new AdaptiveTextBlock 79 | { 80 | Text = userDetail.PreferredName, 81 | Wrap = true, 82 | Weight = AdaptiveTextWeight.Bolder, 83 | }, 84 | new AdaptiveTextBlock 85 | { 86 | Text = userDetail.JobTitle, 87 | Wrap = true, 88 | Spacing = AdaptiveSpacing.None, 89 | }, 90 | new AdaptiveTextBlock 91 | { 92 | Text = $"_{Strings.SkillsTitle}_", 93 | Wrap = true, 94 | }, 95 | new AdaptiveTextBlock 96 | { 97 | Text = skills, 98 | Wrap = true, 99 | Spacing = AdaptiveSpacing.None, 100 | }, 101 | }, 102 | Actions = new List 103 | { 104 | new AdaptiveOpenUrlAction 105 | { 106 | Title = Strings.ChatTitle, 107 | Url = new System.Uri($"{InitiateChatUrl}{userDetail.WorkEmail}"), 108 | }, 109 | new AdaptiveShowCardAction 110 | { 111 | Title = Strings.DetailsTitle, 112 | Card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 0)) 113 | { 114 | Body = new List 115 | { 116 | new AdaptiveTextBlock 117 | { 118 | Text = Strings.AboutMeTitle, 119 | Separator = true, 120 | Wrap = true, 121 | Weight = AdaptiveTextWeight.Bolder, 122 | }, 123 | new AdaptiveTextBlock 124 | { 125 | Text = userDetail.AboutMe, 126 | Wrap = true, 127 | Spacing = AdaptiveSpacing.None, 128 | }, 129 | new AdaptiveTextBlock 130 | { 131 | Text = Strings.InterestTitle, 132 | Separator = true, 133 | Wrap = true, 134 | Weight = AdaptiveTextWeight.Bolder, 135 | }, 136 | new AdaptiveTextBlock 137 | { 138 | Text = interests, 139 | Wrap = true, 140 | Spacing = AdaptiveSpacing.None, 141 | }, 142 | new AdaptiveTextBlock 143 | { 144 | Text = Strings.SchoolsTitle, 145 | Separator = true, 146 | Wrap = true, 147 | Weight = AdaptiveTextWeight.Bolder, 148 | }, 149 | new AdaptiveTextBlock 150 | { 151 | Text = schools, 152 | Wrap = true, 153 | Spacing = AdaptiveSpacing.None, 154 | }, 155 | }, 156 | Actions = new List 157 | { 158 | new AdaptiveOpenUrlAction 159 | { 160 | Title = Strings.GotoProfileTitle, 161 | Url = new System.Uri($"{userDetail.Path}&v=profiledetails"), 162 | }, 163 | }, 164 | }, 165 | }, 166 | }, 167 | }; 168 | return new Attachment 169 | { 170 | ContentType = AdaptiveCard.ContentType, 171 | Content = userDetailCard, 172 | }; 173 | } 174 | } 175 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Cards/WelcomeCard.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Cards 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using AdaptiveCards; 10 | using Microsoft.Bot.Schema; 11 | using Microsoft.Teams.Apps.ExpertFinder.Common; 12 | using Microsoft.Teams.Apps.ExpertFinder.Models; 13 | using Microsoft.Teams.Apps.ExpertFinder.Resources; 14 | 15 | /// 16 | /// Implements Welcome Card. 17 | /// 18 | public static class WelcomeCard 19 | { 20 | /// 21 | /// This method will construct the user welcome card when bot is added by user. 22 | /// 23 | /// Application base url. 24 | /// User welcome card attchment. 25 | public static Attachment GetCard(string appBaseUrl) 26 | { 27 | var userWelcomeCard = new AdaptiveCard(new AdaptiveSchemaVersion(1, 0)) 28 | { 29 | Body = new List 30 | { 31 | new AdaptiveColumnSet 32 | { 33 | Columns = new List 34 | { 35 | new AdaptiveColumn 36 | { 37 | Width = AdaptiveColumnWidth.Auto, 38 | Items = new List 39 | { 40 | new AdaptiveImage 41 | { 42 | Url = new Uri($"{appBaseUrl}/Artifacts/appLogo.png"), 43 | Size = AdaptiveImageSize.Large, 44 | }, 45 | }, 46 | }, 47 | new AdaptiveColumn 48 | { 49 | Width = AdaptiveColumnWidth.Auto, 50 | Items = new List 51 | { 52 | new AdaptiveTextBlock 53 | { 54 | Size = AdaptiveTextSize.Large, 55 | Wrap = true, 56 | Text = Strings.WelcomeText, 57 | Weight = AdaptiveTextWeight.Bolder, 58 | }, 59 | new AdaptiveTextBlock 60 | { 61 | Size = AdaptiveTextSize.Default, 62 | Wrap = true, 63 | Text = Strings.WelcomeCardContent, 64 | }, 65 | }, 66 | }, 67 | }, 68 | }, 69 | new AdaptiveTextBlock 70 | { 71 | HorizontalAlignment = AdaptiveHorizontalAlignment.Left, 72 | Text = $"**{Strings.SearchTitle}**: {Strings.SearchWelcomeCardContent}", 73 | Wrap = true, 74 | }, 75 | new AdaptiveTextBlock 76 | { 77 | HorizontalAlignment = AdaptiveHorizontalAlignment.Left, 78 | Text = $"**{Strings.MyProfileTitle}**: {Strings.MyProfileWelcomeCardContent}", 79 | Wrap = true, 80 | }, 81 | }, 82 | Actions = new List 83 | { 84 | new AdaptiveSubmitAction 85 | { 86 | Title = Strings.SearchTitle, 87 | Data = new AdaptiveCardAction 88 | { 89 | MsteamsCardAction = new CardAction 90 | { 91 | Type = ActionTypes.MessageBack, 92 | DisplayText = Strings.SearchTitle, 93 | }, 94 | Command = Constants.Search, 95 | }, 96 | }, 97 | new AdaptiveSubmitAction 98 | { 99 | Title = Strings.MyProfileTitle, 100 | Data = new AdaptiveCardAction 101 | { 102 | MsteamsCardAction = new CardAction 103 | { 104 | Type = ActionTypes.MessageBack, 105 | DisplayText = Strings.MyProfileTitle, 106 | }, 107 | Command = Constants.MyProfile, 108 | }, 109 | }, 110 | }, 111 | }; 112 | 113 | return new Attachment 114 | { 115 | ContentType = AdaptiveCard.ContentType, 116 | Content = userWelcomeCard, 117 | }; 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expert-finder", 3 | "version": "1.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@microsoft/applicationinsights-react-js": "^2.3.1", 7 | "@microsoft/teams-js": "^1.10.0", 8 | "@stardust-ui/react": "^0.40.4", 9 | "axios": "^0.21.1", 10 | "msteams-ui-icons-react": "^0.4.2", 11 | "react": "^16.12.0", 12 | "react-appinsights": "^3.0.0-rc.6", 13 | "react-dom": "^16.12.0", 14 | "react-router-dom": "^5.1.2", 15 | "react-scripts": "^4.0.3", 16 | "typescript": "^3.7.4", 17 | "typestyle": "^2.0.4" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": "react-app" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "devDependencies": { 41 | "@testing-library/jest-dom": "^4.2.4", 42 | "@testing-library/react": "^9.4.0", 43 | "@testing-library/user-event": "^7.2.1", 44 | "@types/jest": "^24.0.24", 45 | "@types/node": "^12.12.21", 46 | "@types/react": "^16.9.17", 47 | "@types/react-dom": "^16.9.4", 48 | "@types/react-router-dom": "^4.3.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/public/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 11 | Expert finder 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/src/api/axiosDecorator.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import axios, { AxiosResponse, AxiosRequestConfig } from "axios"; 6 | 7 | export class AxiosJWTDecorator { 8 | 9 | /** 10 | * Post data to api 11 | * @param {String} url Resource uri 12 | * @param {Object} data Request body data 13 | * @param {String} token Custom jwt token 14 | */ 15 | public async post>( 16 | url: string, 17 | data?: any, 18 | token?: string 19 | ): Promise { 20 | try { 21 | let config: AxiosRequestConfig = axios.defaults; 22 | config.headers["Authorization"] = `Bearer ${token}`; 23 | 24 | return await axios.post(url, data, config); 25 | } catch (error) { 26 | this.handleError(error); 27 | throw error; 28 | } 29 | } 30 | 31 | /** 32 | * Get data to api 33 | * @param {String} token Custom jwt token 34 | */ 35 | public async get>( 36 | url: string, 37 | token?: string, 38 | locale?: string | null 39 | ): Promise { 40 | try { 41 | let config: AxiosRequestConfig = axios.defaults; 42 | config.headers["Authorization"] = `Bearer ${token}`; 43 | if (locale) { 44 | config.headers["Accept-Language"] = `${locale}`; 45 | } 46 | return await axios.get(url, config); 47 | } catch (error) { 48 | this.handleError(error); 49 | throw error; 50 | } 51 | } 52 | 53 | /** 54 | * Handle error occured during api call. 55 | * @param {Object} error Error response object 56 | */ 57 | private handleError(error: any): void { 58 | if (error.hasOwnProperty("response")) { 59 | const errorStatus = error.response.status; 60 | if (errorStatus === 403) { 61 | window.location.href = "/errorpage/403"; 62 | } else if (errorStatus === 401) { 63 | window.location.href = "/errorpage/401"; 64 | } else { 65 | window.location.href = "/errorpage"; 66 | } 67 | } else { 68 | window.location.href = "/errorpage"; 69 | } 70 | } 71 | 72 | } 73 | 74 | const axiosJWTDecoratorInstance = new AxiosJWTDecorator(); 75 | export default axiosJWTDecoratorInstance; -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/src/api/profileSearchApi.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import axios from "./axiosDecorator"; 6 | 7 | const baseAxiosUrl = window.location.origin; 8 | 9 | /** 10 | * Get user profiles from api 11 | * @param {String} searchText User entered search text 12 | * @param {String Array} filters User selected filters 13 | * @param {String | Null} token Custom jwt token 14 | */ 15 | export const getUserProfiles = async (searchText: string, filters: string[], token: any): Promise => { 16 | 17 | let url = baseAxiosUrl + "/api/users"; 18 | const data = { 19 | searchText: searchText, 20 | SearchFilters: filters 21 | }; 22 | return await axios.post(url, data, token); 23 | } 24 | 25 | /** 26 | * Get localized resource strings from api 27 | */ 28 | export const getResourceStrings = async (token: any, locale: string): Promise => { 29 | 30 | let url = baseAxiosUrl + "/api/resources/strings"; 31 | return await axios.get(url, token, locale); 32 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/src/app.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import * as React from "react"; 6 | import * as microsoftTeams from "@microsoft/teams-js"; 7 | import { AppRoute } from "./router/router"; 8 | import Resources from "./constants/resources"; 9 | import { Provider, themes } from "@stardust-ui/react"; 10 | import { RouteComponentProps } from "react-router-dom"; 11 | 12 | export interface IAppState { 13 | theme: string; 14 | themeStyle: any; 15 | } 16 | 17 | export default class App extends React.Component<{}, IAppState> { 18 | 19 | constructor(props: any) { 20 | super(props); 21 | this.state = { 22 | theme: "", 23 | themeStyle: themes.teams, 24 | } 25 | } 26 | 27 | /** 28 | * Initializes Microsft Teams sdk and get current theme from teams context 29 | */ 30 | public componentDidMount() { 31 | microsoftTeams.initialize(); 32 | microsoftTeams.getContext((context) => { 33 | let theme = context.theme || ""; 34 | this.updateTheme(theme); 35 | this.setState({ 36 | theme: theme 37 | }); 38 | }); 39 | 40 | microsoftTeams.registerOnThemeChangeHandler((theme) => { 41 | this.updateTheme(theme); 42 | this.setState({ 43 | theme: theme, 44 | }, () => { 45 | this.forceUpdate(); 46 | }); 47 | }); 48 | } 49 | 50 | /** 51 | * Set current theme state received from teams context 52 | * @param {String} theme Current theme name 53 | */ 54 | private updateTheme = (theme: string) => { 55 | if (theme === Resources.dark) { 56 | this.setState({ 57 | themeStyle: themes.teamsDark 58 | }); 59 | } else if (theme === Resources.contrast) { 60 | this.setState({ 61 | themeStyle: themes.teamsHighContrast 62 | }); 63 | } else { 64 | this.setState({ 65 | themeStyle: themes.teams 66 | }); 67 | } 68 | 69 | if (theme) { 70 | // Possible values for theme: "default", "light", "dark" and "contrast" 71 | document.querySelector(Resources.body) 72 | document.body.className = Resources.theme + "-" + (theme === Resources.default ? Resources.light : theme); 73 | } 74 | } 75 | 76 | /** 77 | * Renders the component 78 | */ 79 | public render(): JSX.Element { 80 | 81 | return ( 82 | 83 |
84 | 85 |
86 |
87 | ); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/src/components/emptySearchResultMessage.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import * as React from "react"; 6 | import { MSTeamsIcon, MSTeamsIconWeight, MSTeamsIconType } from "msteams-ui-icons-react"; 7 | 8 | interface IEmptySearchResultMessageProps { 9 | NoSearchResultFoundMessage: string, 10 | } 11 | 12 | const EmptySearchResultMessage: React.FunctionComponent = props => { 13 | 14 | return ( 15 |
16 |
17 |
18 | 19 |
20 | 21 |
22 |
23 | {props.NoSearchResultFoundMessage} 24 |
25 |
26 |
27 |
28 | ); 29 | } 30 | 31 | export default EmptySearchResultMessage; -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/src/components/errorPage.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import * as React from "react"; 6 | import { RouteComponentProps, Link } from "react-router-dom"; 7 | import { Text, Loader } from "@stardust-ui/react"; 8 | import { IAppSettings } from "./searchUserWrapperPage"; 9 | import { getResourceStrings } from "../api/profileSearchApi"; 10 | import * as microsoftTeams from "@microsoft/teams-js"; 11 | import "../styles/site.css"; 12 | 13 | interface IResourceString { 14 | unauthorizedErrorMessage: string, 15 | forbiddenErrorMessage: string, 16 | generalErrorMessage: string, 17 | refreshLinkText: string 18 | } 19 | 20 | interface errorPageState { 21 | loader: boolean; 22 | resourceStrings: IResourceString, 23 | } 24 | 25 | export class ErrorPage extends React.Component { 26 | locale: string = ""; 27 | private appSettings: IAppSettings = { 28 | telemetry: "", 29 | theme: "", 30 | token: "" 31 | }; 32 | 33 | constructor(props: any) { 34 | super(props); 35 | this.state = { 36 | loader: true, 37 | resourceStrings: { 38 | unauthorizedErrorMessage: "Sorry, an error occurred while trying to access this service.", 39 | forbiddenErrorMessage: "Sorry, seems like you don't have permission to access this page.", 40 | generalErrorMessage: "Oops! An unexpected error seems to have occured. Why not try refreshing your page? Or you can contact your administrator if the problem persists.", 41 | refreshLinkText: "Refresh" 42 | } 43 | }; 44 | let storageValue = localStorage.getItem("appsettings"); 45 | if (storageValue) { 46 | this.appSettings = JSON.parse(storageValue) as IAppSettings; 47 | } 48 | } 49 | 50 | async componentDidMount() { 51 | microsoftTeams.initialize(); 52 | microsoftTeams.getContext((context) => { 53 | this.locale = context.locale; 54 | this.getResourceStrings(); 55 | }); 56 | } 57 | 58 | /** 59 | *Get localized resource strings from API 60 | */ 61 | async getResourceStrings() { 62 | let response = await getResourceStrings(this.appSettings.token, this.locale); 63 | 64 | if (response.status === 200 && response.data) { 65 | this.setState({ 66 | loader: false, 67 | resourceStrings: response.data 68 | }); 69 | } 70 | else { 71 | this.setState({ 72 | loader: false 73 | }); 74 | } 75 | } 76 | 77 | /** 78 | * Renders the component 79 | */ 80 | public render(): JSX.Element { 81 | 82 | const params = this.props.match.params; 83 | let message = `${this.state.resourceStrings.generalErrorMessage}`; 84 | 85 | if ("id" in params) { 86 | const id = params["id"]; 87 | if (id === "401") { 88 | message = `${this.state.resourceStrings.unauthorizedErrorMessage}`; 89 | } else if (id === "403") { 90 | message = `${this.state.resourceStrings.forbiddenErrorMessage}`; 91 | } 92 | else { 93 | message = `${this.state.resourceStrings.generalErrorMessage}`; 94 | } 95 | } 96 | if (!this.state.loader) { 97 | return ( 98 |
99 | 100 | {this.state.resourceStrings.refreshLinkText} 101 |
102 | ); 103 | } 104 | else { 105 | return ( 106 |
107 | 108 |
109 | ); 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/src/components/filterCheckboxGroup.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import * as React from "react"; 6 | import Resources from "../constants/resources"; 7 | import { ISelectedFilter } from "./searchResultInitialMessage"; 8 | import { Checkbox } from "@stardust-ui/react"; 9 | 10 | interface IUserCheckboxGroupProps { 11 | selectedFilterValues: ISelectedFilter[], 12 | onFilterSelectionChange: (values: string[]) => void, 13 | SkillsLabel: string, 14 | InterestsLabel: string, 15 | SchoolsLabel: string 16 | } 17 | 18 | const FilterCheckboxGroup: React.FunctionComponent = props => { 19 | 20 | let userSelectedFilterValues = props.selectedFilterValues.map(filter => filter.value) 21 | 22 | function isCheckboxChecked(value: string) { 23 | 24 | const selectedFilters = userSelectedFilterValues.filter(filterValue => filterValue === value); 25 | 26 | if (selectedFilters.length) { 27 | return true; 28 | } else { 29 | return false; 30 | } 31 | } 32 | 33 | function onCheckboxChecked(value: string) { 34 | let isFilterSelected = userSelectedFilterValues.includes(value) 35 | let selectedFilterValues: string[] = []; 36 | if (isFilterSelected) { 37 | selectedFilterValues = userSelectedFilterValues.filter(filterValue => filterValue !== value) 38 | } 39 | else { 40 | selectedFilterValues = [...userSelectedFilterValues, value] 41 | } 42 | props.onFilterSelectionChange(selectedFilterValues); 43 | } 44 | 45 | return ( 46 |
47 |
48 | onCheckboxChecked(Resources.SkillsValue)} label={props.SkillsLabel} checked={isCheckboxChecked(Resources.SkillsValue)} /> 49 |
50 |
51 | onCheckboxChecked(Resources.interestsValue)} label={props.InterestsLabel} checked={isCheckboxChecked(Resources.interestsValue)} /> 52 |
53 |
54 | onCheckboxChecked(Resources.schoolsValue)} label={props.SchoolsLabel} checked={isCheckboxChecked(Resources.schoolsValue)} /> 55 |
56 |
57 | ); 58 | } 59 | 60 | export default FilterCheckboxGroup; -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/src/components/filterNamesComponent.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import * as React from "react"; 6 | import { MSTeamsIcon, MSTeamsIconWeight, MSTeamsIconType } from "msteams-ui-icons-react"; 7 | import Resources from "../constants/resources"; 8 | import "../styles/userProfile.css"; 9 | import { ISelectedFilter } from "./searchResultInitialMessage"; 10 | 11 | interface IFilterNamesComponentProps { 12 | selectedFilters: ISelectedFilter[], 13 | onFilterRemoved: (selectedFilter: ISelectedFilter) => void 14 | } 15 | 16 | export class FilterNamesComponent extends React.Component { 17 | 18 | constructor(props: IFilterNamesComponentProps) { 19 | super(props); 20 | } 21 | 22 | /** 23 | * Remove filter from selected filter collection 24 | * @param {ISelectedFilter} filter User entered search text 25 | */ 26 | private onCloseClick = (filter: ISelectedFilter) => { 27 | this.props.onFilterRemoved(filter); 28 | } 29 | 30 | /** 31 | * Remove filter from selected filter collection 32 | * @param {ISelectedFilter} filter User entered search text 33 | * @param {Object} event Event object 34 | */ 35 | private onCloseKeyPress = (filter: ISelectedFilter, event) => { 36 | var keyCode = event.which || event.keyCode; 37 | if (keyCode === Resources.keyCodeEnter || keyCode === Resources.keyCodeSpace) { 38 | this.onCloseClick(filter); 39 | } 40 | } 41 | 42 | /** 43 | * Renders the component 44 | */ 45 | public render(): JSX.Element[] { 46 | 47 | return (this.props.selectedFilters.map((filter, key) => { 48 | return ( 49 |
50 |
51 |
52 |
{filter.label}
53 | this.onCloseClick(filter)} 56 | onKeyDown={(event) => this.onCloseKeyPress(filter, event)}> 57 | 61 | 62 |
63 |
64 |
65 | ); 66 | }) 67 | ) 68 | } 69 | 70 | styles = { 71 | icon: { 72 | fontSize: "1rem" 73 | }, 74 | } 75 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/src/components/filterPopUp.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import * as React from "react"; 6 | import { MSTeamsIcon, MSTeamsIconWeight, MSTeamsIconType } from "msteams-ui-icons-react"; 7 | import { Flex, Popup } from "@stardust-ui/react"; 8 | import { ISelectedFilter } from "./searchResultInitialMessage"; 9 | import FilterCheckboxGroup from "./filterCheckboxGroup"; 10 | 11 | interface ITeamsSearchUserProps { 12 | selectedFilterValues: ISelectedFilter[], 13 | onFilterSelectionChange: (values: string[]) => void, 14 | SkillsLabel: string, 15 | InterestsLabel: string, 16 | SchoolsLabel: string 17 | } 18 | 19 | export default class FilterPopUp extends React.Component { 20 | 21 | constructor(props: ITeamsSearchUserProps) { 22 | super(props); 23 | } 24 | 25 | /** 26 | * Notify parent component that filter selection change 27 | * @param {String Array} values Selected filters 28 | */ 29 | onGroupChecked = (values: string[]) => { 30 | this.props.onFilterSelectionChange(values); 31 | }; 32 | 33 | /** 34 | * Renders the component 35 | */ 36 | public render(): JSX.Element { 37 | 38 | return ( 39 | 40 | 44 | 45 | 46 | } 47 | content={ 48 | 55 | } 56 | /> 57 | 58 | ); 59 | } 60 | } 61 | 62 | 63 | -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/src/components/initialResultMessage.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import * as React from "react"; 6 | import { MSTeamsIcon, MSTeamsIconWeight, MSTeamsIconType } from "msteams-ui-icons-react"; 7 | 8 | interface IInitialResultMessageProps { 9 | InitialResultMessageHeaderText: string, 10 | InitialResultMessageBodyText: string, 11 | } 12 | 13 | const InitialResultMessage: React.FunctionComponent = props => { 14 | 15 | return ( 16 |
17 |
18 |
19 | 23 |
24 | 25 |
26 |
27 | {props.InitialResultMessageHeaderText} 28 |
29 |
30 | {props.InitialResultMessageBodyText} 31 |
32 |
33 |
34 |
35 | ); 36 | } 37 | 38 | export default InitialResultMessage; -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/src/components/profileSearchTextBox.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import * as React from "react"; 6 | import { Input } from "@stardust-ui/react"; 7 | import Resources from "../constants/resources"; 8 | 9 | 10 | interface IInputControlProps { 11 | selectSearchText: (searchText: string) => void, 12 | placeHolderText: string 13 | } 14 | 15 | interface IInputControlState { 16 | value: string 17 | } 18 | 19 | export default class ProfileSearchTextBoxComponent extends React.Component { 20 | 21 | constructor(props: IInputControlProps) { 22 | super(props); 23 | this.state = { value: "" }; 24 | this.handleChange = this.handleChange.bind(this); 25 | this.handleKeyPress = this.handleKeyPress.bind(this); 26 | } 27 | 28 | /** 29 | * Set State value of textbox input control 30 | * @param {Any} e Event object 31 | */ 32 | handleChange(e: any) { 33 | this.setState({ value: e.target.value }); 34 | } 35 | 36 | /** 37 | * Used to call parent search method on enter keypress in textbox 38 | * @param {Any} event Event object 39 | */ 40 | handleKeyPress(event: any) { 41 | var keyCode = event.which || event.keyCode; 42 | if (keyCode === Resources.keyCodeEnter) { 43 | this.props.selectSearchText(event.target.value); 44 | } 45 | } 46 | 47 | /** 48 | * Renders the component 49 | */ 50 | public render() { 51 | return ( 52 |
53 | 64 |
65 | ); 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/src/components/searchResultInitialMessage.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import * as React from "react"; 6 | import EmptySearchResultMessage from "./emptySearchResultMessage"; 7 | import InitialResultMessage from "./initialResultMessage"; 8 | import UserProfilesList from "./userProfilesList"; 9 | import "../styles/userProfile.css"; 10 | 11 | export interface IUserProfile { 12 | aboutMe: string, 13 | interests: string, 14 | jobTitle: string, 15 | path: string, 16 | preferredName: string, 17 | schools: string, 18 | skills: string, 19 | workEmail: string 20 | } 21 | 22 | export interface ISearchResultProps { 23 | isSearching: boolean; 24 | searchResultList: IUserProfile[]; 25 | selectedProfiles: IUserProfile[]; 26 | onCheckboxSelected: (profile: IUserProfile, status: boolean) => void, 27 | InitialResultMessageHeaderText: string, 28 | InitialResultMessageBodyText: string, 29 | SkillsLabel: string, 30 | NoSearchResultFoundMessage: string 31 | } 32 | 33 | export interface ISelectedFilter { 34 | value: string; 35 | label: string; 36 | } 37 | 38 | export class SearchResultMessage extends React.Component { 39 | 40 | constructor(props: ISearchResultProps) { 41 | super(props); 42 | } 43 | 44 | /** 45 | * Renders the component 46 | */ 47 | public render(): JSX.Element { 48 | 49 | if (!this.props.isSearching) { 50 | return ( 51 | 54 | ); 55 | } 56 | else if (this.props.searchResultList.length > 0) { 57 | return ( 58 | 59 | ); 60 | } 61 | else { 62 | return ( 63 | 64 | ) 65 | } 66 | } 67 | }; -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/src/components/userProfilesList.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import * as React from "react"; 6 | import { IUserProfile } from "./searchResultInitialMessage"; 7 | import { Checkbox } from "@stardust-ui/react"; 8 | 9 | interface IUserProfilesListProps { 10 | searchResultList: IUserProfile[]; 11 | selectedProfiles: IUserProfile[]; 12 | onCheckboxSelected: (profile: IUserProfile, status: boolean) => void, 13 | SkillsLabel: string 14 | } 15 | 16 | const UserProfilesList: React.FunctionComponent = props => { 17 | 18 | /** 19 | * Used in checkbox component to decide whether checkbox is checked or not. 20 | * @param {IUserProfile} value Selected user profile 21 | */ 22 | function isCheckboxChecked(value: IUserProfile) { 23 | const selectedProfile = props.selectedProfiles.filter(userProfile => userProfile.preferredName === value.preferredName); 24 | 25 | if (selectedProfile.length) { 26 | return true; 27 | } else { 28 | return false; 29 | } 30 | } 31 | 32 | /** 33 | * Notify parent component that profile selection change 34 | * @param {IUserProfile} value Selected user profile 35 | */ 36 | function onCheckboxChecked(value: IUserProfile) { 37 | let isProfileSelected = isCheckboxChecked(value) 38 | props.onCheckboxSelected(value, !isProfileSelected); 39 | 40 | } 41 | 42 | let profilesNamesList = props.searchResultList.map((item, key) => { 43 | return (
44 |
45 |
46 | onCheckboxChecked(item)} 48 | checked={isCheckboxChecked(item)} /> 49 |
50 |
51 |
{item.preferredName}
52 |
{item.jobTitle}
53 |
{props.SkillsLabel}: {item.skills}
54 |
55 |
56 |
); 57 | }); 58 | return ( 59 |
60 | {profilesNamesList} 61 |
62 | ); 63 | } 64 | 65 | export default UserProfilesList; -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/src/constants/resources.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | export default class Resources { 6 | 7 | //Themes 8 | public static readonly body: string = "body"; 9 | public static readonly theme: string = "theme"; 10 | public static readonly default: string = "default"; 11 | public static readonly light: string = "light"; 12 | public static readonly dark: string = "dark"; 13 | public static readonly contrast: string = "contrast"; 14 | 15 | //KeyCodes 16 | public static readonly keyCodeEnter: number = 13; 17 | public static readonly keyCodeSpace: number = 32; 18 | 19 | //Profile Search 20 | public static readonly MaxUserProfileLimit: number = 5; 21 | 22 | //Search Popup 23 | public static readonly SkillsValue: string = "skills"; 24 | public static readonly interestsValue: string = "interests"; 25 | public static readonly schoolsValue: string = "schools"; 26 | 27 | //Task Module 28 | public static readonly UserSearchBotCommand: string = "search"; 29 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/src/index.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import * as React from "react"; 6 | import * as ReactDOM from "react-dom"; 7 | import { BrowserRouter as Router } from "react-router-dom"; 8 | import App from "./app"; 9 | 10 | 11 | ReactDOM.render( 12 | 13 | 14 | , document.getElementById("root")); -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/src/router/router.tsx: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | import * as React from "react"; 6 | import { BrowserRouter, Route, Switch } from "react-router-dom"; 7 | import { ProfileSearchWrapperPage } from "../components/searchUserWrapperPage"; 8 | import { ErrorPage } from "../components/errorPage"; 9 | 10 | export const AppRoute: React.FunctionComponent<{}> = () => { 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | 25 | -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/src/styles/site.css: -------------------------------------------------------------------------------- 1 |  2 | .Loader { 3 | background-color: #fff; 4 | width: 100%; 5 | margin: 17rem auto; 6 | } 7 | 8 | .search-textbox-container { 9 | width: 90%; 10 | display: inline-block; 11 | } 12 | 13 | .search-textbox-header { 14 | padding-top: 1.5rem; 15 | padding-bottom: 0.5rem; 16 | padding-left: 3.2rem; 17 | font-size: 1.3rem; 18 | } 19 | 20 | .search-filter-container { 21 | display: inline-block; 22 | margin-left: 1rem; 23 | vertical-align: middle; 24 | cursor: pointer; 25 | } 26 | 27 | .search-textbox { 28 | padding-left: 3.2rem; 29 | } 30 | 31 | .filter-icon { 32 | line-height: 1.7rem; 33 | cursor: pointer; 34 | } 35 | 36 | .search-profiles-seperator { 37 | padding-top: 1.5rem; 38 | margin-left: 3.2rem; 39 | margin-right: 2.3rem; 40 | border-bottom: 0.1rem solid #d2cfcf; 41 | } 42 | 43 | .initial-result-message-container { 44 | padding-top: 1.5rem; 45 | margin-left: 3.2rem; 46 | margin-right: 2.3rem; 47 | display: flex; 48 | } 49 | 50 | .initial-result-message-icon { 51 | display: inline-block; 52 | vertical-align: top; 53 | } 54 | 55 | .result-message-filter-icon { 56 | color: #92C353; 57 | display: inline-block; 58 | } 59 | 60 | .initial-result-message-text { 61 | display: inline-block; 62 | margin-left: 1.5rem; 63 | } 64 | 65 | .active { 66 | background-color: #464775; 67 | width: 10rem; 68 | border-radius: 5%; 69 | padding: 0.5rem; 70 | color: #fff; 71 | } 72 | 73 | .refresh-page-link { 74 | top: 17rem; 75 | } 76 | /* width */ 77 | ::-webkit-scrollbar { 78 | width: 0.7rem; 79 | } 80 | 81 | /* Handle */ 82 | ::-webkit-scrollbar-thumb { 83 | background: rgba(255,255,255,.5); 84 | border-radius: 0.5rem; 85 | } 86 | 87 | /*Theme*/ 88 | 89 | .theme-dark { 90 | background-color: #2d2c2c; 91 | color: #ffffff; 92 | } 93 | 94 | .theme-dark .Loader { 95 | background-color: #2d2c2c; 96 | } 97 | 98 | .theme-dark .appContainer { 99 | background-color: #2d2c2c; 100 | } 101 | 102 | .theme-dark .search-profiles-seperator { 103 | border-bottom-color: #636262; 104 | } 105 | 106 | 107 | .theme-contrast { 108 | background-color: #000000; 109 | color: #fff; 110 | } 111 | 112 | .theme-contrast .Loader { 113 | background-color: #000000; 114 | } 115 | 116 | .theme-contrast .appContainer { 117 | background-color: #000000; 118 | } 119 | 120 | .theme-contrast .search-profiles-seperator { 121 | border-bottom-color: #fff; 122 | } 123 | -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/src/styles/userProfile.css: -------------------------------------------------------------------------------- 1 | .user-search-list-view { 2 | position: absolute; 3 | height: auto; 4 | width: 100%; 5 | margin-top: 1rem; 6 | margin-bottom: 5.5rem; 7 | } 8 | 9 | .user-search-container { 10 | overflow-y: scroll; 11 | } 12 | 13 | .user-profile-container { 14 | display: flex; 15 | align-items: center; 16 | border: 0.2rem solid #f3f2f1; 17 | margin-top: 0.1rem; 18 | margin-bottom: 0.1rem; 19 | margin-left: 3.2rem; 20 | margin-right: 2.3rem; 21 | padding-top: 0.3rem; 22 | padding-bottom: 0.3rem; 23 | border-radius: 0.4rem; 24 | } 25 | 26 | .user-profile-checkbox { 27 | align-self: normal; 28 | margin-left: 0.8rem; 29 | margin-top: 1.8rem; 30 | } 31 | 32 | .user-profile-nametext { 33 | font-family: "Segoe UI Semibold"; 34 | font-weight: 600; 35 | letter-spacing: 0; 36 | line-height: 20px; 37 | } 38 | .user-profile-content { 39 | width: 93%; 40 | cursor: default; 41 | } 42 | 43 | .user-profile-content-skills { 44 | font-size: 1.2rem; 45 | text-overflow: ellipsis; 46 | white-space: nowrap; 47 | overflow: hidden; 48 | width: 97%; 49 | } 50 | 51 | .initial-message-header { 52 | font-family: "Segoe UI Semibold"; 53 | font-weight: 600; 54 | } 55 | 56 | .view-profile-button-container { 57 | position: fixed; 58 | width: 100%; 59 | height: 5.4rem; 60 | bottom: 0%; 61 | background-color: #fff; 62 | } 63 | 64 | .view-profile-inner-container { 65 | margin-left: 3.2rem; 66 | margin-right: 2.3rem; 67 | border-top: 0.2rem solid #f3f2f1; 68 | background-color: #fff; 69 | } 70 | 71 | .view-button-container { 72 | float: right; 73 | margin-top: 1rem; 74 | font-family: "Segoe UI Semibold"; 75 | font-weight: 600; 76 | } 77 | 78 | .selected-filters-container { 79 | margin-left: 2.5rem; 80 | } 81 | 82 | .selected-filters-innercontainer { 83 | display: flex; 84 | } 85 | 86 | .filter-name-block-container { 87 | display: flex; 88 | margin-left: 0.7rem; 89 | background-color: #f3f2f1; 90 | margin-top: 0.5rem; 91 | } 92 | 93 | .filter-name-block { 94 | display: flex; 95 | padding: 0.5rem 1.2rem; 96 | } 97 | 98 | .filter-name-text { 99 | font-family: "Segoe UI Semibold"; 100 | font-weight: 600; 101 | letter-spacing: 0; 102 | line-height: 20px; 103 | } 104 | 105 | .filter-name-close-button { 106 | align-self: center; 107 | margin-left: 0.6rem; 108 | padding: 0.2rem; 109 | } 110 | 111 | .error-message { 112 | text-align: center; 113 | width: 80%; 114 | height: 10rem; 115 | position: absolute; 116 | top: 4rem; 117 | bottom: 0; 118 | left: 0; 119 | right: 0; 120 | margin: auto; 121 | font-size: 1.5rem; 122 | } 123 | 124 | .profile-error-message { 125 | text-align: center; 126 | height: 5rem; 127 | position: absolute; 128 | top: 2.5rem; 129 | bottom: 0; 130 | left: 0; 131 | right: 0; 132 | margin: auto; 133 | } 134 | 135 | .theme-dark .user-search-list-view { 136 | background-color: #2d2c2c; 137 | } 138 | 139 | .theme-dark .view-profile-button-container { 140 | background-color: #2d2c2c; 141 | } 142 | 143 | .theme-dark .view-profile-inner-container { 144 | background-color: #2d2c2c; 145 | border-color: #636262; 146 | } 147 | 148 | .theme-dark .user-profile-container { 149 | border-color: #636262; 150 | } 151 | 152 | .theme-dark .filter-name-block-container { 153 | background-color: #2d2c2c; 154 | border: 0.5px solid #636262; 155 | } 156 | 157 | .theme-contrast .user-search-list-view { 158 | background-color: #000000; 159 | } 160 | 161 | .theme-contrast .view-profile-button-container { 162 | background-color: #000000; 163 | } 164 | 165 | .theme-contrast .view-profile-inner-container { 166 | background-color: #000000; 167 | border-color: #fff; 168 | } 169 | 170 | .theme-contrast .user-profile-container { 171 | border-color: #fff; 172 | } 173 | 174 | .theme-contrast .filter-name-block-container { 175 | background-color: #000; 176 | border: 0.5px solid #fff; 177 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noImplicitAny": false, 20 | "noEmit": true, 21 | "jsx": "preserve" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /Source/ExpertFinder/ClientApp/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint-react" 5 | ], 6 | "linterOptions": { 7 | "exclude": [ 8 | "node_modules/**/*.ts" 9 | ] 10 | }, 11 | "rules": { 12 | "jsx-no-lambda": false, 13 | "member-access": false, 14 | "no-console": false, 15 | "ordered-imports": false, 16 | "quotemark": false, 17 | "semicolon": false 18 | }, 19 | "rulesDirectory": [ 20 | ] 21 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Common/Constants.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Common 6 | { 7 | /// 8 | /// Constant values that are used in multiple places. 9 | /// 10 | public static class Constants 11 | { 12 | /// 13 | /// Text that triggers my profile action. 14 | /// 15 | public const string MyProfile = "MY PROFILE"; 16 | 17 | /// 18 | /// Text that triggers search user action. 19 | /// 20 | public const string Search = "SEARCH"; 21 | 22 | /// 23 | /// Task fetch action Type. 24 | /// 25 | public const string FetchActionType = "task/fetch"; 26 | } 27 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Common/Extensions/SharePointSearchCellsResultExtension.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Common.Extensions 6 | { 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using Microsoft.Teams.Apps.ExpertFinder.Models.SharePoint; 10 | 11 | /// 12 | /// A class that extends class to get given key value. 13 | /// 14 | public static class SharePointSearchCellsResultExtension 15 | { 16 | /// 17 | /// Get value assosciated with key from SharePoint search response cell data model. 18 | /// 19 | /// Collection of data from SharePoint search response cell data model. 20 | /// String key that is used to get value associated with. 21 | /// Value that matches given key in provided collection of SharePoint search response cell data. 22 | public static string GetCellsValue(this IEnumerable cells, string key) 23 | { 24 | return cells.Where(item => item.Key == key).Select(item => item.Value).FirstOrDefault(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Common/GraphApiHelper.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Common 6 | { 7 | using System.Net.Http; 8 | using System.Net.Http.Headers; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using Microsoft.Extensions.Logging; 12 | using Microsoft.Teams.Apps.ExpertFinder.Common.Interfaces; 13 | using Microsoft.Teams.Apps.ExpertFinder.Models; 14 | using Newtonsoft.Json; 15 | 16 | /// 17 | /// The class that represent the helper methods to access Microsoft Graph API. 18 | /// 19 | public class GraphApiHelper : IGraphApiHelper 20 | { 21 | /// 22 | /// Post user details to API request url. 23 | /// 24 | private const string UserProfileGraphEndpointUrl = "https://graph.microsoft.com/v1.0/me"; 25 | 26 | /// 27 | /// Provides a base class for sending HTTP requests and receiving HTTP responses from a resource identified by a URI. 28 | /// 29 | private readonly HttpClient client; 30 | 31 | /// 32 | /// Instance to send logs to the Application Insights service.. 33 | /// 34 | private readonly ILogger logger; 35 | 36 | /// 37 | /// Initializes a new instance of the class. 38 | /// 39 | /// Provides a base class for sending HTTP requests and receiving HTTP responses from a resource identified by a URI. 40 | /// Instance to send logs to the Application Insights service. 41 | public GraphApiHelper(HttpClient client, ILogger logger) 42 | { 43 | this.client = client; 44 | this.logger = logger; 45 | } 46 | 47 | /// 48 | public async Task UpdateUserProfileDetailsAsync(string token, string body) 49 | { 50 | using (var request = new HttpRequestMessage(HttpMethod.Patch, UserProfileGraphEndpointUrl)) 51 | { 52 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 53 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); 54 | request.Content = new StringContent(body, Encoding.UTF8, "application/json"); 55 | 56 | using (var userProfileUpdateResponse = await this.client.SendAsync(request).ConfigureAwait(false)) 57 | { 58 | if (userProfileUpdateResponse.IsSuccessStatusCode) 59 | { 60 | return true; 61 | } 62 | 63 | var errorMessage = await userProfileUpdateResponse.Content.ReadAsStringAsync().ConfigureAwait(false); 64 | this.logger.LogInformation($"Graph API user profile update error: {errorMessage}"); 65 | 66 | return false; 67 | } 68 | } 69 | } 70 | 71 | /// 72 | public async Task GetUserProfileAsync(string token) 73 | { 74 | using (var request = new HttpRequestMessage(HttpMethod.Get, $"{UserProfileGraphEndpointUrl}?$select=id,displayname,jobTitle,aboutme,skills,interests,schools")) 75 | { 76 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); 77 | 78 | using (var response = await this.client.SendAsync(request).ConfigureAwait(false)) 79 | { 80 | if (response.IsSuccessStatusCode) 81 | { 82 | var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 83 | return JsonConvert.DeserializeObject(json); 84 | } 85 | 86 | var errorMessage = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 87 | this.logger.LogInformation($"Error getting user profile from Graph: {errorMessage}"); 88 | 89 | return null; 90 | } 91 | } 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Common/Interfaces/ICustomTokenHelper.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Common 6 | { 7 | using System.Collections.Generic; 8 | using System.Security.Claims; 9 | 10 | /// 11 | /// Helper class for JWT token generation and validation for given resource, e.g. SharePoint. 12 | /// 13 | public interface ICustomTokenHelper 14 | { 15 | /// 16 | /// Generate custom jwt access token to authenticate/verify valid request on API side. 17 | /// 18 | /// User account's object id within Azure Active Directory. 19 | /// Service uri where responses to this activity should be sent. 20 | /// Unique user id from activity. 21 | /// Expiry of token. 22 | /// Custom jwt access token. 23 | string GenerateAPIAuthToken(string aadObjectId, string serviceURL, string fromId, int jwtExpiryMinutes); 24 | } 25 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Common/Interfaces/IGraphApiHelper.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Common.Interfaces 6 | { 7 | using System.Threading.Tasks; 8 | using Microsoft.Teams.Apps.ExpertFinder.Models; 9 | 10 | /// 11 | /// Provides the helper methods to access Microsoft Graph API. 12 | /// 13 | public interface IGraphApiHelper 14 | { 15 | /// 16 | /// Get user profile details from Microsoft Graph. 17 | /// 18 | /// Microsoft Graph user access token. 19 | /// User profile details. 20 | Task GetUserProfileAsync(string token); 21 | 22 | /// 23 | /// Call Microsoft Graph API to update user profile details. 24 | /// 25 | /// Microsoft Graph API user access token. 26 | /// User profile. 27 | /// A task that returns true if user profile is successfully updated and false if it fails. 28 | /// Reference link for Graph API used for updating user profile is"https://docs.microsoft.com/en-us/graph/api/user-update?view=graph-rest-beta&tabs=http". 29 | Task UpdateUserProfileDetailsAsync(string token, string body); 30 | } 31 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Common/Interfaces/ISharePointApiHelper.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Common.Interfaces 6 | { 7 | using System.Collections.Generic; 8 | using System.Threading.Tasks; 9 | using Microsoft.Teams.Apps.ExpertFinder.Models.SharePoint; 10 | 11 | /// 12 | /// Handles API calls for SharePoint to get user details based on query. 13 | /// 14 | public interface ISharePointApiHelper 15 | { 16 | /// 17 | /// Get user profiles from SharePoint based on search text and filters. 18 | /// 19 | /// Search text to match. 20 | /// List of property filters to perform serch on. 21 | /// SharePoint user access token. 22 | /// SharePoint base uri. 23 | /// User profile collection that matches search query. 24 | Task> GetUserProfilesAsync(string searchText, IList searchFilters, string token, string resourceBaseUrl); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Common/Interfaces/ITokenHelper.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Common.Interfaces 6 | { 7 | using System.Threading.Tasks; 8 | 9 | /// 10 | /// Helper class to generate Azure Active Directory user access token for given resource, e.g. Microsoft Graph. 11 | /// 12 | public interface ITokenHelper 13 | { 14 | /// 15 | /// Get user access token for given resource using Bot OAuth client instance. 16 | /// 17 | /// Activity from id. 18 | /// Resource url for which token will be acquired. 19 | /// A task that represents security access token for given resource. 20 | Task GetUserTokenAsync(string fromId, string resourceUrl); 21 | } 22 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Common/Interfaces/IUserProfileActivityStorageHelper.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Common.Interfaces 6 | { 7 | using System.Threading.Tasks; 8 | using Microsoft.Teams.Apps.ExpertFinder.Models; 9 | 10 | /// 11 | /// Implements storage helper which stores user profile card activity details in Microsoft Azure Table service. 12 | /// 13 | public interface IUserProfileActivityStorageHelper 14 | { 15 | /// 16 | /// Stores or update user profile card activity id and user profile card id in table storage. 17 | /// 18 | /// Holds user profile activity id and card id to uniquely identify user activity that is being edited. 19 | /// A of type bool where true represents user profile activity information is saved or updated.False indicates failure in saving data. 20 | Task UpsertUserProfileConversationDataAsync(UserProfileActivityInfo userProfileConversatioEntity); 21 | 22 | /// 23 | /// Get user profile card activity id and user profile card id from table storage based on user profile card id. 24 | /// 25 | /// Unique user profile card id. 26 | /// A task that represent object to hold user profile card activity id and user profile card id. 27 | Task GetUserProfileConversationDataAsync(string myProfileCardId); 28 | } 29 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Common/SharePointApiHelper.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Common 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Net.Http; 11 | using System.Net.Http.Headers; 12 | using System.Text; 13 | using System.Threading.Tasks; 14 | using Microsoft.Teams.Apps.ExpertFinder.Common.Extensions; 15 | using Microsoft.Teams.Apps.ExpertFinder.Common.Interfaces; 16 | using Microsoft.Teams.Apps.ExpertFinder.Models.SharePoint; 17 | using Newtonsoft.Json; 18 | using Newtonsoft.Json.Linq; 19 | 20 | /// 21 | /// Handles API calls for SharePoint to get user details based on query. 22 | /// 23 | public class SharePointApiHelper : ISharePointApiHelper 24 | { 25 | /// 26 | /// Default SharePoint search filter criteria. 27 | /// 28 | private const string DefaultSearchType = "skills"; 29 | 30 | /// 31 | /// SharePoint constant source id for user profile search. 32 | /// 33 | private const string SharePointSearchSourceId = "B09A7990-05EA-4AF9-81EF-EDFAB16C4E31"; 34 | 35 | /// 36 | /// Provides a base class for sending HTTP requests and receiving HTTP responses from a resource identified by a URI. 37 | /// 38 | private readonly HttpClient client; 39 | 40 | /// 41 | /// Initializes a new instance of the class. 42 | /// 43 | /// Provides a base class for sending HTTP requests and receiving HTTP responses from a resource identified by a URI. 44 | public SharePointApiHelper(HttpClient client) 45 | { 46 | this.client = client; 47 | } 48 | 49 | /// 50 | public async Task> GetUserProfilesAsync(string searchText, IList searchFilters, string token, string resourceBaseUrl) 51 | { 52 | using (var request = new HttpRequestMessage(HttpMethod.Get, this.GetSharePointSearchRequestUri(searchText, searchFilters, resourceBaseUrl))) 53 | { 54 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 55 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); 56 | 57 | using (var response = await this.client.SendAsync(request).ConfigureAwait(false)) 58 | { 59 | if (response.IsSuccessStatusCode) 60 | { 61 | var result = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 62 | var searchResult = JsonConvert.DeserializeObject(JObject.Parse(result).SelectToken("PrimaryQueryResult").ToString()); 63 | var searchResultRows = searchResult.RelevantResults.Table.Rows; 64 | 65 | return searchResultRows.Select(user => new UserProfileDetail() 66 | { 67 | AboutMe = user.Cells.GetCellsValue("AboutMe"), 68 | Interests = user.Cells.GetCellsValue("Interests"), 69 | JobTitle = user.Cells.GetCellsValue("JobTitle"), 70 | PreferredName = user.Cells.GetCellsValue("PreferredName"), 71 | Schools = user.Cells.GetCellsValue("Schools"), 72 | Skills = user.Cells.GetCellsValue("Skills"), 73 | WorkEmail = user.Cells.GetCellsValue("WorkEmail"), 74 | Path = user.Cells.GetCellsValue("OriginalPath"), 75 | }).ToList(); 76 | } 77 | else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) 78 | { 79 | var errorMessage = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 80 | throw new UnauthorizedAccessException($"Error getting user profiles: {errorMessage}"); 81 | } 82 | else 83 | { 84 | var errorMessage = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 85 | throw new Exception($"Error getting user profiles ({response.ReasonPhrase}): {errorMessage}"); 86 | } 87 | } 88 | } 89 | } 90 | 91 | /// 92 | /// Generate SharePoint search query REST API uri. 93 | /// 94 | /// Search text to match. 95 | /// List of property filters to perform serch on. 96 | /// SharePoint base uri. 97 | /// SharePoint search query REST API uri. 98 | /// Returned url will be like "https://{SharepointSteName}.sharepoint.com/_api/search/query?querytext='{SearchQuery}'&sourceid=B09A7990-05EA-4AF9-81EF-EDFAB16C4E31". 99 | private string GetSharePointSearchRequestUri(string searchText, IList searchFilters, string baseUri) 100 | { 101 | StringBuilder searchString = new StringBuilder(); 102 | 103 | if (searchFilters != null && searchFilters.Count > 0) 104 | { 105 | if (searchFilters.Count > 1) 106 | { 107 | var items = searchFilters.Take(searchFilters.Count - 1).ToList(); 108 | items.ForEach(value => 109 | { 110 | searchString.Append(value + ":" + searchText + " OR "); 111 | }); 112 | } 113 | 114 | searchString.Append(searchFilters.Last() + ":" + searchText); 115 | } 116 | else 117 | { 118 | searchString.Append(DefaultSearchType + ":" + searchText); 119 | } 120 | 121 | return $"{baseUri}_api/search/query?querytext='{searchString.ToString()}'&sourceid='{SharePointSearchSourceId}'"; 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Common/TokenHelper.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Common 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.IdentityModel.Tokens.Jwt; 10 | using System.Security.Claims; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | using Microsoft.Bot.Connector; 14 | using Microsoft.Extensions.Logging; 15 | using Microsoft.Extensions.Options; 16 | using Microsoft.IdentityModel.Tokens; 17 | using Microsoft.Teams.Apps.ExpertFinder.Common.Interfaces; 18 | using Microsoft.Teams.Apps.ExpertFinder.Models.Configuration; 19 | 20 | /// 21 | /// Helper class for JWT token generation, validation and generate AAD user access token for given resource, e.g. Microsoft Graph, SharePoint. 22 | /// 23 | public class TokenHelper : ITokenHelper, ICustomTokenHelper 24 | { 25 | /// 26 | /// Instance of the Microsoft Bot Connector OAuthClient class. 27 | /// 28 | private readonly OAuthClient oAuthClient; 29 | 30 | /// 31 | /// Represents a set of key/value application configuration properties. 32 | /// 33 | private readonly BotSettings botSettings; 34 | 35 | /// 36 | /// Sends logs to the Application Insights service. 37 | /// 38 | private readonly ILogger logger; 39 | 40 | /// 41 | /// Initializes a new instance of the class. 42 | /// Helps generating custom token, validating custom token and generate AADv1 user access token for given resource. 43 | /// 44 | /// Instance of the Microsoft Bot Connector OAuthClient class. 45 | /// A set of key/value application configuration properties. 46 | /// Instance to send logs to the Application Insights service. 47 | public TokenHelper(OAuthClient oAuthClient, IOptionsMonitor botSettings, ILogger logger) 48 | { 49 | this.botSettings = botSettings.CurrentValue; 50 | this.oAuthClient = oAuthClient; 51 | this.logger = logger; 52 | } 53 | 54 | /// 55 | public string GenerateAPIAuthToken(string aadObjectId, string serviceURL, string fromId, int jwtExpiryMinutes) 56 | { 57 | SymmetricSecurityKey signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(this.botSettings.TokenSigningKey)); 58 | SigningCredentials signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); 59 | 60 | SecurityTokenDescriptor securityTokenDescriptor = new SecurityTokenDescriptor() 61 | { 62 | Subject = new ClaimsIdentity( 63 | new List() 64 | { 65 | new Claim("aadObjectId", aadObjectId), 66 | new Claim("serviceURL", serviceURL), 67 | new Claim("fromId", fromId), 68 | }, "Custom"), 69 | NotBefore = DateTime.UtcNow, 70 | SigningCredentials = signingCredentials, 71 | Issuer = this.botSettings.AppBaseUri, 72 | Audience = this.botSettings.AppBaseUri, 73 | IssuedAt = DateTime.UtcNow, 74 | Expires = DateTime.UtcNow.AddMinutes(jwtExpiryMinutes), 75 | }; 76 | 77 | JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler(); 78 | SecurityToken token = tokenHandler.CreateToken(securityTokenDescriptor); 79 | 80 | return tokenHandler.WriteToken(token); 81 | } 82 | 83 | /// 84 | public async Task GetUserTokenAsync(string fromId, string resourceUrl) 85 | { 86 | try 87 | { 88 | var token = await this.oAuthClient.UserToken.GetAadTokensAsync(fromId, this.botSettings.OAuthConnectionName, new Bot.Schema.AadResourceUrls { ResourceUrls = new string[] { resourceUrl } }).ConfigureAwait(false); 89 | return token?[resourceUrl]?.Token; 90 | } 91 | catch (Exception ex) 92 | { 93 | this.logger.LogError(ex, "Failed to get user AAD access token for given resource using bot OAuthClient instance."); 94 | return null; 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Common/UserProfileActivityStorageHelper.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Common 6 | { 7 | using System; 8 | using System.Net; 9 | using System.Threading.Tasks; 10 | using Microsoft.Extensions.Options; 11 | using Microsoft.Teams.Apps.ExpertFinder.Common.Interfaces; 12 | using Microsoft.Teams.Apps.ExpertFinder.Models; 13 | using Microsoft.Teams.Apps.ExpertFinder.Models.Configuration; 14 | using Microsoft.WindowsAzure.Storage; 15 | using Microsoft.WindowsAzure.Storage.Table; 16 | 17 | /// 18 | /// Implements storage helper which stores user profile card activity details in Microsoft Azure Table service. 19 | /// 20 | public class UserProfileActivityStorageHelper : IUserProfileActivityStorageHelper 21 | { 22 | /// 23 | /// Task for initialization. 24 | /// 25 | private readonly Lazy initializeTask; 26 | 27 | /// 28 | /// Microsoft Azure Table Storage connection string. 29 | /// 30 | private readonly string connectionString; 31 | 32 | /// 33 | /// Microsoft Azure Table Storage table name. 34 | /// 35 | private readonly string tableName; 36 | 37 | /// 38 | /// Represents a table in the Microsoft Azure Table service. 39 | /// 40 | private CloudTable profileCloudTable; 41 | 42 | /// 43 | /// Initializes a new instance of the class. 44 | /// 45 | /// A set of key/value application configuration properties. 46 | public UserProfileActivityStorageHelper(IOptionsMonitor botSettings) 47 | { 48 | this.initializeTask = new Lazy(() => this.InitializeAsync()); 49 | this.connectionString = botSettings.CurrentValue.StorageConnectionString; 50 | this.tableName = "UserProfileActivityInfo"; 51 | } 52 | 53 | /// 54 | public async Task UpsertUserProfileConversationDataAsync(UserProfileActivityInfo userProfileConversationEntity) 55 | { 56 | var result = await this.StoreOrUpdateEntityAsync(userProfileConversationEntity).ConfigureAwait(false); 57 | return result.HttpStatusCode == (int)HttpStatusCode.NoContent; 58 | } 59 | 60 | /// 61 | public async Task GetUserProfileConversationDataAsync(string myProfileCardId) 62 | { 63 | TableResult searchResult; 64 | await this.EnsureInitializedAsync().ConfigureAwait(false); 65 | var searchOperation = TableOperation.Retrieve(UserProfileActivityInfo.UserProfileActivityInfoPartitionKey, myProfileCardId); 66 | searchResult = await this.profileCloudTable.ExecuteAsync(searchOperation).ConfigureAwait(false); 67 | return (UserProfileActivityInfo)searchResult.Result; 68 | } 69 | 70 | /// 71 | /// Store or update user profile activity information entity which holds user profile card activity id and user profile card id in table storage. 72 | /// 73 | /// Object that contains user profile card activity id and user profile card unique id. 74 | /// A task that represents configuration entity is saved or updated. 75 | private async Task StoreOrUpdateEntityAsync(UserProfileActivityInfo entity) 76 | { 77 | await this.EnsureInitializedAsync().ConfigureAwait(false); 78 | TableOperation addOrUpdateOperation = TableOperation.InsertOrReplace(entity); 79 | return await this.profileCloudTable.ExecuteAsync(addOrUpdateOperation).ConfigureAwait(false); 80 | } 81 | 82 | /// 83 | /// Create UserProfile table if it doesnt exists. 84 | /// 85 | /// A representing the asynchronous operation task which represents table is created if its not exists. 86 | private async Task InitializeAsync() 87 | { 88 | CloudStorageAccount storageAccount = CloudStorageAccount.Parse(this.connectionString); 89 | CloudTableClient cloudTableClient = storageAccount.CreateCloudTableClient(); 90 | this.profileCloudTable = cloudTableClient.GetTableReference(this.tableName); 91 | await this.profileCloudTable.CreateIfNotExistsAsync().ConfigureAwait(false); 92 | } 93 | 94 | /// 95 | /// Ensures .Microsoft Azure Table Storage should be created before working on table. 96 | /// 97 | /// Represents an asynchronous operation. 98 | private async Task EnsureInitializedAsync() 99 | { 100 | await this.initializeTask.Value.ConfigureAwait(false); 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Controllers/BotController.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Controllers 6 | { 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Bot.Builder; 10 | using Microsoft.Bot.Builder.Integration.AspNet.Core; 11 | 12 | /// 13 | /// This ASP Controller is created to handle a request. Dependency Injection will provide the Adapter and IBot implementation at runtime. 14 | /// Multiple different IBot implementations running at different endpoints can be 15 | /// achieved by specifying a more specific type for the bot constructor argument. 16 | /// 17 | [Route("api/messages")] 18 | [ApiController] 19 | public class BotController : ControllerBase 20 | { 21 | /// 22 | /// Bot adapter. 23 | /// 24 | private readonly IBotFrameworkHttpAdapter adapter; 25 | 26 | /// 27 | /// Bot. 28 | /// 29 | private readonly IBot bot; 30 | 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// Dependency Injection will provide the Adapter and IBot implementation at runtime. 34 | /// 35 | /// Expert Finder Bot Adapter instance. 36 | /// Expert Finder Bot instance. 37 | public BotController(IBotFrameworkHttpAdapter adapter, IBot bot) 38 | { 39 | this.adapter = adapter; 40 | this.bot = bot; 41 | } 42 | 43 | /// 44 | /// POST: api/Messages 45 | /// Delegate the processing of the HTTP POST to the adapter. 46 | /// The adapter will invoke the bot. 47 | /// 48 | /// A task that represents the work queued to execute. 49 | [HttpPost] 50 | public async Task PostAsync() 51 | { 52 | // Delegate the processing of the HTTP POST to the adapter. 53 | // The adapter will invoke the bot. 54 | await this.adapter.ProcessAsync(this.Request, this.Response, this.bot).ConfigureAwait(false); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Controllers/ResourceController.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Controllers 6 | { 7 | using System; 8 | using System.Globalization; 9 | using Microsoft.AspNetCore.Authorization; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.AspNetCore.Mvc; 12 | using Microsoft.Extensions.Localization; 13 | using Microsoft.Extensions.Logging; 14 | using Microsoft.Teams.Apps.ExpertFinder.Resources; 15 | 16 | /// 17 | /// Controller to handle strings. 18 | /// 19 | [Route("api/resource")] 20 | [ApiController] 21 | [Authorize] 22 | public class ResourceController : ControllerBase 23 | { 24 | /// 25 | /// Sends logs to the Application Insights service. 26 | /// 27 | private readonly ILogger logger; 28 | 29 | /// 30 | /// The current cultures' string localizer. 31 | /// 32 | private readonly IStringLocalizer localizer; 33 | 34 | /// 35 | /// Initializes a new instance of the class. 36 | /// 37 | /// Instance to send logs to the Application Insights service. 38 | /// The current cultures' string localizer. 39 | public ResourceController(ILogger logger, IStringLocalizer localizer) 40 | { 41 | this.logger = logger; 42 | this.localizer = localizer; 43 | } 44 | 45 | /// 46 | /// Get resource strings for displaying in client app. 47 | /// 48 | /// Object containing required strings to be used in client app. 49 | [HttpGet] 50 | [Route("/api/resources/strings")] 51 | public ActionResult GetResourceStrings() 52 | { 53 | try 54 | { 55 | var strings = new 56 | { 57 | Strings.SearchTextBoxPlaceholder, 58 | Strings.InitialSearchResultMessageBodyText, 59 | Strings.InitialSearchResultMessageHeaderText, 60 | Strings.SearchResultNoItemsText, 61 | Strings.SkillsTitle, 62 | Strings.InterestTitle, 63 | Strings.SchoolsTitle, 64 | Strings.ViewButtonText, 65 | Strings.MaxUserProfilesError, 66 | Strings.UnauthorizedErrorMessage, 67 | Strings.ForbiddenErrorMessage, 68 | Strings.GeneralErrorMessage, 69 | Strings.RefreshLinkText, 70 | }; 71 | return this.Ok(strings); 72 | } 73 | catch (Exception ex) 74 | { 75 | this.logger.LogError(ex, "Error while getting strings from resource controller."); 76 | return this.StatusCode(StatusCodes.Status500InternalServerError, ex.Message); 77 | } 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Controllers/UserProfileController.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Controllers 6 | { 7 | using System; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | using Microsoft.AspNetCore.Authorization; 11 | using Microsoft.AspNetCore.Http; 12 | using Microsoft.AspNetCore.Mvc; 13 | using Microsoft.Extensions.Logging; 14 | using Microsoft.Extensions.Options; 15 | using Microsoft.Teams.Apps.ExpertFinder.Common.Interfaces; 16 | using Microsoft.Teams.Apps.ExpertFinder.Models.Configuration; 17 | using Microsoft.Teams.Apps.ExpertFinder.Models.SharePoint; 18 | 19 | /// 20 | /// Controller to handle SharePoint API operations. 21 | /// 22 | [Route("api/users")] 23 | [ApiController] 24 | [Authorize] 25 | public class UserProfileController : ControllerBase 26 | { 27 | /// 28 | /// Helper for acquiring AAD token for given resource. 29 | /// 30 | private readonly ITokenHelper tokenHelper; 31 | 32 | /// 33 | /// Instance of SharePoint search REST API helper. 34 | /// 35 | private readonly ISharePointApiHelper sharePointApiHelper; 36 | 37 | /// 38 | /// SharePoint site uri. 39 | /// 40 | private readonly string sharePointSiteUri; 41 | 42 | /// 43 | /// Sends logs to the Application Insights service. 44 | /// 45 | private readonly ILogger logger; 46 | 47 | /// 48 | /// Initializes a new instance of the class. 49 | /// 50 | /// Instance of SharePoint search REST API helper. 51 | /// Instance of class for validating custom jwt access token. 52 | /// A set of key/value application configuration properties. 53 | /// Instance to send logs to the Application Insights service. 54 | public UserProfileController(ISharePointApiHelper sharePointApiHelper, ITokenHelper tokenHelper, IOptionsMonitor botSettings, ILogger logger) 55 | { 56 | this.sharePointApiHelper = sharePointApiHelper; 57 | this.tokenHelper = tokenHelper; 58 | this.sharePointSiteUri = botSettings.CurrentValue.SharePointSiteUrl; 59 | this.logger = logger; 60 | } 61 | 62 | /// 63 | /// Post call to search service. 64 | /// 65 | /// User search query which includes search text and search filters. 66 | /// List of user profile details which matches search text for properties given by search filters. 67 | public async Task Post(UserSearch searchQuery) 68 | { 69 | try 70 | { 71 | var jwtToken = this.Request.Headers["Authorization"].ToString().Split(' ')[1]; 72 | 73 | if (searchQuery == null) 74 | { 75 | return this.StatusCode(StatusCodes.Status403Forbidden); 76 | } 77 | 78 | var fromId = this.User.Claims.Where(claim => claim.Type == "fromId").Select(claim => claim.Value).FirstOrDefault(); 79 | if (string.IsNullOrEmpty(fromId)) 80 | { 81 | this.logger.LogInformation("Failed to get fromId from token."); 82 | return this.StatusCode(StatusCodes.Status401Unauthorized); 83 | } 84 | 85 | var userToken = await this.tokenHelper.GetUserTokenAsync(fromId, this.sharePointSiteUri).ConfigureAwait(false); 86 | this.logger.LogInformation("Initiated call to user search service"); 87 | var userProfiles = await this.sharePointApiHelper.GetUserProfilesAsync(searchQuery.SearchText, searchQuery.SearchFilters, userToken, this.sharePointSiteUri).ConfigureAwait(false); 88 | 89 | this.logger.LogInformation("Call to search service succeeded"); 90 | return this.Ok(userProfiles); 91 | } 92 | catch (UnauthorizedAccessException ex) 93 | { 94 | this.logger.LogError(ex, "Failed to get user token to make post call to api."); 95 | return this.StatusCode(StatusCodes.Status401Unauthorized); 96 | } 97 | catch (Exception ex) 98 | { 99 | this.logger.LogError(ex, "Error while making post call to search service."); 100 | return this.BadRequest(ex.Message); 101 | } 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Dialogs/LogoutDialog.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Dialogs 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Microsoft.Bot.Builder; 12 | using Microsoft.Bot.Builder.Dialogs; 13 | using Microsoft.Bot.Schema; 14 | using Microsoft.Teams.Apps.ExpertFinder.Resources; 15 | 16 | /// 17 | /// Dialog for handling interruption. 18 | /// 19 | public class LogoutDialog : ComponentDialog 20 | { 21 | /// 22 | /// Text that triggers logout action. 23 | /// 24 | private static readonly ISet LogoutCommands = new HashSet { "LOGOUT", "SIGNOUT", "LOG OUT", "SIGN OUT" }; 25 | 26 | /// 27 | /// Bot OAuth connection name. 28 | /// 29 | private readonly string connectionName; 30 | 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// 34 | /// Dialog Id. 35 | /// AADv1 connection name. 36 | public LogoutDialog(string id, string connectionName) 37 | : base(id) 38 | { 39 | this.connectionName = connectionName; 40 | } 41 | 42 | /// 43 | /// Called when the dialog is started and pushed onto the parent's dialog stack. 44 | /// 45 | /// The inner Microsoft.Bot.Builder.Dialogs.DialogContext for the current turn of conversation. 46 | /// Optional, initial information to pass to the dialog. 47 | /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. 48 | /// A task representing the asynchronous operation. 49 | protected override async Task OnBeginDialogAsync(DialogContext dialogContext, object options, CancellationToken cancellationToken = default) 50 | { 51 | var result = await this.InterruptAsync(dialogContext, cancellationToken).ConfigureAwait(false); 52 | if (result != null) 53 | { 54 | return result; 55 | } 56 | 57 | return await base.OnBeginDialogAsync(dialogContext, options, cancellationToken).ConfigureAwait(false); 58 | } 59 | 60 | /// 61 | /// Called when the dialog is _continued_, where it is the active dialog and the user replies with a new activity. 62 | /// 63 | /// The inner Microsoft.Bot.Builder.Dialogs.DialogContext for the current turn of conversation. 64 | /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. 65 | /// A task representing the asynchronous operation. 66 | protected override async Task OnContinueDialogAsync(DialogContext dialogContext, CancellationToken cancellationToken = default) 67 | { 68 | var result = await this.InterruptAsync(dialogContext, cancellationToken).ConfigureAwait(false); 69 | if (result != null) 70 | { 71 | return result; 72 | } 73 | 74 | return await base.OnContinueDialogAsync(dialogContext, cancellationToken).ConfigureAwait(false); 75 | } 76 | 77 | /// 78 | /// Handling interruption. 79 | /// 80 | /// The inner Microsoft.Bot.Builder.Dialogs.DialogContext for the current turn of conversation. 81 | /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. 82 | /// A task representing the asynchronous operation. 83 | private async Task InterruptAsync(DialogContext dialogContext, CancellationToken cancellationToken = default) 84 | { 85 | if (dialogContext.Context.Activity.Type != ActivityTypes.Message) 86 | { 87 | return null; 88 | } 89 | 90 | var text = dialogContext.Context.Activity.Text; 91 | if (string.IsNullOrEmpty(text)) 92 | { 93 | return null; 94 | } 95 | 96 | if (LogoutCommands.Contains(text.ToUpperInvariant().Trim()) || text.Trim().Equals(Strings.BotCommandLogout, StringComparison.CurrentCultureIgnoreCase)) 97 | { 98 | // The bot adapter encapsulates the authentication processes. 99 | var botAdapter = (BotFrameworkAdapter)dialogContext.Context.Adapter; 100 | await botAdapter.SignOutUserAsync(dialogContext.Context, this.connectionName, null, cancellationToken).ConfigureAwait(false); 101 | await dialogContext.Context.SendActivityAsync(MessageFactory.Text(Strings.SignOutText), cancellationToken).ConfigureAwait(false); 102 | return await dialogContext.CancelAllDialogsAsync(cancellationToken).ConfigureAwait(false); 103 | } 104 | 105 | return null; 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Microsoft.Teams.Apps.ExpertFinder.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | latest 6 | ClientApp\ 7 | true 8 | Latest 9 | true 10 | 11 | 12 | 13 | bin\Debug\Microsoft.Teams.Apps.ExpertFinder.xml 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | all 32 | runtime; build; native; contentfiles; analyzers; buildtransitive 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | <_ContentIncludedByDefault Remove="stylecop.json" /> 46 | 47 | 48 | 49 | 50 | 51 | Always 52 | 53 | 54 | Always 55 | 56 | 57 | 58 | 59 | 60 | True 61 | True 62 | Strings.resx 63 | 64 | 65 | 66 | 67 | 68 | Always 69 | 70 | 71 | 72 | 73 | 74 | PublicResXFileCodeGenerator 75 | Strings.Designer.cs 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | %(DistFiles.Identity) 102 | PreserveNewest 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/AdaptiveCardAction.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models 6 | { 7 | using Microsoft.Bot.Schema; 8 | using Newtonsoft.Json; 9 | 10 | /// 11 | /// Adaptive card action model class. 12 | /// 13 | public class AdaptiveCardAction 14 | { 15 | /// 16 | /// Gets or sets Msteams card action type. 17 | /// 18 | [JsonProperty("msteams")] 19 | public CardAction MsteamsCardAction { get; set; } 20 | 21 | /// 22 | /// Gets or sets commands from which task module is invoked. 23 | /// 24 | [JsonProperty("command")] 25 | public string Command { get; set; } 26 | 27 | /// 28 | /// Gets or sets my profile card unique guid. 29 | /// 30 | [JsonProperty("MyProfileCardId")] 31 | public string MyProfileCardId { get; set; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/Configuration/AADSettings.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models.Configuration 6 | { 7 | /// 8 | /// Provides app setting related to AAD bot connection. 9 | /// 10 | public class AADSettings 11 | { 12 | /// 13 | /// Gets or sets AADv1 bot connection name. 14 | /// 15 | public string ConnectionName { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/Configuration/BotSettings.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models.Configuration 6 | { 7 | /// 8 | /// Provides app settings related to Expert Finder bot. 9 | /// 10 | public class BotSettings 11 | { 12 | /// 13 | /// Gets or sets application base uri. 14 | /// 15 | public string AppBaseUri { get; set; } 16 | 17 | /// 18 | /// Gets or sets application Insights instrumentation key which we passes to client application. 19 | /// 20 | public string AppInsightsInstrumentationKey { get; set; } 21 | 22 | /// 23 | /// Gets or sets bot OAuth connection name. 24 | /// 25 | public string OAuthConnectionName { get; set; } 26 | 27 | /// 28 | /// Gets or sets a random key used to sign the JWT sent to the task module. 29 | /// 30 | public string TokenSigningKey { get; set; } 31 | 32 | /// 33 | /// Gets or sets SharePoint site Uri. 34 | /// 35 | public string SharePointSiteUrl { get; set; } 36 | 37 | /// 38 | /// Gets or sets Azure Table Storage connection string. 39 | /// 40 | public string StorageConnectionString { get; set; } 41 | 42 | /// 43 | /// Gets or sets tenant id. 44 | /// 45 | public string TenantId { get; set; } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/Configuration/SharePointSettings.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models.Configuration 6 | { 7 | /// 8 | /// Provides app settings related to SharePoint. 9 | /// 10 | public class SharePointSettings 11 | { 12 | /// 13 | /// Gets SharePoint search rest api uri. 14 | /// 15 | public string SharePointSiteUrl { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/Configuration/StorageSettings.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models.Configuration 6 | { 7 | /// 8 | /// Provides app setting related to Azure Table Storage. 9 | /// 10 | public class StorageSettings 11 | { 12 | /// 13 | /// Gets or sets Azure Table Storage connection string. 14 | /// 15 | public string StorageConnectionString { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/Configuration/TokenSettings.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models.Configuration 6 | { 7 | /// 8 | /// Provides app setting related to jwt token. 9 | /// 10 | public class TokenSettings : AADSettings 11 | { 12 | /// 13 | /// Gets application base uri. 14 | /// 15 | public string AppBaseUri { get; set; } 16 | 17 | /// 18 | /// Gets random key to create jwt security key. 19 | /// 20 | public string SecurityKey { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/ConversationData.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models 6 | { 7 | /// 8 | /// User conversation state model class. 9 | /// 10 | public class ConversationData 11 | { 12 | /// 13 | /// Gets or sets a value indicating whether welcome card sent to user. 14 | /// 15 | /// Value is null when bot is installed for first time. 16 | public bool? IsWelcomeCardSent { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/EditProfileCardAction.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models 6 | { 7 | using Microsoft.Bot.Schema; 8 | using Newtonsoft.Json; 9 | 10 | /// 11 | /// Edit profile task module model class. 12 | /// 13 | public class EditProfileCardAction 14 | { 15 | /// 16 | /// Gets or sets msteams card action type. 17 | /// 18 | [JsonProperty("msteams")] 19 | public CardAction Msteams { get; set; } 20 | 21 | /// 22 | /// Gets or sets bot command name. 23 | /// 24 | [JsonProperty("command")] 25 | public string Command { get; set; } 26 | 27 | /// 28 | /// Gets or sets user profile card unique id. 29 | /// 30 | [JsonProperty("MyProfileCardId")] 31 | public string MyProfileCardId { get; set; } 32 | 33 | /// 34 | /// Gets or sets user about me details. 35 | /// 36 | [JsonProperty("aboutMe")] 37 | public string AboutMe { get; set; } 38 | 39 | /// 40 | /// Gets or sets user interest details. 41 | /// 42 | [JsonProperty("interests")] 43 | public string Interests { get; set; } 44 | 45 | /// 46 | /// Gets or sets user school details. 47 | /// 48 | [JsonProperty("schools")] 49 | public string Schools { get; set; } 50 | 51 | /// 52 | /// Gets or sets user skill details. 53 | /// 54 | [JsonProperty("skills")] 55 | public string Skills { get; set; } 56 | } 57 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/SearchSubmitAction.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models 6 | { 7 | using System.Collections.Generic; 8 | using Microsoft.Teams.Apps.ExpertFinder.Models.SharePoint; 9 | using Newtonsoft.Json; 10 | 11 | /// 12 | /// Submit action on view of search task module model class. 13 | /// 14 | public class SearchSubmitAction 15 | { 16 | /// 17 | /// Gets or sets commands from which task module is invoked. 18 | /// 19 | [JsonProperty("command")] 20 | public string Command { get; set; } 21 | 22 | /// 23 | /// Gets or sets user profile details from task module. 24 | /// 25 | [JsonProperty("searchresults")] 26 | public List UserProfiles { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/SharePoint/SearchPropertiesResult.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models.SharePoint 6 | { 7 | /// 8 | /// Properties result data from SharePoint search response cell data model. 9 | /// 10 | public class SearchPropertiesResult 11 | { 12 | /// 13 | /// Gets or sets key value. 14 | /// 15 | public string Key { get; set; } 16 | 17 | /// 18 | /// Gets or sets value. 19 | /// 20 | public string Value { get; set; } 21 | 22 | /// 23 | /// Gets or sets data type of value. 24 | /// 25 | public string ValueType { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/SharePoint/SearchRelevantResult.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models.SharePoint 6 | { 7 | /// 8 | /// Holds relevant result data from from SharePoint search response data model. 9 | /// 10 | public class SearchRelevantResult 11 | { 12 | /// 13 | /// Gets or sets table data from SharePoint search response data model. 14 | /// 15 | public SearchTableResult Table { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/SharePoint/SearchResponse.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models.SharePoint 6 | { 7 | /// 8 | /// SharePoint Search api response data model. 9 | /// 10 | public class SearchResponse 11 | { 12 | /// 13 | /// Gets or sets relevant results from SharePoint search response data. 14 | /// 15 | public SearchRelevantResult RelevantResults { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/SharePoint/SearchRowResult.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models.SharePoint 6 | { 7 | using System.Collections.Generic; 8 | 9 | /// 10 | /// Holds table row data from SharePoint search response data model. 11 | /// 12 | public class SearchRowResult 13 | { 14 | /// 15 | /// Gets or sets cell data from SharePoint search response table data model. 16 | /// 17 | public List Cells { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/SharePoint/SearchTableResult.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models.SharePoint 6 | { 7 | using System.Collections.Generic; 8 | 9 | /// 10 | /// Holds table data from SharePoint search response data model. 11 | /// 12 | public class SearchTableResult 13 | { 14 | /// 15 | /// Gets or sets row collection from SharePoint search response data model. 16 | /// 17 | public List Rows { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/SharePoint/UserProfileDetail.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models.SharePoint 6 | { 7 | /// 8 | /// User details model for SharePoint to show as api request response. 9 | /// 10 | public class UserProfileDetail 11 | { 12 | /// 13 | /// Gets or sets user about me. 14 | /// 15 | public string AboutMe { get; set; } 16 | 17 | /// 18 | /// Gets or sets user interest. 19 | /// 20 | public string Interests { get; set; } 21 | 22 | /// 23 | /// Gets or sets user job title. 24 | /// 25 | public string JobTitle { get; set; } 26 | 27 | /// 28 | /// Gets or sets user schools. 29 | /// 30 | public string Schools { get; set; } 31 | 32 | /// 33 | /// Gets or sets user name. 34 | /// 35 | public string PreferredName { get; set; } 36 | 37 | /// 38 | /// Gets or sets user skills. 39 | /// 40 | public string Skills { get; set; } 41 | 42 | /// 43 | /// Gets or sets user work email. 44 | /// 45 | public string WorkEmail { get; set; } 46 | 47 | /// 48 | /// Gets or sets user picture path. 49 | /// 50 | public string Path { get; set; } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/SharePoint/UserSearch.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models.SharePoint 6 | { 7 | using System.Collections.Generic; 8 | 9 | /// 10 | /// User search request model. 11 | /// 12 | public class UserSearch 13 | { 14 | /// 15 | /// Gets or sets search text. 16 | /// 17 | public string SearchText { get; set; } 18 | 19 | /// 20 | /// Gets or sets search filters selected by user. 21 | /// 22 | public List SearchFilters { get; set; } 23 | } 24 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/UserData.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models 6 | { 7 | /// 8 | /// User conversation state model class. 9 | /// 10 | public class UserData 11 | { 12 | /// 13 | /// Gets or sets a value indicating whether welcome card sent to user. 14 | /// 15 | /// Value is null when bot is installed for first time. 16 | public bool? IsWelcomeCardSent { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/UserProfileActivityInfo.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models 6 | { 7 | using Microsoft.WindowsAzure.Storage.Table; 8 | 9 | /// 10 | /// Holds user profile activity id and card id to uniquely identify user activity that is being edited. 11 | /// 12 | public class UserProfileActivityInfo : TableEntity 13 | { 14 | /// 15 | /// Partition key for UserProfile table. 16 | /// 17 | public const string UserProfileActivityInfoPartitionKey = "UserProfileActivityInfo"; 18 | 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// Holds user profile activity id and card id to uniquely identify user activity that is being edited. 22 | /// 23 | public UserProfileActivityInfo() 24 | { 25 | this.PartitionKey = UserProfileActivityInfoPartitionKey; 26 | } 27 | 28 | /// 29 | /// Gets or sets user profile card activity id. 30 | /// 31 | public string MyProfileCardActivityId { get; set; } 32 | 33 | /// 34 | /// Gets or sets custom unique guid id of user profile card. 35 | /// 36 | public string MyProfileCardId 37 | { 38 | get { return this.RowKey; } 39 | set { this.RowKey = value; } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/UserProfileDetail.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models 6 | { 7 | using Newtonsoft.Json; 8 | 9 | /// 10 | /// User profile details model class. 11 | /// 12 | public class UserProfileDetail : UserProfileDetailBase 13 | { 14 | /// 15 | /// Gets or sets odataContext. 16 | /// 17 | [JsonProperty("@odata.context")] 18 | public string OdataContext { get; set; } 19 | 20 | /// 21 | /// Gets or sets user unique id. 22 | /// 23 | [JsonProperty("id")] 24 | public string Id { get; set; } 25 | 26 | /// 27 | /// Gets or sets user display name. 28 | /// 29 | [JsonProperty("displayName")] 30 | public string DisplayName { get; set; } 31 | 32 | /// 33 | /// Gets or sets user job title. 34 | /// 35 | [JsonProperty("jobTitle")] 36 | public string JobTitle { get; set; } 37 | } 38 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Models/UserProfileDetailBase.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder.Models 6 | { 7 | using System.Collections.Generic; 8 | using Newtonsoft.Json; 9 | 10 | /// 11 | /// Model for submit action on edit profile for microsoft graph api. 12 | /// 13 | public class UserProfileDetailBase 14 | { 15 | /// 16 | /// Gets or sets user about me. 17 | /// 18 | [JsonProperty("aboutMe")] 19 | public string AboutMe { get; set; } 20 | 21 | /// 22 | /// Gets or sets skill details. 23 | /// 24 | [JsonProperty("skills")] 25 | public List Skills { get; set; } 26 | 27 | /// 28 | /// Gets or sets interest details. 29 | /// 30 | [JsonProperty("interests")] 31 | public List Interests { get; set; } 32 | 33 | /// 34 | /// Gets or sets school details. 35 | /// 36 | [JsonProperty("schools")] 37 | public List Schools { get; set; } 38 | } 39 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Program.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder 6 | { 7 | using Microsoft.AspNetCore; 8 | using Microsoft.AspNetCore.Hosting; 9 | 10 | /// 11 | /// Program class. 12 | /// 13 | public class Program 14 | { 15 | /// 16 | /// Main method. 17 | /// 18 | /// String array of arguments. 19 | public static void Main(string[] args) 20 | { 21 | CreateWebHostBuilder(args).Build().Run(); 22 | } 23 | 24 | /// 25 | /// Creates instance of web host builder. 26 | /// 27 | /// Array of arguments. 28 | /// Web host builder. 29 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 30 | WebHost.CreateDefaultBuilder(args) 31 | .UseStartup(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Source/ExpertFinder/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:50835/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Microsoft.Teams.Apps.ExpertFinder": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:50836/" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/Startup.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.ExpertFinder 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Globalization; 10 | using System.Linq; 11 | using System.Net.Http; 12 | using System.Text; 13 | using Microsoft.AspNetCore.Authentication.JwtBearer; 14 | using Microsoft.AspNetCore.Builder; 15 | using Microsoft.AspNetCore.Hosting; 16 | using Microsoft.AspNetCore.Localization; 17 | using Microsoft.AspNetCore.Mvc; 18 | using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer; 19 | using Microsoft.Bot.Builder; 20 | using Microsoft.Bot.Builder.Azure; 21 | using Microsoft.Bot.Builder.Integration.AspNet.Core; 22 | using Microsoft.Bot.Connector; 23 | using Microsoft.Bot.Connector.Authentication; 24 | using Microsoft.Extensions.Configuration; 25 | using Microsoft.Extensions.DependencyInjection; 26 | using Microsoft.IdentityModel.Tokens; 27 | using Microsoft.Teams.Apps.ExpertFinder.Bots; 28 | using Microsoft.Teams.Apps.ExpertFinder.Common; 29 | using Microsoft.Teams.Apps.ExpertFinder.Common.Interfaces; 30 | using Microsoft.Teams.Apps.ExpertFinder.Models.Configuration; 31 | using Polly; 32 | using Polly.Extensions.Http; 33 | 34 | /// 35 | /// This a Startup class for this Bot. 36 | /// 37 | public class Startup 38 | { 39 | /// 40 | /// Initializes a new instance of the class. 41 | /// 42 | /// object that passes the application configuration key-values. 43 | public Startup(IConfiguration configuration) 44 | { 45 | this.Configuration = configuration; 46 | } 47 | 48 | /// 49 | /// Gets object that passes the application configuration key-values.. 50 | /// 51 | public IConfiguration Configuration { get; } 52 | 53 | /// 54 | /// This method gets called by the runtime. Use this method to add services to the container. 55 | /// 56 | /// Service Collection Interface. 57 | public void ConfigureServices(IServiceCollection services) 58 | { 59 | services.AddHttpClient().AddPolicyHandler(GetRetryPolicy()); 60 | services.AddHttpClient().AddPolicyHandler(GetRetryPolicy()); 61 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); 62 | services.Configure(options => 63 | { 64 | options.AppBaseUri = this.Configuration["AppBaseUri"]; 65 | options.AppInsightsInstrumentationKey = this.Configuration["APPINSIGHTS_INSTRUMENTATIONKEY"]; 66 | options.OAuthConnectionName = this.GetFirstSetting("OAuthConnectionName", "ConnectionName"); 67 | options.SharePointSiteUrl = this.Configuration["SharePointSiteUrl"]; 68 | options.TenantId = this.Configuration["TenantId"]; 69 | options.TokenSigningKey = this.GetFirstSetting("TokenSigningKey", "SecurityKey"); 70 | options.StorageConnectionString = this.Configuration["StorageConnectionString"]; 71 | }); 72 | services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 73 | .AddJwtBearer(options => 74 | { 75 | options.TokenValidationParameters = new TokenValidationParameters 76 | { 77 | ValidateAudience = true, 78 | ValidAudiences = new List { this.Configuration["AppBaseUri"] }, 79 | ValidIssuers = new List { this.Configuration["AppBaseUri"] }, 80 | ValidateIssuer = true, 81 | ValidateIssuerSigningKey = true, 82 | IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(this.GetFirstSetting("TokenSigningKey", "SecurityKey"))), 83 | RequireExpirationTime = true, 84 | ValidateLifetime = true, 85 | ClockSkew = TimeSpan.FromSeconds(30), 86 | }; 87 | }); 88 | 89 | // In production, the React files will be served from this directory 90 | services.AddSpaStaticFiles(configuration => 91 | { 92 | configuration.RootPath = "ClientApp/build"; 93 | }); 94 | 95 | services.AddMemoryCache(); 96 | 97 | // Create the Bot Framework Adapter with error handling enabled. 98 | services.AddSingleton(); 99 | 100 | services.AddSingleton(new MicrosoftAppCredentials(this.Configuration["MicrosoftAppId"], this.Configuration["MicrosoftAppPassword"])); 101 | 102 | // For conversation state. 103 | services.AddSingleton(new AzureBlobStorage(this.Configuration["StorageConnectionString"], "bot-state")); 104 | 105 | // Create the Conversation state. (Used by the Dialog system itself.) 106 | services.AddSingleton(); 107 | 108 | // Create the User state. (Used in this bot's Dialog implementation.) 109 | services.AddSingleton(); 110 | 111 | // Create the telemetry middleware(used by the telemetry initializer) to track conversation events 112 | services.AddSingleton(); 113 | 114 | // The Dialog that will be run by the bot. 115 | services.AddSingleton(); 116 | 117 | services.AddSingleton(); 118 | services.AddSingleton(); 119 | services.AddSingleton(); 120 | services.AddSingleton(new OAuthClient(new MicrosoftAppCredentials(this.Configuration["MicrosoftAppId"], this.Configuration["MicrosoftAppPassword"]))); 121 | 122 | // Add i18n. 123 | services.AddLocalization(options => options.ResourcesPath = "Resources"); 124 | 125 | services.Configure(options => 126 | { 127 | var defaultCulture = CultureInfo.GetCultureInfo(this.Configuration["i18n:DefaultCulture"]); 128 | var supportedCultures = this.Configuration["i18n:SupportedCultures"].Split(',') 129 | .Select(culture => CultureInfo.GetCultureInfo(culture)) 130 | .ToList(); 131 | 132 | options.DefaultRequestCulture = new RequestCulture(defaultCulture); 133 | options.SupportedCultures = supportedCultures; 134 | options.SupportedUICultures = supportedCultures; 135 | 136 | options.RequestCultureProviders = new List 137 | { 138 | new BotLocalizationCultureProvider(), 139 | }; 140 | }); 141 | 142 | // Create the bot as a transient. In this case the ASP Controller is expecting an IBot. 143 | services.AddTransient(); 144 | services.AddApplicationInsightsTelemetry(); 145 | } 146 | 147 | /// 148 | /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 149 | /// 150 | /// Application Builder. 151 | /// Hosting Environment. 152 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 153 | { 154 | app.UseRequestLocalization(); 155 | if (env.IsDevelopment()) 156 | { 157 | app.UseDeveloperExceptionPage(); 158 | } 159 | else 160 | { 161 | app.UseHsts(); 162 | } 163 | 164 | app.UseHttpsRedirection(); 165 | app.UseAuthentication(); 166 | app.UseStaticFiles(); 167 | app.UseSpaStaticFiles(); 168 | app.UseMvc(); 169 | app.UseStaticFiles(); 170 | app.UseSpa(spa => 171 | { 172 | spa.Options.SourcePath = "ClientApp"; 173 | 174 | if (env.IsDevelopment()) 175 | { 176 | spa.UseReactDevelopmentServer(npmScript: "start"); 177 | } 178 | }); 179 | } 180 | 181 | /// 182 | /// Retry policy for for transient error cases. 183 | /// If there is no success code in response, request will be sent again for two times 184 | /// with interval of 2 and 8 seconds respectively. 185 | /// 186 | /// Policy. 187 | private static IAsyncPolicy GetRetryPolicy() 188 | { 189 | return HttpPolicyExtensions 190 | .HandleTransientHttpError() 191 | .OrResult(response => response.IsSuccessStatusCode == false) 192 | .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); 193 | } 194 | 195 | /// 196 | /// Find the first configuration value in the list that is not null or empty. 197 | /// 198 | /// List of keys to check. 199 | /// First configuration value that is not null or empty. 200 | private string GetFirstSetting(params string[] keys) 201 | { 202 | return keys.Select(key => this.Configuration[key]).Where(value => !string.IsNullOrEmpty(value)).FirstOrDefault(); 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /Source/ExpertFinder/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Source/ExpertFinder/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "MicrosoftAppId": "", 3 | "MicrosoftAppPassword": "", 4 | "APPINSIGHTS_INSTRUMENTATIONKEY": "", 5 | "StorageConnectionString": "", 6 | "OAuthConnectionName": "", 7 | "AppBaseUri": "", 8 | "SharePointSiteUrl": "", 9 | "TokenSigningKey": "", 10 | "TenantId": "", 11 | "Logging": { 12 | "LogLevel": { 13 | "Default": "Warning" 14 | }, 15 | "ApplicationInsights": { 16 | "LogLevel": { 17 | "Default": "Warning" 18 | } 19 | } 20 | }, 21 | "i18n": { 22 | "DefaultCulture": "en", 23 | "SupportedCultures": "en,ar,de,es,fr,he,ja,ko,pt-BR,ru,zh-CN,zh-TW" 24 | } 25 | } -------------------------------------------------------------------------------- /Source/ExpertFinder/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | // ACTION REQUIRED: This file was automatically added to your project, but it 3 | // will not take effect until additional steps are taken to enable it. See the 4 | // following page for additional information: 5 | // 6 | // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md 7 | 8 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 9 | "settings": { 10 | "documentationRules": { 11 | "companyName": "Microsoft" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Source/ExpertFinder/wwwroot/Artifacts/appLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-expertfinder/d75c1fc8022c31c696bf7dac069b21d58382709f/Source/ExpertFinder/wwwroot/Artifacts/appLogo.png -------------------------------------------------------------------------------- /Source/ExpertFinder/wwwroot/Artifacts/validationIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-expertfinder/d75c1fc8022c31c696bf7dac069b21d58382709f/Source/ExpertFinder/wwwroot/Artifacts/validationIcon.png -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.ExpertFinder.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28803.156 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Teams.Apps.ExpertFinder", "ExpertFinder\Microsoft.Teams.Apps.ExpertFinder.csproj", "{3C706627-EB11-49E3-9B11-67395CEF9B82}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {3C706627-EB11-49E3-9B11-67395CEF9B82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {3C706627-EB11-49E3-9B11-67395CEF9B82}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {3C706627-EB11-49E3-9B11-67395CEF9B82}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {3C706627-EB11-49E3-9B11-67395CEF9B82}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {A4DFE5C4-A20F-4D1B-89E5-DFCE229F4B20} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /deploy.bot.cmd: -------------------------------------------------------------------------------- 1 | @if "%SCM_TRACE_LEVEL%" NEQ "4" @echo off 2 | 3 | :: ---------------------- 4 | :: KUDU Deployment Script 5 | :: Version: 1.0.17 6 | :: ---------------------- 7 | 8 | :: Prerequisites 9 | :: ------------- 10 | 11 | :: Verify node.js installed 12 | where node 2>nul >nul 13 | IF %ERRORLEVEL% NEQ 0 ( 14 | echo Missing node.js executable, please install node.js, if already installed make sure it can be reached from current environment. 15 | goto error 16 | ) 17 | 18 | :: Setup 19 | :: ----- 20 | 21 | setlocal enabledelayedexpansion 22 | 23 | SET ARTIFACTS=%~dp0%..\artifacts 24 | 25 | IF NOT DEFINED DEPLOYMENT_SOURCE ( 26 | SET DEPLOYMENT_SOURCE=%~dp0%. 27 | ) 28 | 29 | IF NOT DEFINED DEPLOYMENT_TARGET ( 30 | SET DEPLOYMENT_TARGET=%ARTIFACTS%\wwwroot 31 | ) 32 | 33 | IF NOT DEFINED NEXT_MANIFEST_PATH ( 34 | SET NEXT_MANIFEST_PATH=%ARTIFACTS%\manifest 35 | 36 | IF NOT DEFINED PREVIOUS_MANIFEST_PATH ( 37 | SET PREVIOUS_MANIFEST_PATH=%ARTIFACTS%\manifest 38 | ) 39 | ) 40 | 41 | IF NOT DEFINED KUDU_SYNC_CMD ( 42 | :: Install kudu sync 43 | echo Installing Kudu Sync 44 | call npm install kudusync -g --silent 45 | IF !ERRORLEVEL! NEQ 0 goto error 46 | 47 | :: Locally just running "kuduSync" would also work 48 | SET KUDU_SYNC_CMD=%appdata%\npm\kuduSync.cmd 49 | ) 50 | IF NOT DEFINED DEPLOYMENT_TEMP ( 51 | SET DEPLOYMENT_TEMP=%temp%\___deployTemp%random% 52 | SET CLEAN_LOCAL_DEPLOYMENT_TEMP=true 53 | ) 54 | 55 | IF DEFINED CLEAN_LOCAL_DEPLOYMENT_TEMP ( 56 | IF EXIST "%DEPLOYMENT_TEMP%" rd /s /q "%DEPLOYMENT_TEMP%" 57 | mkdir "%DEPLOYMENT_TEMP%" 58 | ) 59 | 60 | IF DEFINED MSBUILD_PATH goto MsbuildPathDefined 61 | SET MSBUILD_PATH=%ProgramFiles(x86)%\MSBuild\14.0\Bin\MSBuild.exe 62 | :MsbuildPathDefined 63 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 64 | :: Deployment 65 | :: ---------- 66 | 67 | echo Handling ASP.NET Core Web Application deployment. 68 | 69 | :: 1. Restore nuget packages 70 | call :ExecuteCmd dotnet restore "%DEPLOYMENT_SOURCE%\Source\Microsoft.Teams.Apps.ExpertFinder.sln" 71 | IF !ERRORLEVEL! NEQ 0 goto error 72 | 73 | :: 2. Build and publish 74 | call :ExecuteCmd dotnet publish "%DEPLOYMENT_SOURCE%\Source\ExpertFinder\Microsoft.Teams.Apps.ExpertFinder.csproj" --output "%DEPLOYMENT_TEMP%" --configuration Release 75 | IF !ERRORLEVEL! NEQ 0 goto error 76 | 77 | :: 3. KuduSync 78 | call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 -f "%DEPLOYMENT_TEMP%" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.hg;.deployment;deploy.cmd" 79 | IF !ERRORLEVEL! NEQ 0 goto error 80 | 81 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 82 | goto end 83 | 84 | :: Execute command routine that will echo out when error 85 | :ExecuteCmd 86 | setlocal 87 | set _CMD_=%* 88 | call %_CMD_% 89 | if "%ERRORLEVEL%" NEQ "0" echo Failed exitCode=%ERRORLEVEL%, command=%_CMD_% 90 | exit /b %ERRORLEVEL% 91 | 92 | :error 93 | endlocal 94 | echo An error has occurred during web site deployment. 95 | call :exitSetErrorLevel 96 | call :exitFromFunction 2>nul 97 | 98 | :exitSetErrorLevel 99 | exit /b 1 100 | 101 | :exitFromFunction 102 | () 103 | 104 | :end 105 | endlocal 106 | echo Finished successfully. 107 | -------------------------------------------------------------------------------- /deploy.cmd: -------------------------------------------------------------------------------- 1 | @if "%SCM_TRACE_LEVEL%" NEQ "4" @echo off 2 | 3 | IF "%SITE_ROLE%" == "bot" ( 4 | deploy.bot.cmd 5 | ) ELSE ( 6 | echo You have to set SITE_ROLE setting to "bot" 7 | exit /b 1 8 | ) -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "2.1.515" 4 | } 5 | } --------------------------------------------------------------------------------