├── .deployment ├── global.json ├── Source ├── Microsoft.Teams.Apps.CrowdSourcer.AzureFunction │ ├── host.json │ ├── local.settings.json │ ├── stylecop.json │ ├── Microsoft.Teams.Apps.CrowdSourcer.AzureFunction.csproj │ ├── Startup.cs │ └── PublishFunction.cs ├── Microsoft.Teams.Apps.CrowdSourcer │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── Models │ │ ├── Details.cs │ │ └── AdaptiveSubmitActionData.cs │ ├── stylecop.json │ ├── Properties │ │ └── launchSettings.json │ ├── ConfigurationCredentialProvider.cs │ ├── Program.cs │ ├── Controllers │ │ └── BotController.cs │ ├── AdapterWithErrorHandler.cs │ ├── Microsoft.Teams.Apps.CrowdSourcer.csproj │ ├── Startup.cs │ ├── Resources │ │ ├── Strings.resx │ │ └── Strings.Designer.cs │ └── Cards │ │ └── CrowdSourcerCards.cs ├── Microsoft.Teams.Apps.CrowdSourcer.Common │ ├── stylecop.json │ ├── Providers │ │ ├── ISearchServiceDataProvider.cs │ │ ├── IConfigurationStorageProvider.cs │ │ ├── IObjectIdToNameMapper.cs │ │ ├── ISearchService.cs │ │ ├── IQnaServiceProvider.cs │ │ ├── ConfigurationStorageProvider.cs │ │ ├── ObjectIdToNameMapper.cs │ │ ├── SearchServiceDataProvider.cs │ │ ├── SearchService.cs │ │ └── QnaServiceProvider.cs │ ├── Models │ │ ├── KbConfiguration.cs │ │ ├── NameIdMapping.cs │ │ └── AzureSearchEntity.cs │ ├── Microsoft.Teams.Apps.CrowdSourcer.Common.csproj │ └── Constants.cs └── Microsoft.Teams.Apps.CrowdSourcer.sln ├── Manifest ├── color.png ├── outline.png └── manifest.json ├── deploy.cmd ├── CODE_OF_CONDUCT.md ├── LICENSE ├── SECURITY.md ├── deploy.function.cmd ├── deploy.bot.cmd ├── README.md ├── .gitignore └── Deployment └── azuredeploy.json /.deployment: -------------------------------------------------------------------------------- 1 | [config] 2 | command = deploy.cmd -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "2.1.515" 4 | } 5 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.AzureFunction/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } -------------------------------------------------------------------------------- /Manifest/color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-crowdsourcer/HEAD/Manifest/color.png -------------------------------------------------------------------------------- /Manifest/outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-crowdsourcer/HEAD/Manifest/outline.png -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /deploy.cmd: -------------------------------------------------------------------------------- 1 | @if "%SCM_TRACE_LEVEL%" NEQ "4" @echo off 2 | 3 | IF "%SITE_ROLE%" == "bot" ( 4 | deploy.bot.cmd 5 | ) ELSE ( 6 | IF "%SITE_ROLE%" == "function" ( 7 | deploy.function.cmd 8 | ) ELSE ( 9 | echo You have to set SITE_ROLE setting to either "bot" or "function" 10 | exit /b 1 11 | ) 12 | ) -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.AzureFunction/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "DefaultEndpointsProtocol", 5 | "AzureWebJobsDashboard": "", 6 | "QnAMakerSubscriptionKey": "", 7 | "QnAMakerApiUrl": "", 8 | "StorageConnectionString": "", 9 | "SearchServiceName": "", 10 | "SearchServiceKey": "" 11 | } 12 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "MicrosoftAppId": "", 3 | "MicrosoftAppPassword": "", 4 | "QnAMakerHostUrl": "", 5 | "QnAMakerApiUrl": "", 6 | "QnAMakerSubscriptionKey": "", 7 | "ScoreThreshold": "60", 8 | "StorageConnectionString": "", 9 | "ApplicationInsights": { 10 | "InstrumentationKey": "" 11 | }, 12 | "TenantId": "", 13 | "SearchServiceName": "", 14 | "SearchServiceKey": "" 15 | } 16 | 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer/Models/Details.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer.Models 6 | { 7 | /// 8 | /// Adaptive card details. 9 | /// 10 | public class Details 11 | { 12 | /// 13 | /// Gets or sets original question. 14 | /// 15 | public string Question { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer/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/Microsoft.Teams.Apps.CrowdSourcer.Common/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/Microsoft.Teams.Apps.CrowdSourcer.AzureFunction/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/Microsoft.Teams.Apps.CrowdSourcer.Common/Providers/ISearchServiceDataProvider.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer.Common.Providers 6 | { 7 | using System.Threading.Tasks; 8 | 9 | /// 10 | /// Search service data provider interface. 11 | /// 12 | public interface ISearchServiceDataProvider 13 | { 14 | /// 15 | /// This method downloads the knowledgebase and stores the json string to blob storage. 16 | /// 17 | /// knowledgebase id. 18 | /// task. 19 | Task SetupAzureSearchDataAsync(string kbId); 20 | } 21 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:3978/", 8 | "sslPort": 0 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "CoreBot": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "applicationUrl": "http://localhost:3978", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer/ConfigurationCredentialProvider.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer 6 | { 7 | using Microsoft.Bot.Connector.Authentication; 8 | using Microsoft.Extensions.Configuration; 9 | 10 | /// 11 | /// Implementation of that gets the app credentials from configuration. 12 | /// 13 | public class ConfigurationCredentialProvider : SimpleCredentialProvider 14 | { 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// Configuration. 19 | public ConfigurationCredentialProvider(IConfiguration configuration) 20 | : base(configuration["MicrosoftAppId"], configuration["MicrosoftAppPassword"]) 21 | { 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.Common/Providers/IConfigurationStorageProvider.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer.Common.Providers 6 | { 7 | using System.Threading.Tasks; 8 | using Microsoft.Teams.Apps.CrowdSourcer.Common.Models; 9 | 10 | /// 11 | /// configuration storage provider interface. 12 | /// 13 | public interface IConfigurationStorageProvider 14 | { 15 | /// 16 | /// get knowledge base Id from storage. 17 | /// 18 | /// Kb configuration. 19 | Task GetKbConfigAsync(); 20 | 21 | /// 22 | /// create knowledge base Id configuration in storage. 23 | /// 24 | /// knowledge base configuration entity. 25 | /// Knowledge base configuration details. 26 | Task CreateKbConfigAsync(KbConfiguration entity); 27 | } 28 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.Common/Providers/IObjectIdToNameMapper.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer.Common.Providers 6 | { 7 | using System.Threading.Tasks; 8 | using Microsoft.Teams.Apps.CrowdSourcer.Common.Models; 9 | 10 | /// 11 | /// Object Id name mapping storage provider interface. 12 | /// 13 | public interface IObjectIdToNameMapper 14 | { 15 | /// 16 | /// method is used to get name based on aad object id. 17 | /// 18 | /// row key of the table. 19 | /// name. 20 | Task GetNameAsync(string objectId); 21 | 22 | /// 23 | /// This method is used to add or update the aad object id and name mapping. 24 | /// 25 | /// table entity. 26 | /// table entity inserted or updated. 27 | Task UpdateNameMappingAsync(NameIdMapping entity); 28 | } 29 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer/Program.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | // Licensed under the MIT License. 5 | // Generated with Bot Builder V4 SDK Template for Visual Studio CoreBot v4.5.0 6 | 7 | namespace Microsoft.Teams.Apps.CrowdSourcer 8 | { 9 | using Microsoft.AspNetCore; 10 | using Microsoft.AspNetCore.Hosting; 11 | 12 | /// 13 | /// Main class. 14 | /// 15 | public class Program 16 | { 17 | /// 18 | /// main method of project. 19 | /// 20 | /// arguments. 21 | public static void Main(string[] args) 22 | { 23 | CreateWebHostBuilder(args).Build().Run(); 24 | } 25 | 26 | /// 27 | /// Create WebHostBuilder and initialize startup. 28 | /// 29 | /// arguments. 30 | /// WebHostBuilder. 31 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 32 | WebHost.CreateDefaultBuilder(args) 33 | .UseStartup(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer/Models/AdaptiveSubmitActionData.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer.Models 6 | { 7 | using Microsoft.Bot.Schema; 8 | using Newtonsoft.Json; 9 | 10 | /// 11 | /// Adaptive Card Action class. 12 | /// 13 | public class AdaptiveSubmitActionData 14 | { 15 | /// 16 | /// Gets or sets Msteams object. 17 | /// 18 | [JsonProperty("msteams")] 19 | public CardAction MsTeams { get; set; } 20 | 21 | /// 22 | /// Gets or sets details. 23 | /// 24 | [JsonProperty("details")] 25 | public Details Details { get; set; } 26 | 27 | /// 28 | /// Gets or sets Updated question. 29 | /// 30 | [JsonProperty("question")] 31 | public string Question { get; set; } 32 | 33 | /// 34 | /// Gets or sets Answer. 35 | /// 36 | [JsonProperty("answer")] 37 | public string Answer { get; set; } 38 | } 39 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.Common/Models/KbConfiguration.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer.Common.Models 6 | { 7 | using Microsoft.WindowsAzure.Storage.Table; 8 | 9 | /// 10 | /// Knowledgebase configuration storage entity. 11 | /// 12 | public class KbConfiguration : TableEntity 13 | { 14 | /// 15 | /// Constant value used as a partition key in storage. 16 | /// 17 | public const string KbConfigurationPartitionKey = "msteams"; 18 | 19 | /// 20 | /// Constant value used as a row key in storage. 21 | /// 22 | public const string KbConfigurationRowKey = "knowledgebaseId"; 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | public KbConfiguration() 28 | { 29 | this.PartitionKey = KbConfigurationPartitionKey; 30 | this.RowKey = KbConfigurationRowKey; 31 | } 32 | 33 | /// 34 | /// Gets or sets KbId. 35 | /// 36 | public string KbId { get; set; } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.Common/Providers/ISearchService.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer.Common.Providers 6 | { 7 | using System.Collections.Generic; 8 | using System.Threading.Tasks; 9 | using Microsoft.Teams.Apps.CrowdSourcer.Models; 10 | 11 | /// 12 | /// azure blob search service interface. 13 | /// 14 | public interface ISearchService 15 | { 16 | /// 17 | /// This method gives search result(Konwledgebase QnA pairs) based on teamId and search query for specific messaging extension command Id. 18 | /// 19 | /// searchQuery. 20 | /// messaging extension commandId. 21 | /// team Id. 22 | /// search result list. 23 | Task> GetAzureSearchEntitiesAsync(string searchQuery, string commandId, string teamId); 24 | 25 | /// 26 | /// Creates Index, Data Source and Indexer for search service. 27 | /// 28 | /// task. 29 | Task InitializeSearchServiceDependency(); 30 | } 31 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.Common/Microsoft.Teams.Apps.CrowdSourcer.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | all 29 | runtime; build; native; contentfiles; analyzers 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.Common/Models/NameIdMapping.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer.Common.Models 6 | { 7 | using Microsoft.WindowsAzure.Storage.Table; 8 | 9 | /// 10 | /// Name Id Mapping table entity. 11 | /// 12 | public class NameIdMapping : TableEntity 13 | { 14 | /// 15 | /// Constant value used as a partition key for name id mapping storage. 16 | /// 17 | public const string NameIdMappingPartitionkey = "username"; 18 | 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | public NameIdMapping() 23 | { 24 | this.PartitionKey = NameIdMappingPartitionkey; // constant 25 | } 26 | 27 | /// 28 | /// Gets or sets ObjectId. 29 | /// 30 | public string ObjectId 31 | { 32 | get 33 | { 34 | return this.RowKey; 35 | } 36 | 37 | set 38 | { 39 | this.RowKey = value; 40 | } 41 | } 42 | 43 | /// 44 | /// Gets or sets Name. 45 | /// 46 | public string Name { get; set; } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.AzureFunction/Microsoft.Teams.Apps.CrowdSourcer.AzureFunction.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp2.1 4 | v2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | PreserveNewest 33 | 34 | 35 | Always 36 | 37 | 38 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.AzureFunction/Startup.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | using System; 6 | using Microsoft.Azure.CognitiveServices.Knowledge.QnAMaker; 7 | using Microsoft.Azure.WebJobs; 8 | using Microsoft.Azure.WebJobs.Hosting; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Teams.Apps.CrowdSourcer.AzureFunction; 11 | using Microsoft.Teams.Apps.CrowdSourcer.Common.Providers; 12 | 13 | [assembly: WebJobsStartup(typeof(Startup))] 14 | 15 | namespace Microsoft.Teams.Apps.CrowdSourcer.AzureFunction 16 | { 17 | /// 18 | /// Azure function Startup Class. 19 | /// 20 | public class Startup : IWebJobsStartup 21 | { 22 | /// 23 | /// Application startup configuration. 24 | /// 25 | /// webjobs builder. 26 | public void Configure(IWebJobsBuilder builder) 27 | { 28 | builder.Services.AddSingleton(); 29 | IQnAMakerClient qnaMakerClient = new QnAMakerClient(new ApiKeyServiceClientCredentials(Environment.GetEnvironmentVariable("QnAMakerSubscriptionKey"))) { Endpoint = Environment.GetEnvironmentVariable("QnAMakerApiUrl") }; 30 | builder.Services.AddSingleton((provider) => new QnaServiceProvider( 31 | provider.GetRequiredService(), 32 | qnaMakerClient)); 33 | builder.Services.AddSingleton(); 34 | builder.Services.AddSingleton(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer/Controllers/BotController.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer.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 14 | /// implementation at runtime. 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 | private readonly IBotFrameworkHttpAdapter adapter; 22 | private readonly IBot bot; 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | /// Bot adapter. 28 | /// Bot Interface. 29 | public BotController(IBotFrameworkHttpAdapter adapter, IBot bot) 30 | { 31 | this.adapter = adapter; 32 | this.bot = bot; 33 | } 34 | 35 | /// 36 | /// Executing the Post Async method. 37 | /// 38 | /// A representing the result of the asynchronous operation. 39 | [HttpPost] 40 | public async Task PostAsync() 41 | { 42 | // Delegate the processing of the HTTP POST to the adapter. 43 | // The adapter will invoke the bot. 44 | await this.adapter.ProcessAsync(this.Request, this.Response, this.bot); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.271 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Teams.Apps.CrowdSourcer", "Microsoft.Teams.Apps.CrowdSourcer\Microsoft.Teams.Apps.CrowdSourcer.csproj", "{FBE5A0B8-15F7-4690-B3DD-0232D78EA69A}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Teams.Apps.CrowdSourcer.Common", "Microsoft.Teams.Apps.CrowdSourcer.Common\Microsoft.Teams.Apps.CrowdSourcer.Common.csproj", "{D23095AD-1E36-4157-AB9D-BE2EC3824614}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Teams.Apps.CrowdSourcer.AzureFunction", "Microsoft.Teams.Apps.CrowdSourcer.AzureFunction\Microsoft.Teams.Apps.CrowdSourcer.AzureFunction.csproj", "{424829C3-5ABA-4924-88B1-8401F09D5952}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {FBE5A0B8-15F7-4690-B3DD-0232D78EA69A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {FBE5A0B8-15F7-4690-B3DD-0232D78EA69A}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {FBE5A0B8-15F7-4690-B3DD-0232D78EA69A}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {FBE5A0B8-15F7-4690-B3DD-0232D78EA69A}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {D23095AD-1E36-4157-AB9D-BE2EC3824614}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {D23095AD-1E36-4157-AB9D-BE2EC3824614}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {D23095AD-1E36-4157-AB9D-BE2EC3824614}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {D23095AD-1E36-4157-AB9D-BE2EC3824614}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {424829C3-5ABA-4924-88B1-8401F09D5952}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {424829C3-5ABA-4924-88B1-8401F09D5952}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {424829C3-5ABA-4924-88B1-8401F09D5952}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {424829C3-5ABA-4924-88B1-8401F09D5952}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {4B541E64-2E78-439D-8967-ADFA4E6E3869} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer/AdapterWithErrorHandler.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | // Licensed under the MIT License. 5 | // Generated with Bot Builder V4 SDK Template for Visual Studio CoreBot v4.5.0 6 | 7 | namespace Microsoft.Teams.Apps.CrowdSourcer 8 | { 9 | using System; 10 | using Microsoft.ApplicationInsights; 11 | using Microsoft.Bot.Builder; 12 | using Microsoft.Bot.Builder.Integration.AspNet.Core; 13 | using Microsoft.Bot.Schema; 14 | using Microsoft.Extensions.Configuration; 15 | using Microsoft.Teams.Apps.CrowdSourcer.Resources; 16 | 17 | /// 18 | /// Log any leaked exception from the application. 19 | /// 20 | public class AdapterWithErrorHandler : BotFrameworkHttpAdapter 21 | { 22 | /// 23 | /// Initializes a new instance of the class. 24 | /// 25 | /// configuration. 26 | /// telemetry client. 27 | /// conversation state. 28 | public AdapterWithErrorHandler(IConfiguration configuration, TelemetryClient telemetryClient, ConversationState conversationState = null) 29 | : base(configuration) 30 | { 31 | this.OnTurnError = async (turnContext, exception) => 32 | { 33 | // Log any leaked exception from the application. 34 | telemetryClient.TrackException(exception); 35 | 36 | // Send a catch-all apology to the user. 37 | var errorMessage = MessageFactory.Text(Strings.ErrorMsgText, Strings.ErrorMsgText, InputHints.ExpectingInput); 38 | await turnContext.SendActivityAsync(errorMessage); 39 | 40 | if (conversationState != null) 41 | { 42 | try 43 | { 44 | // Delete the conversationState for the current conversation to prevent the 45 | // bot from getting stuck in a error-loop caused by being in a bad state. 46 | // ConversationState should be thought of as similar to "cookie-state" in a Web pages. 47 | await conversationState.DeleteAsync(turnContext); 48 | } 49 | catch (Exception e) 50 | { 51 | telemetryClient.TrackException(e); 52 | } 53 | } 54 | }; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer/Microsoft.Teams.Apps.CrowdSourcer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | latest 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | all 41 | runtime; build; native; contentfiles; analyzers 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | True 53 | True 54 | Strings.resx 55 | 56 | 57 | 58 | 59 | 60 | Always 61 | 62 | 63 | 64 | 65 | 66 | ResXFileCodeGenerator 67 | Strings.Designer.cs 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.Common/Models/AzureSearchEntity.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer.Models 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using Microsoft.Azure.CognitiveServices.Knowledge.QnAMaker.Models; 10 | using Microsoft.Azure.Search; 11 | using Microsoft.Azure.Search.Models; 12 | using Newtonsoft.Json; 13 | 14 | /// 15 | /// this entity is used for azure search. 16 | /// 17 | public class AzureSearchEntity 18 | { 19 | /// 20 | /// Gets or sets Question Id. 21 | /// 22 | [System.ComponentModel.DataAnnotations.Key] 23 | [IsFilterable] 24 | [JsonProperty("id")] 25 | public string Id { get; set; } 26 | 27 | /// 28 | /// Gets or sets Answer text. 29 | /// 30 | [IsSearchable] 31 | [IsFilterable] 32 | [Analyzer(AnalyzerName.AsString.ElMicrosoft)] 33 | [JsonProperty("answer")] 34 | public string Answer { get; set; } 35 | 36 | /// 37 | /// Gets or sets Source. 38 | /// 39 | [IsSearchable] 40 | [Analyzer(AnalyzerName.AsString.ElMicrosoft)] 41 | [JsonProperty("source")] 42 | public string Source { get; set; } 43 | 44 | /// 45 | /// Gets or sets list of Questions. 46 | /// 47 | [IsSearchable] 48 | [IsFilterable] 49 | [Analyzer(AnalyzerName.AsString.ElMicrosoft)] 50 | [JsonProperty("questions")] 51 | public IList Questions { get; set; } 52 | 53 | /// 54 | /// Gets or sets Metadata. 55 | /// 56 | [JsonProperty("metadata")] 57 | public IList Metadata { get; set; } 58 | 59 | /// 60 | /// Gets or sets CreatedDate. 61 | /// 62 | [IsSortable] 63 | [IsFilterable] 64 | [JsonProperty("createddate")] 65 | public DateTimeOffset? CreatedDate { get; set; } 66 | 67 | /// 68 | /// Gets or sets UpdatedDate. 69 | /// 70 | [IsSortable] 71 | [IsFilterable] 72 | [JsonProperty("updateddate")] 73 | public DateTimeOffset? UpdatedDate { get; set; } 74 | 75 | /// 76 | /// Gets or sets TeamId. 77 | /// 78 | [IsSearchable] 79 | [IsFilterable] 80 | [Analyzer(AnalyzerName.AsString.ElMicrosoft)] 81 | [JsonProperty("teamid")] 82 | public string TeamId { get; set; } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /deploy.function.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 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 51 | :: Deployment 52 | :: ---------- 53 | echo Handling function App deployment with Msbuild. 54 | 55 | :: 1. Restore nuget packages 56 | call :ExecuteCmd nuget.exe restore "%DEPLOYMENT_SOURCE%\Source\Microsoft.Teams.Apps.CrowdSourcer.sln" -MSBuildPath "%MSBUILD_15_DIR%" 57 | IF !ERRORLEVEL! NEQ 0 goto error 58 | 59 | :: 2. Build and publish 60 | call :ExecuteCmd "%MSBUILD_15_DIR%\MSBuild.exe" "%DEPLOYMENT_SOURCE%\Source\Microsoft.Teams.Apps.CrowdSourcer.AzureFunction\Microsoft.Teams.Apps.CrowdSourcer.AzureFunction.csproj" /p:DeployOnBuild=true /p:configuration=Release /p:publishurl="%DEPLOYMENT_TEMP%" %SCM_BUILD_ARGS% 61 | IF !ERRORLEVEL! NEQ 0 goto error 62 | 63 | :: 3. KuduSync 64 | IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" ( 65 | 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" 66 | IF !ERRORLEVEL! NEQ 0 goto error 67 | ) 68 | 69 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 70 | goto end 71 | 72 | :: Execute command routine that will echo out when error 73 | :ExecuteCmd 74 | setlocal 75 | set _CMD_=%* 76 | call %_CMD_% 77 | if "%ERRORLEVEL%" NEQ "0" echo Failed exitCode=%ERRORLEVEL%, command=%_CMD_% 78 | exit /b %ERRORLEVEL% 79 | 80 | :error 81 | endlocal 82 | echo An error has occurred during web site deployment. 83 | call :exitSetErrorLevel 84 | call :exitFromFunction 2>nul 85 | 86 | :exitSetErrorLevel 87 | exit /b 1 88 | 89 | :exitFromFunction 90 | () 91 | 92 | :end 93 | endlocal 94 | echo Finished successfully. -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.Common/Constants.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer.Common 6 | { 7 | /// 8 | /// constants. 9 | /// 10 | public static class Constants 11 | { 12 | /// 13 | /// Unanswered name. 14 | /// 15 | public const string Unanswered = "#$unanswered$#"; 16 | 17 | /// 18 | /// Action name. 19 | /// 20 | public const string SubmitAddCommand = "submit/add"; 21 | 22 | /// 23 | /// save command. 24 | /// 25 | public const string SaveCommand = "save"; 26 | 27 | /// 28 | /// delete command. 29 | /// 30 | public const string DeleteCommand = "delete"; 31 | 32 | /// 33 | /// no command. 34 | /// 35 | public const string NoCommand = "no"; 36 | 37 | /// 38 | /// add command text. 39 | /// 40 | public const string AddCommand = "add question"; 41 | 42 | /// 43 | /// qna metadata team id name. 44 | /// 45 | public const string MetadataTeamId = "teamid"; 46 | 47 | /// 48 | /// qna metadata createdat name. 49 | /// 50 | public const string MetadataCreatedAt = "createdat"; 51 | 52 | /// 53 | /// qna metadata createdby name. 54 | /// 55 | public const string MetadataCreatedBy = "createdby"; 56 | 57 | /// 58 | /// qna metadata conversationid name. 59 | /// 60 | public const string MetadataConversationId = "conversationid"; 61 | 62 | /// 63 | /// qna metadata updatedat name. 64 | /// 65 | public const string MetadataUpdatedAt = "updatedat"; 66 | 67 | /// 68 | /// qna metadata updatedby name. 69 | /// 70 | public const string MetadataUpdatedBy = "updatedby"; 71 | 72 | /// 73 | /// MessagingExtension recently created command id. 74 | /// 75 | public const string CreatedCommandId = "created"; 76 | 77 | /// 78 | /// MessagingExtension recently edited command id. 79 | /// 80 | public const string EditedCommandId = "edited"; 81 | 82 | /// 83 | /// MessagingExtension unanswered command id. 84 | /// 85 | public const string UnansweredCommandId = "unanswered"; 86 | 87 | /// 88 | /// Blob Container Name. 89 | /// 90 | public const string StorageContainer = "crowdsourcer-search-container"; 91 | 92 | /// 93 | /// Folder inside blob container. 94 | /// 95 | public const string FolderName = "crowdsourcer-metadata"; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /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.CrowdSourcer.sln" 71 | IF !ERRORLEVEL! NEQ 0 goto error 72 | 73 | :: 2. Build and publish 74 | call :ExecuteCmd dotnet publish "%DEPLOYMENT_SOURCE%\Source\Microsoft.Teams.Apps.CrowdSourcer\Microsoft.Teams.Apps.CrowdSourcer.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 | -------------------------------------------------------------------------------- /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.0.0", 5 | "id":"8122b9a6-5589-479f-a41d-2a3df1668b1e", 6 | "packageName":"com.microsoft.teams.crowdsourcer", 7 | "developer":{ 8 | "name":"<>", 9 | "websiteUrl":"<>", 10 | "privacyUrl":"<>", 11 | "termsOfUseUrl":"<>" 12 | }, 13 | "icons":{ 14 | "color":"color.png", 15 | "outline":"outline.png" 16 | }, 17 | "name":{ 18 | "short":"Crowdsourcer", 19 | "full":"Crowdsourcer Bot" 20 | }, 21 | "description":{ 22 | "short":"QnA bot that works on the concept of crowdsourcing information in teams.", 23 | "full":"A friendly Q&A bot that helps a group of people collaborate to obtain voluntary answers to their queries in a fun and transparent manner." 24 | }, 25 | "accentColor":"#152450", 26 | "bots":[ 27 | { 28 | "botId":"<>", 29 | "scopes":[ 30 | "team" 31 | ], 32 | "supportsFiles":false, 33 | "isNotificationOnly":false 34 | } 35 | ], 36 | "composeExtensions":[ 37 | { 38 | "botId":"<>", 39 | "canUpdateConfiguration":false, 40 | "commands":[ 41 | { 42 | "id":"created", 43 | "title":"Recent answers", 44 | "description":"Search recent answers", 45 | "initialRun":true, 46 | "parameters":[ 47 | { 48 | "name":"created", 49 | "title":"created", 50 | "description":"Recently created" 51 | } 52 | ] 53 | }, 54 | { 55 | "id":"edited", 56 | "title":"Recent updates", 57 | "description":"Search recent updates", 58 | "initialRun":true, 59 | "parameters":[ 60 | { 61 | "name":"edited", 62 | "title":"edited", 63 | "description":"Recently edited" 64 | } 65 | ] 66 | }, 67 | { 68 | "id":"unanswered", 69 | "title":"Unanswered", 70 | "description":"Search unanswered", 71 | "initialRun":true, 72 | "parameters":[ 73 | { 74 | "name":"unanswered", 75 | "title":"Unanswered", 76 | "description":"Search unanswered question" 77 | } 78 | ] 79 | }, 80 | { 81 | "id":"addquestion", 82 | "type":"action", 83 | "title":"Add a question", 84 | "description":"Add a qna pair", 85 | "initialRun":true, 86 | "fetchTask":true, 87 | "context":[ 88 | "compose" 89 | ], 90 | "parameters":[ 91 | { 92 | "name":"addquestion", 93 | "title":"Add a question", 94 | "description":"Add a qna pair" 95 | } 96 | ] 97 | } 98 | ] 99 | } 100 | ], 101 | "permissions":[ 102 | "identity", 103 | "messageTeamMembers" 104 | ], 105 | "validDomains":[ 106 | "<>" 107 | ] 108 | } -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.Common/Providers/IQnaServiceProvider.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer.Common.Providers 6 | { 7 | using System.Collections.Generic; 8 | using System.Threading.Tasks; 9 | using Microsoft.Azure.CognitiveServices.Knowledge.QnAMaker.Models; 10 | 11 | /// 12 | /// qna maker service provider interface. 13 | /// 14 | public interface IQnaServiceProvider 15 | { 16 | /// 17 | /// this method is used to add QnA pair in Kb. 18 | /// 19 | /// question text. 20 | /// answer text. 21 | /// created by user. 22 | /// team id. 23 | /// conversation id. 24 | /// task. 25 | Task AddQnaAsync(string question, string answer, string createdBy, string teamId, string conversationId); 26 | 27 | /// 28 | /// this method is used to create knowledgebase. 29 | /// 30 | /// kb id. 31 | Task CreateKnowledgeBaseAsync(); 32 | 33 | /// 34 | /// this method is used to delete Qna pair from KB. 35 | /// 36 | /// question id. 37 | /// team id. 38 | /// delete response. 39 | Task DeleteQnaAsync(int questionId, string teamId); 40 | 41 | /// 42 | /// get answer from kb for a given question. 43 | /// 44 | /// prod or test. 45 | /// question text. 46 | /// team id. 47 | /// qnaSearchResult response. 48 | Task GenerateAnswerAsync(bool isTest, string question, string teamId); 49 | 50 | /// 51 | /// this method can be used to publish the Kb. 52 | /// 53 | /// kb id. 54 | /// task. 55 | Task PublishKnowledgebaseAsync(string kbId); 56 | 57 | /// 58 | /// this method is used to update Qna pair in Kb. 59 | /// 60 | /// question id. 61 | /// answer text. 62 | /// updated by user. 63 | /// updated question text. 64 | /// original question text. 65 | /// team id. 66 | /// task. 67 | Task UpdateQnaAsync(int questionId, string answer, string updatedBy, string updatedQuestion, string question, string teamId); 68 | 69 | /// 70 | /// Checks whether knowledge base need to be published. 71 | /// 72 | /// Knowledgebase ID. 73 | /// boolean variable to publish or not. 74 | Task GetPublishStatusAsync(string kbId); 75 | 76 | /// 77 | /// this method returns the downloaded kb documents. 78 | /// 79 | /// knowledgebase Id. 80 | /// json string. 81 | Task> DownloadKnowledgebaseAsync(string kbId); 82 | 83 | /// 84 | /// This method is used to delete a knowledgebase. 85 | /// 86 | /// knowledgebase Id. 87 | /// task. 88 | Task DeleteKnowledgebaseAsync(string kbId); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.Common/Providers/ConfigurationStorageProvider.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer.Common.Providers 6 | { 7 | using System; 8 | using System.Threading.Tasks; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Teams.Apps.CrowdSourcer.Common.Models; 11 | using Microsoft.WindowsAzure.Storage; 12 | using Microsoft.WindowsAzure.Storage.Table; 13 | 14 | /// 15 | /// Configuration StorageProvider. 16 | /// 17 | public class ConfigurationStorageProvider : IConfigurationStorageProvider 18 | { 19 | private const string CrowdSourcerTableName = "crowdsourcerconfig"; 20 | private readonly Lazy initializeTask; 21 | private CloudTableClient cloudTableClient; 22 | private CloudTable configurationCloudTable; 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | /// config settings. 28 | public ConfigurationStorageProvider(IConfiguration configuration) 29 | { 30 | this.initializeTask = new Lazy(() => this.InitializeAsync(configuration["StorageConnectionString"])); 31 | } 32 | 33 | /// 34 | /// get knowledge base Id from storage. 35 | /// 36 | /// Kb Configuration. 37 | public async Task GetKbConfigAsync() 38 | { 39 | await this.EnsureInitializedAsync(); 40 | TableOperation retrieveOperation = TableOperation.Retrieve(KbConfiguration.KbConfigurationPartitionKey, KbConfiguration.KbConfigurationRowKey); 41 | TableResult result = await this.configurationCloudTable.ExecuteAsync(retrieveOperation); 42 | return result?.Result as KbConfiguration; 43 | } 44 | 45 | /// 46 | /// create knowledge base Id configuration in storage. 47 | /// 48 | /// KbConfiguration entity. 49 | /// Knowledge base configuration details. 50 | public async Task CreateKbConfigAsync(KbConfiguration entity) 51 | { 52 | if (entity == null) 53 | { 54 | throw new ArgumentNullException("entity"); 55 | } 56 | 57 | await this.EnsureInitializedAsync(); 58 | TableOperation insertOrMergeOperation = TableOperation.InsertOrReplace(entity); 59 | TableResult result = await this.configurationCloudTable.ExecuteAsync(insertOrMergeOperation); 60 | return result.Result as KbConfiguration; 61 | } 62 | 63 | /// 64 | /// Create crowdsourcerconfig table if it does not exist. 65 | /// 66 | /// storage account connection string. 67 | /// representing the asynchronous operation task which represents table is created if its not existing. 68 | private async Task InitializeAsync(string connectionString) 69 | { 70 | CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString); 71 | this.cloudTableClient = storageAccount.CreateCloudTableClient(); 72 | this.configurationCloudTable = this.cloudTableClient.GetTableReference(CrowdSourcerTableName); 73 | if (!await this.configurationCloudTable.ExistsAsync()) 74 | { 75 | await this.configurationCloudTable.CreateIfNotExistsAsync(); 76 | } 77 | 78 | return this.configurationCloudTable; 79 | } 80 | 81 | /// 82 | /// this method is called to ensure InitializeAsync method is called before any storage operation. 83 | /// 84 | /// Task. 85 | private async Task EnsureInitializedAsync() 86 | { 87 | await this.initializeTask.Value; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.Common/Providers/ObjectIdToNameMapper.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer.Common.Providers 6 | { 7 | using System; 8 | using System.Threading.Tasks; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Teams.Apps.CrowdSourcer.Common.Models; 11 | using Microsoft.WindowsAzure.Storage; 12 | using Microsoft.WindowsAzure.Storage.Table; 13 | 14 | /// 15 | /// Name Id mapping storage provider class. 16 | /// 17 | public class ObjectIdToNameMapper : IObjectIdToNameMapper 18 | { 19 | private const string NameIdMappingTableName = "crowdsourcernames"; 20 | private readonly Lazy initializeTask; 21 | private CloudTableClient cloudTableClient; 22 | private CloudTable configurationCloudTable; 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | /// configuration settings. 28 | public ObjectIdToNameMapper(IConfiguration configuration) 29 | { 30 | this.initializeTask = new Lazy(() => this.InitializeAsync(configuration["StorageConnectionString"])); 31 | } 32 | 33 | /// 34 | /// This method is used to add or update the aad object id and name mapping. 35 | /// 36 | /// table entity. 37 | /// table entity inserted or updated. 38 | public async Task UpdateNameMappingAsync(NameIdMapping entity) 39 | { 40 | if (entity == null) 41 | { 42 | throw new ArgumentNullException("entity"); 43 | } 44 | 45 | await this.EnsureInitializedAsync(); 46 | TableOperation insertOrMergeOperation = TableOperation.InsertOrReplace(entity); 47 | TableResult result = await this.configurationCloudTable.ExecuteAsync(insertOrMergeOperation); 48 | return result.Result as NameIdMapping; 49 | } 50 | 51 | /// 52 | /// This method is used to get name based on aad object id. 53 | /// 54 | /// aad object Id. 55 | /// name. 56 | public async Task GetNameAsync(string objectId) 57 | { 58 | await this.EnsureInitializedAsync(); 59 | TableOperation retrieveOperation = TableOperation.Retrieve(NameIdMapping.NameIdMappingPartitionkey, objectId); 60 | TableResult result = await this.configurationCloudTable.ExecuteAsync(retrieveOperation); 61 | var mappingEntity = result?.Result as NameIdMapping; 62 | return mappingEntity.Name; 63 | } 64 | 65 | /// 66 | /// Create name mapping table if it doesnt exists. 67 | /// 68 | /// storage account connection string. 69 | /// representing the asynchronous operation task which represents table is created if its not existing. 70 | private async Task InitializeAsync(string connectionString) 71 | { 72 | CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString); 73 | this.cloudTableClient = storageAccount.CreateCloudTableClient(); 74 | this.configurationCloudTable = this.cloudTableClient.GetTableReference(NameIdMappingTableName); 75 | if (!await this.configurationCloudTable.ExistsAsync()) 76 | { 77 | await this.configurationCloudTable.CreateIfNotExistsAsync(); 78 | } 79 | 80 | return this.configurationCloudTable; 81 | } 82 | 83 | /// 84 | /// This method is called to ensure InitializeAsync method is called before any storage operation. 85 | /// 86 | /// Task. 87 | private async Task EnsureInitializedAsync() 88 | { 89 | await this.initializeTask.Value; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer/Startup.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | // Licensed under the MIT License. 5 | // Generated with Bot Builder V4 SDK Template for Visual Studio CoreBot v4.5.0 6 | 7 | namespace Microsoft.Teams.Apps.CrowdSourcer 8 | { 9 | using System.Threading.Tasks; 10 | using Microsoft.ApplicationInsights; 11 | using Microsoft.AspNetCore.Builder; 12 | using Microsoft.AspNetCore.Hosting; 13 | using Microsoft.AspNetCore.Mvc; 14 | using Microsoft.Azure.CognitiveServices.Knowledge.QnAMaker; 15 | using Microsoft.Bot.Builder; 16 | using Microsoft.Bot.Builder.Integration.AspNet.Core; 17 | using Microsoft.Bot.Connector.Authentication; 18 | using Microsoft.Extensions.DependencyInjection; 19 | using Microsoft.Teams.Apps.CrowdSourcer.Bots; 20 | using Microsoft.Teams.Apps.CrowdSourcer.Cards; 21 | using Microsoft.Teams.Apps.CrowdSourcer.Common.Providers; 22 | 23 | /// 24 | /// This a Startup class for this Bot. 25 | /// 26 | public class Startup 27 | { 28 | /// 29 | /// Initializes a new instance of the class. 30 | /// 31 | /// Startup Configuration. 32 | public Startup(Extensions.Configuration.IConfiguration configuration) 33 | { 34 | this.Configuration = configuration; 35 | } 36 | 37 | /// 38 | /// Gets Configurations Interfaces. 39 | /// 40 | public Extensions.Configuration.IConfiguration Configuration { get; } 41 | 42 | /// 43 | /// This method gets called by the runtime. Use this method to add services to the container. 44 | /// 45 | /// Service Collection Interface. 46 | public void ConfigureServices(IServiceCollection services) 47 | { 48 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); 49 | services.AddSingleton(); 50 | services.AddSingleton(); 51 | services.AddSingleton(new MicrosoftAppCredentials(this.Configuration["MicrosoftAppId"], this.Configuration["MicrosoftAppPassword"])); 52 | services.AddSingleton(new ConfigurationStorageProvider(this.Configuration)); 53 | services.AddSingleton(new ObjectIdToNameMapper(this.Configuration)); 54 | 55 | IQnAMakerClient qnaMakerClient = new QnAMakerClient(new ApiKeyServiceClientCredentials(this.Configuration["QnAMakerSubscriptionKey"])) { Endpoint = this.Configuration["QnAMakerApiUrl"] }; 56 | string endpointKey = Task.Run(() => qnaMakerClient.EndpointKeys.GetKeysAsync()).Result.PrimaryEndpointKey; 57 | 58 | services.AddSingleton((provider) => new QnaServiceProvider( 59 | provider.GetRequiredService(), 60 | this.Configuration, 61 | qnaMakerClient, 62 | new QnAMakerRuntimeClient(new EndpointKeyServiceClientCredentials(endpointKey)) { RuntimeEndpoint = this.Configuration["QnAMakerHostUrl"] })); 63 | 64 | services.AddSingleton(); 65 | services.AddSingleton(); 66 | 67 | services.AddTransient((provider) => new CrowdSourcerBot( 68 | provider.GetRequiredService(), 69 | provider.GetRequiredService(), 70 | this.Configuration, 71 | provider.GetRequiredService(), 72 | provider.GetRequiredService(), 73 | provider.GetRequiredService(), 74 | provider.GetRequiredService())); 75 | 76 | services.AddApplicationInsightsTelemetry(); 77 | services.AddLocalization(); 78 | } 79 | 80 | /// 81 | /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 82 | /// 83 | /// Application Builder. 84 | /// Hosting Environment. 85 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 86 | { 87 | if (env.IsDevelopment()) 88 | { 89 | app.UseDeveloperExceptionPage(); 90 | } 91 | else 92 | { 93 | app.UseHsts(); 94 | } 95 | 96 | app.UseDefaultFiles(); 97 | app.UseStaticFiles(); 98 | app.UseMvc(); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.Common/Providers/SearchServiceDataProvider.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer.Common.Providers 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | using Microsoft.Azure.CognitiveServices.Knowledge.QnAMaker.Models; 12 | using Microsoft.Teams.Apps.CrowdSourcer.Models; 13 | using Microsoft.WindowsAzure.Storage; 14 | using Microsoft.WindowsAzure.Storage.Blob; 15 | using Newtonsoft.Json; 16 | 17 | /// 18 | /// azure search service blob storage data provider. 19 | /// 20 | public class SearchServiceDataProvider : ISearchServiceDataProvider 21 | { 22 | private readonly IQnaServiceProvider qnaServiceProvider; 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | /// qna ServiceProvider. 28 | public SearchServiceDataProvider(IQnaServiceProvider qnaServiceProvider) 29 | { 30 | this.qnaServiceProvider = qnaServiceProvider; 31 | } 32 | 33 | /// 34 | /// this method downloads the knowledgebase and stores the json string to blob storage. 35 | /// 36 | /// knowledgebase id. 37 | /// task. 38 | public async Task SetupAzureSearchDataAsync(string kbId) 39 | { 40 | IEnumerable qnaDocuments = await this.qnaServiceProvider.DownloadKnowledgebaseAsync(kbId); 41 | string azureJson = this.GenerateFormattedJson(qnaDocuments); 42 | await this.AddDatatoBlobStorage(azureJson); 43 | } 44 | 45 | /// 46 | /// Function to convert input JSON to align with Schema Definition. 47 | /// 48 | /// qna documents. 49 | /// create json format for search. 50 | private string GenerateFormattedJson(IEnumerable qnaDocuments) 51 | { 52 | List searchEntityList = new List(); 53 | foreach (var item in qnaDocuments) 54 | { 55 | var createdDate = item.Metadata.Where(prop => prop.Name == Constants.MetadataCreatedAt).FirstOrDefault(); 56 | var updatedDate = item.Metadata.Where(prop => prop.Name == Constants.MetadataUpdatedAt).FirstOrDefault(); 57 | var teamId = item.Metadata.Where(prop => prop.Name == Constants.MetadataTeamId).First().Value.ToString(); 58 | 59 | searchEntityList.Add( 60 | new AzureSearchEntity() 61 | { 62 | Id = item.Id.ToString(), 63 | Source = item.Source, 64 | Questions = item.Questions, 65 | Answer = item.Answer, 66 | CreatedDate = createdDate != null ? new DateTimeOffset(new DateTime(Convert.ToInt64(createdDate.Value))) : default(DateTimeOffset), 67 | UpdatedDate = updatedDate != null ? new DateTimeOffset(new DateTime(Convert.ToInt64(updatedDate.Value))) : default(DateTimeOffset), 68 | TeamId = teamId, 69 | Metadata = item.Metadata, 70 | }); 71 | } 72 | 73 | return JsonConvert.SerializeObject(searchEntityList); 74 | } 75 | 76 | /// 77 | /// This method is used to store json to blob storage. 78 | /// 79 | /// knowledgebase jsonData string. 80 | /// task. 81 | private async Task AddDatatoBlobStorage(string jsonData) 82 | { 83 | // Retrieve storage account from connection string. 84 | CloudStorageAccount storageAccount = CloudStorageAccount.Parse(Environment.GetEnvironmentVariable("AzureWebJobsStorage")); 85 | 86 | // Create the blob client. 87 | CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient(); 88 | 89 | // Retrieve a reference to a container. 90 | CloudBlobContainer container = blobClient.GetContainerReference(Constants.StorageContainer); 91 | 92 | // Create the container if it doesn't already exist. 93 | var result = await container.CreateIfNotExistsAsync(); 94 | 95 | // Retrieve reference to blob. 96 | CloudBlockBlob blockBlob = container.GetBlockBlobReference(Constants.FolderName + "/teamscrowdsourcer.json"); 97 | blockBlob.Properties.ContentType = "application/json"; 98 | 99 | // Upload JSON to blob storage. 100 | await blockBlob.UploadTextAsync(jsonData); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crowdsourcer Bot 2 | 3 | | [Documentation](https://github.com/OfficeDev/microsoft-teams-crowdsourcer-app/wiki) | [Deployment guide](https://github.com/OfficeDev/microsoft-teams-crowdsourcer-app/wiki/Deployment-Guide) | [Architecture](https://github.com/OfficeDev/microsoft-teams-crowdsourcer-app/wiki/Solution-Overview) | 4 | | ---- | ---- | ---- | 5 | 6 | Chatbots are an easy way to provide answers to frequently asked questions by users. However, most chatbots fail to engage with users in meaningful way because there is no human in the loop when the chatbot fails. 7 | 8 | Crowdsourcer Bot helps a group of people (team) collaborate to obtain voluntary answers to their queries in a fun and transparent manner. It works on the principle of tapping on to the crowd intelligence and collective wisdom of the group. Using Crowdsourcer ,you can get your questions answered by relying on the knowledge of a team member. The QnA pairs are stored in a Knowledge Base (KB) and over a period of time ,it will help answer most frequently asked questions. 9 | 10 | End-user interacting with Crowdsourcer Bot in a teams context 11 | ![Ask question](https://github.com/OfficeDev/microsoft-teams-crowdsourcer-app/wiki/images/Readme-1.png) 12 | ![Answered question](https://github.com/OfficeDev/microsoft-teams-crowdsourcer-app/wiki/images/Readme-3.png) 13 | ![Messaging Extension](https://github.com/OfficeDev/microsoft-teams-crowdsourcer-app/wiki/images/Readme-4.png) 14 | 15 | ## Legal Notice 16 | This app template is provided under the [MIT License](https://github.com/OfficeDev/microsoft-teams-crowdsourcer-app/blob/master/LICENSE) terms. In addition to these terms, by using this app template you agree to the following: 17 | 18 | - You, not Microsoft, will license the use of your app to users or organization. 19 | 20 | - 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. 21 | 22 | - 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. 23 | 24 | - 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). 25 | 26 | - 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. 27 | 28 | - 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. 29 | 30 | ## **Getting** **Started** 31 | 32 | Begin with the [Solution overview](https://github.com/OfficeDev/microsoft-teams-crowdsourcer-app/wiki/Solution-Overview) to read about what the app does and how it works. 33 | 34 | When you're ready to try out Crowdsourcer, or to use it in your own organization, follow the steps in the [Deployment guide](https://github.com/OfficeDev/microsoft-teams-crowdsourcer-app/wiki/Deployment-Guide). 35 | 36 | ## **Feedback** 37 | 38 | Thoughts? Questions? Ideas? Share them with us on [Teams UserVoice](https://microsoftteams.uservoice.com/forums/555103-public)! 39 | 40 | Please report bugs and other code issues [here](/issues/new). 41 | 42 | ## **Contributing** 43 | 44 | This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit [https://cla.microsoft.com](https://cla.microsoft.com/). 45 | 46 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. 47 | 48 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 49 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.AzureFunction/PublishFunction.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer.PublishFunction 6 | { 7 | using System; 8 | using System.Threading.Tasks; 9 | using Microsoft.Azure.WebJobs; 10 | using Microsoft.Extensions.Logging; 11 | using Microsoft.Teams.Apps.CrowdSourcer.Common.Models; 12 | using Microsoft.Teams.Apps.CrowdSourcer.Common.Providers; 13 | using Polly; 14 | using Polly.Contrib.WaitAndRetry; 15 | using Polly.Retry; 16 | 17 | /// 18 | /// Azure Function to create and publish knowledge bases. 19 | /// 20 | public class PublishFunction 21 | { 22 | /// 23 | /// Retry policy with jitter, Reference: https://github.com/Polly-Contrib/Polly.Contrib.WaitAndRetry#new-jitter-recommendation. 24 | /// 25 | private static RetryPolicy retryPolicy = Policy.Handle() 26 | .WaitAndRetryAsync(Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromMilliseconds(1000), 2)); 27 | 28 | private readonly IConfigurationStorageProvider configurationStorageProvider; 29 | private readonly IQnaServiceProvider qnaServiceProvider; 30 | private readonly ISearchServiceDataProvider searchServiceDataProvider; 31 | private readonly ISearchService searchService; 32 | 33 | /// 34 | /// Initializes a new instance of the class. 35 | /// 36 | /// search service data provider. 37 | /// qna service provider. 38 | /// configuration storage provider. 39 | /// search service. 40 | public PublishFunction(IConfigurationStorageProvider configurationStorageProvider, IQnaServiceProvider qnaServiceProvider, ISearchServiceDataProvider searchServiceDataProvider, ISearchService searchService) 41 | { 42 | this.configurationStorageProvider = configurationStorageProvider; 43 | this.qnaServiceProvider = qnaServiceProvider; 44 | this.searchServiceDataProvider = searchServiceDataProvider; 45 | this.searchService = searchService; 46 | } 47 | 48 | /// 49 | /// Function to get knowledge base Id, create knowledge base if not exist and publish knowledge base. Also setup the azure search service dependencies. 50 | /// 51 | /// Publish frequency. 52 | /// Log. 53 | /// A representing the result of the asynchronous operation. 54 | [FunctionName("PublishFunction")] 55 | public async Task Run([TimerTrigger("0 */15 * * * *", RunOnStartup = true)]TimerInfo myTimer, ILogger log) 56 | { 57 | try 58 | { 59 | KbConfiguration kbConfiguration = await this.configurationStorageProvider.GetKbConfigAsync(); 60 | if (kbConfiguration == null) 61 | { 62 | log.LogInformation("Creating knowledge base"); 63 | string kbId = await this.qnaServiceProvider.CreateKnowledgeBaseAsync(); 64 | 65 | log.LogInformation("Publishing knowledge base"); 66 | await this.qnaServiceProvider.PublishKnowledgebaseAsync(kbId); 67 | 68 | KbConfiguration kbConfigurationEntity = new KbConfiguration() 69 | { 70 | KbId = kbId, 71 | }; 72 | 73 | log.LogInformation("Storing knowledgebase Id in storage " + kbId); 74 | try 75 | { 76 | await retryPolicy.ExecuteAsync(async () => 77 | { 78 | kbConfiguration = await this.configurationStorageProvider.CreateKbConfigAsync(kbConfigurationEntity); 79 | }); 80 | } 81 | catch (Exception ex) 82 | { 83 | log.LogError("Error: " + ex.ToString()); 84 | log.LogWarning("Failed to store knowledgebase Id in storage. Deleting " + kbId); 85 | await this.qnaServiceProvider.DeleteKnowledgebaseAsync(kbId); 86 | return; 87 | } 88 | 89 | log.LogInformation("Setup Azure Search Data"); 90 | await this.searchServiceDataProvider.SetupAzureSearchDataAsync(kbId); 91 | log.LogInformation("Update Azure Search service"); 92 | await this.searchService.InitializeSearchServiceDependency(); 93 | } 94 | else 95 | { 96 | bool toBePublished = await this.qnaServiceProvider.GetPublishStatusAsync(kbConfiguration.KbId); 97 | log.LogInformation("To be Published - " + toBePublished); 98 | log.LogInformation("KbId - " + kbConfiguration.KbId); 99 | 100 | if (toBePublished) 101 | { 102 | log.LogInformation("Publishing knowledgebase"); 103 | await this.qnaServiceProvider.PublishKnowledgebaseAsync(kbConfiguration.KbId); 104 | log.LogInformation("Setup Azure Search Data"); 105 | await this.searchServiceDataProvider.SetupAzureSearchDataAsync(kbConfiguration.KbId); 106 | log.LogInformation("Update Azure Search service"); 107 | await this.searchService.InitializeSearchServiceDependency(); 108 | } 109 | } 110 | } 111 | catch (Exception ex) 112 | { 113 | log.LogError("Error: " + ex.Message); // Exception logging. 114 | log.LogError(ex.ToString()); 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | 5 | # User-specific files 6 | *.rsuser 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Mono auto generated files 16 | mono_crash.* 17 | 18 | # Build results 19 | [Dd]ebug/ 20 | [Dd]ebugPublic/ 21 | [Rr]elease/ 22 | [Rr]eleases/ 23 | x64/ 24 | x86/ 25 | [Aa][Rr][Mm]/ 26 | [Aa][Rr][Mm]64/ 27 | bld/ 28 | [Bb]in/ 29 | [Oo]bj/ 30 | [Ll]og/ 31 | [Ll]ogs/ 32 | 33 | # Visual Studio 2015/2017 cache/options directory 34 | .vs/ 35 | # Uncomment if you have tasks that create the project's static files in wwwroot 36 | #wwwroot/ 37 | 38 | # Visual Studio 2017 auto generated files 39 | Generated\ Files/ 40 | 41 | # MSTest test Results 42 | [Tt]est[Rr]esult*/ 43 | [Bb]uild[Ll]og.* 44 | 45 | # NUNIT 46 | *.VisualState.xml 47 | TestResult.xml 48 | nunit-*.xml 49 | 50 | # Build Results of an ATL Project 51 | [Dd]ebugPS/ 52 | [Rr]eleasePS/ 53 | dlldata.c 54 | 55 | # Benchmark Results 56 | BenchmarkDotNet.Artifacts/ 57 | 58 | # .NET Core 59 | project.lock.json 60 | project.fragment.lock.json 61 | artifacts/ 62 | 63 | # StyleCop 64 | StyleCopReport.xml 65 | 66 | # Files built by Visual Studio 67 | *_i.c 68 | *_p.c 69 | *_h.h 70 | *.ilk 71 | *.meta 72 | *.obj 73 | *.iobj 74 | *.pch 75 | *.pdb 76 | *.ipdb 77 | *.pgc 78 | *.pgd 79 | *.rsp 80 | *.sbr 81 | *.tlb 82 | *.tli 83 | *.tlh 84 | *.tmp 85 | *.tmp_proj 86 | *_wpftmp.csproj 87 | *.log 88 | *.vspscc 89 | *.vssscc 90 | .builds 91 | *.pidb 92 | *.svclog 93 | *.scc 94 | 95 | # Chutzpah Test files 96 | _Chutzpah* 97 | 98 | # Visual C++ cache files 99 | ipch/ 100 | *.aps 101 | *.ncb 102 | *.opendb 103 | *.opensdf 104 | *.sdf 105 | *.cachefile 106 | *.VC.db 107 | *.VC.VC.opendb 108 | 109 | # Visual Studio profiler 110 | *.psess 111 | *.vsp 112 | *.vspx 113 | *.sap 114 | 115 | # Visual Studio Trace Files 116 | *.e2e 117 | 118 | # TFS 2012 Local Workspace 119 | $tf/ 120 | 121 | # Guidance Automation Toolkit 122 | *.gpState 123 | 124 | # ReSharper is a .NET coding add-in 125 | _ReSharper*/ 126 | *.[Rr]e[Ss]harper 127 | *.DotSettings.user 128 | 129 | # JustCode is a .NET coding add-in 130 | .JustCode 131 | 132 | # TeamCity is a build add-in 133 | _TeamCity* 134 | 135 | # DotCover is a Code Coverage Tool 136 | *.dotCover 137 | 138 | # AxoCover is a Code Coverage Tool 139 | .axoCover/* 140 | !.axoCover/settings.json 141 | 142 | # Visual Studio code coverage results 143 | *.coverage 144 | *.coveragexml 145 | 146 | # NCrunch 147 | _NCrunch_* 148 | .*crunch*.local.xml 149 | nCrunchTemp_* 150 | 151 | # MightyMoose 152 | *.mm.* 153 | AutoTest.Net/ 154 | 155 | # Web workbench (sass) 156 | .sass-cache/ 157 | 158 | # Installshield output folder 159 | [Ee]xpress/ 160 | 161 | # DocProject is a documentation generator add-in 162 | DocProject/buildhelp/ 163 | DocProject/Help/*.HxT 164 | DocProject/Help/*.HxC 165 | DocProject/Help/*.hhc 166 | DocProject/Help/*.hhk 167 | DocProject/Help/*.hhp 168 | DocProject/Help/Html2 169 | DocProject/Help/html 170 | 171 | # Click-Once directory 172 | publish/ 173 | 174 | # Publish Web Output 175 | *.[Pp]ublish.xml 176 | *.azurePubxml 177 | # Note: Comment the next line if you want to checkin your web deploy settings, 178 | # but database connection strings (with potential passwords) will be unencrypted 179 | *.pubxml 180 | *.publishproj 181 | 182 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 183 | # checkin your Azure Web App publish settings, but sensitive information contained 184 | # in these scripts will be unencrypted 185 | PublishScripts/ 186 | 187 | # NuGet Packages 188 | *.nupkg 189 | # NuGet Symbol Packages 190 | *.snupkg 191 | # The packages folder can be ignored because of Package Restore 192 | **/[Pp]ackages/* 193 | # except build/, which is used as an MSBuild target. 194 | !**/[Pp]ackages/build/ 195 | # Uncomment if necessary however generally it will be regenerated when needed 196 | #!**/[Pp]ackages/repositories.config 197 | # NuGet v3's project.json files produces more ignorable files 198 | *.nuget.props 199 | *.nuget.targets 200 | 201 | # Microsoft Azure Build Output 202 | csx/ 203 | *.build.csdef 204 | 205 | # Microsoft Azure Emulator 206 | ecf/ 207 | rcf/ 208 | 209 | # Windows Store app package directories and files 210 | AppPackages/ 211 | BundleArtifacts/ 212 | Package.StoreAssociation.xml 213 | _pkginfo.txt 214 | *.appx 215 | *.appxbundle 216 | *.appxupload 217 | 218 | # Visual Studio cache files 219 | # files ending in .cache can be ignored 220 | *.[Cc]ache 221 | # but keep track of directories ending in .cache 222 | !?*.[Cc]ache/ 223 | 224 | # Others 225 | ClientBin/ 226 | ~$* 227 | *~ 228 | *.dbmdl 229 | *.dbproj.schemaview 230 | *.jfm 231 | *.pfx 232 | *.publishsettings 233 | orleans.codegen.cs 234 | 235 | # Including strong name files can present a security risk 236 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 237 | #*.snk 238 | 239 | # Since there are multiple workflows, uncomment next line to ignore bower_components 240 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 241 | #bower_components/ 242 | 243 | # RIA/Silverlight projects 244 | Generated_Code/ 245 | 246 | # Backup & report files from converting an old project file 247 | # to a newer Visual Studio version. Backup files are not needed, 248 | # because we have git ;-) 249 | _UpgradeReport_Files/ 250 | Backup*/ 251 | UpgradeLog*.XML 252 | UpgradeLog*.htm 253 | ServiceFabricBackup/ 254 | *.rptproj.bak 255 | 256 | # SQL Server files 257 | *.mdf 258 | *.ldf 259 | *.ndf 260 | 261 | # Business Intelligence projects 262 | *.rdl.data 263 | *.bim.layout 264 | *.bim_*.settings 265 | *.rptproj.rsuser 266 | *- [Bb]ackup.rdl 267 | *- [Bb]ackup ([0-9]).rdl 268 | *- [Bb]ackup ([0-9][0-9]).rdl 269 | 270 | # Microsoft Fakes 271 | FakesAssemblies/ 272 | 273 | # GhostDoc plugin setting file 274 | *.GhostDoc.xml 275 | 276 | # Node.js Tools for Visual Studio 277 | .ntvs_analysis.dat 278 | node_modules/ 279 | 280 | # Visual Studio 6 build log 281 | *.plg 282 | 283 | # Visual Studio 6 workspace options file 284 | *.opt 285 | 286 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 287 | *.vbw 288 | 289 | # Visual Studio LightSwitch build output 290 | **/*.HTMLClient/GeneratedArtifacts 291 | **/*.DesktopClient/GeneratedArtifacts 292 | **/*.DesktopClient/ModelManifest.xml 293 | **/*.Server/GeneratedArtifacts 294 | **/*.Server/ModelManifest.xml 295 | _Pvt_Extensions 296 | 297 | # Paket dependency manager 298 | .paket/paket.exe 299 | paket-files/ 300 | 301 | # FAKE - F# Make 302 | .fake/ 303 | 304 | # CodeRush personal settings 305 | .cr/personal 306 | 307 | # Python Tools for Visual Studio (PTVS) 308 | __pycache__/ 309 | *.pyc 310 | 311 | # Cake - Uncomment if you are using it 312 | # tools/** 313 | # !tools/packages.config 314 | 315 | # Tabs Studio 316 | *.tss 317 | 318 | # Telerik's JustMock configuration file 319 | *.jmconfig 320 | 321 | # BizTalk build output 322 | *.btp.cs 323 | *.btm.cs 324 | *.odx.cs 325 | *.xsd.cs 326 | 327 | # OpenCover UI analysis results 328 | OpenCover/ 329 | 330 | # Azure Stream Analytics local run output 331 | ASALocalRun/ 332 | 333 | # MSBuild Binary and Structured Log 334 | *.binlog 335 | 336 | # NVidia Nsight GPU debugger configuration file 337 | *.nvuser 338 | 339 | # MFractors (Xamarin productivity tool) working folder 340 | .mfractor/ 341 | 342 | # Local History for Visual Studio 343 | .localhistory/ 344 | 345 | # BeatPulse healthcheck temp database 346 | healthchecksdb 347 | 348 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 349 | MigrationBackup/ 350 | 351 | # Ionide (cross platform F# VS Code tools) working folder 352 | .ionide/ 353 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.Common/Providers/SearchService.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer.Common.Providers 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Threading.Tasks; 10 | using Microsoft.Azure.Search; 11 | using Microsoft.Azure.Search.Models; 12 | using Microsoft.Extensions.Configuration; 13 | using Microsoft.Teams.Apps.CrowdSourcer.Models; 14 | 15 | /// 16 | /// azure blob search service class. 17 | /// 18 | public class SearchService : ISearchService 19 | { 20 | private const string IndexName = "teams-crowdsourcer-index"; 21 | private const string DataSourceName = "crowdsourcer-datasource"; 22 | private const int TopCount = 50; 23 | 24 | private readonly IConfiguration configuration; 25 | private readonly SearchIndexClient searchIndexClient; 26 | 27 | /// 28 | /// Initializes a new instance of the class. 29 | /// 30 | /// configuration settings. 31 | public SearchService(IConfiguration configuration) 32 | { 33 | this.configuration = configuration; 34 | this.searchIndexClient = new SearchIndexClient( 35 | configuration["SearchServiceName"], 36 | IndexName, 37 | new SearchCredentials(configuration["SearchServiceKey"])); 38 | } 39 | 40 | /// 41 | /// This method gives search result(Konwledgebase QnA pairs) based on teamId and search query for specific messaging extension command Id. 42 | /// 43 | /// searchQuery. 44 | /// commandId. 45 | /// team id. 46 | /// search result list. 47 | public async Task> GetAzureSearchEntitiesAsync(string searchQuery, string commandId, string teamId) 48 | { 49 | IList qnaPairs = new List(); 50 | SearchParameters searchParameters = default(SearchParameters); 51 | string searchFilter = string.Empty; 52 | if (string.IsNullOrWhiteSpace(searchQuery)) 53 | { 54 | switch (commandId) 55 | { 56 | case Constants.CreatedCommandId: 57 | searchParameters = new SearchParameters() 58 | { 59 | OrderBy = new[] { "createddate desc" }, 60 | Top = TopCount, 61 | Filter = $"answer ne '{Constants.Unanswered}' and teamid eq '{teamId}'", 62 | }; 63 | break; 64 | case Constants.EditedCommandId: 65 | searchParameters = new SearchParameters() 66 | { 67 | OrderBy = new[] { "updateddate desc" }, 68 | Top = TopCount, 69 | Filter = $"answer ne '{Constants.Unanswered}' and teamid eq '{teamId}'", 70 | }; 71 | break; 72 | case Constants.UnansweredCommandId: 73 | searchParameters = new SearchParameters() 74 | { 75 | OrderBy = new[] { "createddate desc" }, 76 | Top = TopCount, 77 | Filter = $"answer eq '{Constants.Unanswered}' and teamid eq '{teamId}'", 78 | }; 79 | break; 80 | default: 81 | break; 82 | } 83 | } 84 | else 85 | { 86 | searchParameters = new SearchParameters() 87 | { 88 | OrderBy = new[] { "search.score() desc" }, 89 | Top = TopCount, 90 | Filter = $"teamid eq '{teamId}'", 91 | }; 92 | searchFilter = searchQuery; 93 | } 94 | 95 | var documents = await this.searchIndexClient.Documents.SearchAsync(searchFilter + "*", searchParameters); 96 | if (documents != null) 97 | { 98 | foreach (SearchResult searchResult in documents.Results) 99 | { 100 | qnaPairs.Add(searchResult.Document); 101 | } 102 | } 103 | 104 | return qnaPairs; 105 | } 106 | 107 | /// 108 | /// Creates Index, Data Source and Indexer for search service. 109 | /// 110 | /// task. 111 | public async Task InitializeSearchServiceDependency() 112 | { 113 | ISearchServiceClient searchClient = this.CreateSearchServiceClient(); 114 | await this.CreateSearchIndexAsync(searchClient); 115 | await this.CreateDataSourceAsync(searchClient); 116 | await this.CreateOrRunIndexerAsync(searchClient); 117 | } 118 | 119 | /// 120 | /// this methoda creates search service client. 121 | /// 122 | /// search client. 123 | private SearchServiceClient CreateSearchServiceClient() 124 | { 125 | SearchServiceClient serviceClient = new SearchServiceClient(this.configuration["SearchServiceName"], new SearchCredentials(this.configuration["SearchServiceKey"])); 126 | return serviceClient; 127 | } 128 | 129 | /// 130 | /// Creates new SearchIndex with INDEX_NAME provided, if already exists then delete the index and create again. 131 | /// 132 | /// search client. 133 | private async Task CreateSearchIndexAsync(ISearchServiceClient searchClient) 134 | { 135 | if (await searchClient.Indexes.ExistsAsync(IndexName)) 136 | { 137 | await searchClient.Indexes.DeleteAsync(IndexName); 138 | } 139 | 140 | var definition = new Index() 141 | { 142 | Name = IndexName, 143 | Fields = FieldBuilder.BuildForType(), 144 | }; 145 | await searchClient.Indexes.CreateAsync(definition); 146 | } 147 | 148 | /// 149 | /// Creates new DataSource with DATASOURCE_NAME provided, if already exists no change happen. 150 | /// 151 | /// search client. 152 | private async Task CreateDataSourceAsync(ISearchServiceClient searchClient) 153 | { 154 | if (await searchClient.DataSources.ExistsAsync(DataSourceName)) 155 | { 156 | return; 157 | } 158 | 159 | var dataSourceConfig = new DataSource() 160 | { 161 | Name = DataSourceName, 162 | Container = new DataContainer(Constants.StorageContainer, Constants.FolderName), 163 | Credentials = new DataSourceCredentials(this.configuration["StorageConnectionString"]), 164 | Type = DataSourceType.AzureBlob, 165 | }; 166 | 167 | await searchClient.DataSources.CreateAsync(dataSourceConfig); 168 | } 169 | 170 | /// 171 | /// Creates new Indexer or run if it already exists. 172 | /// 173 | /// search client. 174 | private async Task CreateOrRunIndexerAsync(ISearchServiceClient searchClient) 175 | { 176 | if (await searchClient.Indexers.ExistsAsync(IndexName)) 177 | { 178 | await searchClient.Indexers.RunAsync(IndexName); 179 | return; 180 | } 181 | 182 | var parseConfig = new Dictionary(); 183 | parseConfig.Add("parsingMode", "jsonArray"); 184 | 185 | var indexerConfig = new Indexer() 186 | { 187 | Name = IndexName, 188 | DataSourceName = DataSourceName, 189 | TargetIndexName = IndexName, 190 | Parameters = new IndexingParameters() 191 | { 192 | Configuration = parseConfig, 193 | }, 194 | }; 195 | await searchClient.Indexers.CreateAsync(indexerConfig); 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer/Resources/Strings.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 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 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Ask this question 122 | Button title for Unanswered question card response 123 | 124 | 125 | I didn’t find any answers for this question. Do you want to ask the team? 126 | Bot response text when answer to a question is not found 127 | 128 | 129 | Are you sure you want to delete this item? The question and answer will both be deleted. 130 | Delete QnA pair show card confirmation text 131 | 132 | 133 | Deleted by {0} 134 | Text shown on the card when a QnA pair is deleted 135 | 136 | 137 | Delete 138 | button title for Delete action shown on QnA card 139 | 140 | 141 | Question cannot be empty. 142 | Validation error message when user tries to add/update a QnA pair with blank/empty question text 143 | 144 | 145 | View details 146 | Button title for the card created through Messaging Extension 147 | 148 | 149 | Last edited by {0} 150 | Text shown on the card when a QnA pair is updated 151 | 152 | 153 | Type an answer here, if you have one 154 | Ghost text for Answer text input in QnA show card 155 | 156 | 157 | It seems like the question is not available anymore. 158 | validation message QnA is not present in knowledge base 159 | 160 | 161 | **{0} updated this item.** 162 | audit trail text when a QnA pair is updated 163 | 164 | 165 | Update 166 | button title for Update action shown on QnA card 167 | 168 | 169 | *Items are updated every few minutes.* 170 | Knowledge Base update frequency user suggestive text on QnA card 171 | 172 | 173 | Please wait for some time, the answer for this question: "{0}" will be available in short time 174 | error message when user tries to get answer before it is published 175 | 176 | 177 | Welcome! 178 | I am a QnA bot that helps you and your team collaborate to get answers to your questions in a fun and transparent manner. I work on the principle of tapping the crowd intelligence and collective wisdom of your team members. 179 | 180 | Go ahead ! Ask me a question by @mentioning me followed by your question. 181 | Welcome card text 182 | 183 | 184 | Please wait for 15 minutes and try again. 185 | error message when user tries to delete unpublished qna pair 186 | 187 | 188 | Sorry, it looks like something went wrong. 189 | Generic error message used for uncaught exceptions 190 | 191 | 192 | Answer 193 | card text block title answer 194 | 195 | 196 | Question 197 | card question text block 198 | 199 | 200 | No 201 | card button title no 202 | 203 | 204 | Yes 205 | card button title yes 206 | 207 | 208 | Submit 209 | card button title submit 210 | 211 | 212 | Type a question here 213 | card placeholder question text 214 | 215 | 216 | Ask a question 217 | card button title on welcome card 218 | 219 | 220 | Save 221 | card button title save 222 | 223 | 224 | Add question 225 | task module title 226 | 227 | 228 | No entries found 229 | messaging extension no entries text 230 | 231 | 232 | **Deleted by {0}.** 233 | Audit trail text when QnA pair is deleted 234 | 235 | 236 | {0} at {1} 237 | Used in date string format for adaptive card. {0} is for date and {1} is for time 238 | 239 | 240 | Sorry, this bot can only work in a team and is not for use in personal scope. 241 | Error message to show in case the bot is used in personal scope 242 | 243 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer/Resources/Strings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Microsoft.Teams.Apps.CrowdSourcer.Resources { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Strings { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Strings() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Teams.Apps.CrowdSourcer.Resources.Strings", typeof(Strings).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to Ask this question. 65 | /// 66 | internal static string AddEntryTitle { 67 | get { 68 | return ResourceManager.GetString("AddEntryTitle", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to Add question. 74 | /// 75 | internal static string AddQuestionTaskTitle { 76 | get { 77 | return ResourceManager.GetString("AddQuestionTaskTitle", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to I didn’t find any answers for this question. Do you want to ask the team?. 83 | /// 84 | internal static string AnswerNotFound { 85 | get { 86 | return ResourceManager.GetString("AnswerNotFound", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to Answer. 92 | /// 93 | internal static string AnswerTitle { 94 | get { 95 | return ResourceManager.GetString("AnswerTitle", resourceCulture); 96 | } 97 | } 98 | 99 | /// 100 | /// Looks up a localized string similar to Ask a question. 101 | /// 102 | internal static string AskQuestion { 103 | get { 104 | return ResourceManager.GetString("AskQuestion", resourceCulture); 105 | } 106 | } 107 | 108 | /// 109 | /// Looks up a localized string similar to {0} at {1}. 110 | /// 111 | internal static string DateFormat { 112 | get { 113 | return ResourceManager.GetString("DateFormat", resourceCulture); 114 | } 115 | } 116 | 117 | /// 118 | /// Looks up a localized string similar to Are you sure you want to delete this item? The question and answer will both be deleted.. 119 | /// 120 | internal static string DeleteConfirmation { 121 | get { 122 | return ResourceManager.GetString("DeleteConfirmation", resourceCulture); 123 | } 124 | } 125 | 126 | /// 127 | /// Looks up a localized string similar to Deleted by {0}. 128 | /// 129 | internal static string DeletedQnaPair { 130 | get { 131 | return ResourceManager.GetString("DeletedQnaPair", resourceCulture); 132 | } 133 | } 134 | 135 | /// 136 | /// Looks up a localized string similar to **Deleted by {0}.**. 137 | /// 138 | internal static string DeletedQnaPairBold { 139 | get { 140 | return ResourceManager.GetString("DeletedQnaPairBold", resourceCulture); 141 | } 142 | } 143 | 144 | /// 145 | /// Looks up a localized string similar to Delete. 146 | /// 147 | internal static string DeleteEntryTitle { 148 | get { 149 | return ResourceManager.GetString("DeleteEntryTitle", resourceCulture); 150 | } 151 | } 152 | 153 | /// 154 | /// Looks up a localized string similar to Question cannot be empty.. 155 | /// 156 | internal static string EmptyQnaValidation { 157 | get { 158 | return ResourceManager.GetString("EmptyQnaValidation", resourceCulture); 159 | } 160 | } 161 | 162 | /// 163 | /// Looks up a localized string similar to Sorry, it looks like something went wrong.. 164 | /// 165 | internal static string ErrorMsgText { 166 | get { 167 | return ResourceManager.GetString("ErrorMsgText", resourceCulture); 168 | } 169 | } 170 | 171 | /// 172 | /// Looks up a localized string similar to View details. 173 | /// 174 | internal static string GoToThread { 175 | get { 176 | return ResourceManager.GetString("GoToThread", resourceCulture); 177 | } 178 | } 179 | 180 | /// 181 | /// Looks up a localized string similar to Last edited by {0}. 182 | /// 183 | internal static string LastEdited { 184 | get { 185 | return ResourceManager.GetString("LastEdited", resourceCulture); 186 | } 187 | } 188 | 189 | /// 190 | /// Looks up a localized string similar to No. 191 | /// 192 | internal static string No { 193 | get { 194 | return ResourceManager.GetString("No", resourceCulture); 195 | } 196 | } 197 | 198 | /// 199 | /// Looks up a localized string similar to No entries found. 200 | /// 201 | internal static string NoEntriesFound { 202 | get { 203 | return ResourceManager.GetString("NoEntriesFound", resourceCulture); 204 | } 205 | } 206 | 207 | /// 208 | /// Looks up a localized string similar to Sorry, this bot can only work in a team and is not for use in personal scope.. 209 | /// 210 | internal static string NotInScope { 211 | get { 212 | return ResourceManager.GetString("NotInScope", resourceCulture); 213 | } 214 | } 215 | 216 | /// 217 | /// Looks up a localized string similar to Type an answer here, if you have one. 218 | /// 219 | internal static string PlaceholderAnswer { 220 | get { 221 | return ResourceManager.GetString("PlaceholderAnswer", resourceCulture); 222 | } 223 | } 224 | 225 | /// 226 | /// Looks up a localized string similar to Type a question here. 227 | /// 228 | internal static string PlaceholderQuestion { 229 | get { 230 | return ResourceManager.GetString("PlaceholderQuestion", resourceCulture); 231 | } 232 | } 233 | 234 | /// 235 | /// Looks up a localized string similar to It seems like the question is not available anymore.. 236 | /// 237 | internal static string QuestionNotAvailable { 238 | get { 239 | return ResourceManager.GetString("QuestionNotAvailable", resourceCulture); 240 | } 241 | } 242 | 243 | /// 244 | /// Looks up a localized string similar to Question. 245 | /// 246 | internal static string QuestionTitle { 247 | get { 248 | return ResourceManager.GetString("QuestionTitle", resourceCulture); 249 | } 250 | } 251 | 252 | /// 253 | /// Looks up a localized string similar to Save. 254 | /// 255 | internal static string Save { 256 | get { 257 | return ResourceManager.GetString("Save", resourceCulture); 258 | } 259 | } 260 | 261 | /// 262 | /// Looks up a localized string similar to Submit. 263 | /// 264 | internal static string SubmitTitle { 265 | get { 266 | return ResourceManager.GetString("SubmitTitle", resourceCulture); 267 | } 268 | } 269 | 270 | /// 271 | /// Looks up a localized string similar to **{0} updated this item.**. 272 | /// 273 | internal static string UpdatedQnaPair { 274 | get { 275 | return ResourceManager.GetString("UpdatedQnaPair", resourceCulture); 276 | } 277 | } 278 | 279 | /// 280 | /// Looks up a localized string similar to Update. 281 | /// 282 | internal static string UpdateEntryTitle { 283 | get { 284 | return ResourceManager.GetString("UpdateEntryTitle", resourceCulture); 285 | } 286 | } 287 | 288 | /// 289 | /// Looks up a localized string similar to Please wait for 15 minutes and try again.. 290 | /// 291 | internal static string WaitMessage { 292 | get { 293 | return ResourceManager.GetString("WaitMessage", resourceCulture); 294 | } 295 | } 296 | 297 | /// 298 | /// Looks up a localized string similar to *Items are updated every few minutes.*. 299 | /// 300 | internal static string WaitMessageAnswer { 301 | get { 302 | return ResourceManager.GetString("WaitMessageAnswer", resourceCulture); 303 | } 304 | } 305 | 306 | /// 307 | /// Looks up a localized string similar to Please wait for some time, the answer for this question: "{0}" will be available in short time. 308 | /// 309 | internal static string WaitMessageQuestion { 310 | get { 311 | return ResourceManager.GetString("WaitMessageQuestion", resourceCulture); 312 | } 313 | } 314 | 315 | /// 316 | /// Looks up a localized string similar to Welcome! 317 | ///I am a QnA bot that helps you and your team collaborate to get answers to your questions in a fun and transparent manner. I work on the principle of tapping the crowd intelligence and collective wisdom of your team members. 318 | /// 319 | ///Go ahead ! Ask me a question by @mentioning me followed by your question.. 320 | /// 321 | internal static string WelcomeMessage { 322 | get { 323 | return ResourceManager.GetString("WelcomeMessage", resourceCulture); 324 | } 325 | } 326 | 327 | /// 328 | /// Looks up a localized string similar to Yes. 329 | /// 330 | internal static string Yes { 331 | get { 332 | return ResourceManager.GetString("Yes", resourceCulture); 333 | } 334 | } 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer.Common/Providers/QnaServiceProvider.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer.Common.Providers 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Globalization; 10 | using System.Linq; 11 | using System.Threading.Tasks; 12 | using System.Web; 13 | using Microsoft.Azure.CognitiveServices.Knowledge.QnAMaker; 14 | using Microsoft.Azure.CognitiveServices.Knowledge.QnAMaker.Models; 15 | using Microsoft.Extensions.Configuration; 16 | 17 | /// 18 | /// qna maker service provider class. 19 | /// 20 | public class QnaServiceProvider : IQnaServiceProvider 21 | { 22 | /// 23 | /// The amount of time delay before checking the operation status details again. 24 | /// 25 | private const int OperationDelayMilliseconds = 5000; 26 | 27 | /// 28 | /// Retry count to check the operation status, if it is 'NotStarted' or 'Running'. 29 | /// 30 | private const int OperationRetryCount = 10; 31 | private const string DummyQuestion = "dummyquestion"; 32 | private const string DummyAnswer = "dummyanswer"; 33 | private const string DummyMetadataTeamId = "dummy"; 34 | private const string Source = "Bot"; 35 | private const string KbName = "teamscrowdsourcer"; 36 | private const string Environment = "Prod"; 37 | 38 | private readonly double scoreThreshold; 39 | private readonly IQnAMakerClient qnaMakerClient; 40 | private readonly IQnAMakerRuntimeClient qnaMakerRuntimeClient; 41 | private readonly IConfigurationStorageProvider configurationStorageProvider; 42 | 43 | /// 44 | /// Initializes a new instance of the class. 45 | /// 46 | /// storage provider. 47 | /// configuration. 48 | /// qna service client. 49 | /// qna service runtime client. 50 | public QnaServiceProvider(IConfigurationStorageProvider configurationStorageProvider, IConfiguration configuration, IQnAMakerClient qnaMakerClient, IQnAMakerRuntimeClient qnaMakerRuntimeClient) 51 | { 52 | this.configurationStorageProvider = configurationStorageProvider; 53 | this.qnaMakerClient = qnaMakerClient; 54 | this.qnaMakerRuntimeClient = qnaMakerRuntimeClient; 55 | this.scoreThreshold = Convert.ToDouble(configuration["ScoreThreshold"]); 56 | } 57 | 58 | /// 59 | /// Initializes a new instance of the class. 60 | /// 61 | /// storage provider. 62 | /// qna client. 63 | public QnaServiceProvider(IConfigurationStorageProvider configurationStorageProvider, IQnAMakerClient qnaMakerClient) 64 | { 65 | this.configurationStorageProvider = configurationStorageProvider; 66 | this.qnaMakerClient = qnaMakerClient; 67 | } 68 | 69 | /// 70 | /// this method is used to add QnA pair in Kb. 71 | /// 72 | /// question text. 73 | /// answer text. 74 | /// created by user AAD object Id. 75 | /// team id. 76 | /// conversation id. 77 | /// task. 78 | public async Task AddQnaAsync(string question, string answer, string createdBy, string teamId, string conversationId) 79 | { 80 | var kb = await this.configurationStorageProvider.GetKbConfigAsync(); 81 | 82 | // Update kb 83 | var updateKbOperation = await this.qnaMakerClient.Knowledgebase.UpdateAsync(kb.KbId, new UpdateKbOperationDTO 84 | { 85 | // Create JSON of changes. 86 | Add = new UpdateKbOperationDTOAdd 87 | { 88 | QnaList = new List 89 | { 90 | new QnADTO 91 | { 92 | Questions = new List { question }, 93 | Answer = answer, 94 | Source = Source, 95 | Metadata = new List() 96 | { 97 | new MetadataDTO() { Name = Constants.MetadataCreatedAt, Value = DateTime.UtcNow.Ticks.ToString("G", CultureInfo.InvariantCulture) }, 98 | new MetadataDTO() { Name = Constants.MetadataCreatedBy, Value = createdBy }, 99 | new MetadataDTO() { Name = Constants.MetadataTeamId, Value = HttpUtility.UrlEncode(teamId) }, 100 | new MetadataDTO() { Name = Constants.MetadataConversationId, Value = HttpUtility.UrlEncode(conversationId) }, 101 | }, 102 | }, 103 | }, 104 | }, 105 | }); 106 | } 107 | 108 | /// 109 | /// this method is used to update Qna pair in Kb. 110 | /// 111 | /// question id. 112 | /// answer text. 113 | /// updated by user AAD object Id. 114 | /// updated question text. 115 | /// original question text. 116 | /// team id. 117 | /// task. 118 | public async Task UpdateQnaAsync(int questionId, string answer, string updatedBy, string updatedQuestion, string question, string teamId) 119 | { 120 | var kb = await this.configurationStorageProvider.GetKbConfigAsync(); 121 | var questions = default(UpdateQnaDTOQuestions); 122 | if (!string.IsNullOrEmpty(updatedQuestion)) 123 | { 124 | questions = (updatedQuestion == question) ? null 125 | : new UpdateQnaDTOQuestions() 126 | { 127 | Add = new List { updatedQuestion }, 128 | Delete = new List { question }, 129 | }; 130 | } 131 | 132 | if (string.IsNullOrEmpty(answer)) 133 | { 134 | answer = Constants.Unanswered; 135 | } 136 | 137 | // Update kb 138 | var updateKbOperation = await this.qnaMakerClient.Knowledgebase.UpdateAsync(kb.KbId, new UpdateKbOperationDTO 139 | { 140 | // Create JSON of changes. 141 | Update = new UpdateKbOperationDTOUpdate() 142 | { 143 | QnaList = new List() 144 | { 145 | new UpdateQnaDTO() 146 | { 147 | Id = questionId, 148 | Source = Source, 149 | Answer = answer, 150 | Questions = questions, 151 | Metadata = new UpdateQnaDTOMetadata() 152 | { 153 | Add = new List() 154 | { 155 | new MetadataDTO() { Name = Constants.MetadataUpdatedAt, Value = DateTime.UtcNow.Ticks.ToString("G", CultureInfo.InvariantCulture) }, 156 | new MetadataDTO() { Name = Constants.MetadataUpdatedBy, Value = updatedBy }, 157 | }, 158 | }, 159 | }, 160 | }, 161 | }, 162 | }); 163 | } 164 | 165 | /// 166 | /// this method is used to delete Qna pair from KB. 167 | /// 168 | /// question id. 169 | /// team id. 170 | /// task. 171 | public async Task DeleteQnaAsync(int questionId, string teamId) 172 | { 173 | var kb = await this.configurationStorageProvider.GetKbConfigAsync(); 174 | 175 | // to delete a qna based on id. 176 | var updateKbOperation = await this.qnaMakerClient.Knowledgebase.UpdateAsync(kb.KbId, new UpdateKbOperationDTO 177 | { 178 | // Create JSON of changes. 179 | Delete = new UpdateKbOperationDTODelete() 180 | { 181 | Ids = new List() { questionId }, 182 | }, 183 | }); 184 | } 185 | 186 | /// 187 | /// get answer from kb for a given question. 188 | /// 189 | /// prod or test. 190 | /// question text. 191 | /// team id. 192 | /// qnaSearchResult response. 193 | public async Task GenerateAnswerAsync(bool isTest, string question, string teamId) 194 | { 195 | var kb = await this.configurationStorageProvider.GetKbConfigAsync(); 196 | 197 | QnASearchResultList qnaSearchResult = await this.qnaMakerRuntimeClient.Runtime.GenerateAnswerAsync(kb?.KbId, new QueryDTO() 198 | { 199 | IsTest = isTest, 200 | Question = question, 201 | ScoreThreshold = this.scoreThreshold, 202 | StrictFilters = new List { new MetadataDTO() { Name = Constants.MetadataTeamId, Value = HttpUtility.UrlEncode(teamId) } }, 203 | }); 204 | 205 | return qnaSearchResult; 206 | } 207 | 208 | /// 209 | /// this method can be used to publish the Kb. 210 | /// 211 | /// kb id. 212 | /// task. 213 | public async Task PublishKnowledgebaseAsync(string kbId) 214 | { 215 | await this.qnaMakerClient.Knowledgebase.PublishAsync(kbId); 216 | } 217 | 218 | /// 219 | /// this method is used to create knowledgebase. 220 | /// 221 | /// kb id. 222 | public async Task CreateKnowledgeBaseAsync() 223 | { 224 | // Adding one qna pair is mandatory for creating a knowledgebase. So giving dummy values. 225 | // We filter the qna pairs from knowledgebase based on teamid metadata value, so the dummy entry will be ignored. 226 | var createOp = await this.qnaMakerClient.Knowledgebase.CreateAsync(new CreateKbDTO() 227 | { 228 | Name = KbName, 229 | QnaList = new List() 230 | { 231 | new QnADTO() 232 | { 233 | Answer = DummyAnswer, 234 | Questions = new List() { DummyQuestion }, 235 | Source = Source, 236 | Metadata = new List() 237 | { 238 | new MetadataDTO() 239 | { 240 | Name = Constants.MetadataTeamId, 241 | Value = DummyMetadataTeamId, 242 | }, 243 | }, 244 | }, 245 | }, 246 | }); 247 | 248 | createOp = await this.MonitorOperationAsync(createOp); 249 | return createOp?.ResourceLocation?.Split('/').Last(); 250 | } 251 | 252 | /// 253 | /// Checks whether knowledge base need to be published. 254 | /// 255 | /// Knowledgebase ID. 256 | /// boolean variable to publish or not. 257 | public async Task GetPublishStatusAsync(string kbId) 258 | { 259 | KnowledgebaseDTO knowledgebaseDetails = await this.qnaMakerClient.Knowledgebase.GetDetailsAsync(kbId); 260 | if (knowledgebaseDetails.LastChangedTimestamp != null && knowledgebaseDetails.LastPublishedTimestamp != null) 261 | { 262 | return Convert.ToDateTime(knowledgebaseDetails.LastChangedTimestamp) > Convert.ToDateTime(knowledgebaseDetails.LastPublishedTimestamp); 263 | } 264 | else 265 | { 266 | return true; 267 | } 268 | } 269 | 270 | /// 271 | /// this method returns the downloaded kb documents. 272 | /// 273 | /// knowledgebase Id. 274 | /// json string. 275 | public async Task> DownloadKnowledgebaseAsync(string kbId) 276 | { 277 | var qnaDocuments = await this.qnaMakerClient.Knowledgebase.DownloadAsync(kbId, environment: Environment); 278 | return qnaDocuments.QnaDocuments; 279 | } 280 | 281 | /// 282 | /// This method is used to delete a knowledgebase. 283 | /// 284 | /// knowledgebase Id. 285 | /// task. 286 | public async Task DeleteKnowledgebaseAsync(string kbId) 287 | { 288 | await this.qnaMakerClient.Knowledgebase.DeleteAsync(kbId); 289 | } 290 | 291 | /// 292 | /// this method can be used to monitor any qnamaker operation. 293 | /// 294 | /// operation details. 295 | /// operation. 296 | private async Task MonitorOperationAsync(Operation operation) 297 | { 298 | // Loop while operation is success 299 | for (int i = 0; 300 | i < OperationRetryCount && (operation.OperationState == OperationStateType.NotStarted || operation.OperationState == OperationStateType.Running); 301 | i++) 302 | { 303 | await Task.Delay(OperationDelayMilliseconds); 304 | operation = await this.qnaMakerClient.Operations.GetDetailsAsync(operation.OperationId); 305 | } 306 | 307 | if (operation.OperationState != OperationStateType.Succeeded) 308 | { 309 | throw new Exception($"Operation {operation.OperationId} failed. Error {operation.ErrorResponse.Error.Message}"); 310 | } 311 | 312 | return operation; 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /Source/Microsoft.Teams.Apps.CrowdSourcer/Cards/CrowdSourcerCards.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.Teams.Apps.CrowdSourcer.Cards 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | using System.Web; 12 | using AdaptiveCards; 13 | using Microsoft.Bot.Schema; 14 | using Microsoft.Bot.Schema.Teams; 15 | using Microsoft.Teams.Apps.CrowdSourcer.Common; 16 | using Microsoft.Teams.Apps.CrowdSourcer.Common.Providers; 17 | using Microsoft.Teams.Apps.CrowdSourcer.Models; 18 | using Microsoft.Teams.Apps.CrowdSourcer.Resources; 19 | 20 | /// 21 | /// create crowdsourcer cards. 22 | /// 23 | public class CrowdSourcerCards 24 | { 25 | private const int TruncateThresholdLength = 50; 26 | private const int QuestionMaxInputLength = 100; 27 | private const int AnswerMaxInputLength = 500; 28 | private readonly IObjectIdToNameMapper nameMappingStorageProvider; 29 | 30 | /// 31 | /// Initializes a new instance of the class. 32 | /// 33 | /// name id mapping storage provider. 34 | public CrowdSourcerCards(IObjectIdToNameMapper nameMappingStorageProvider) 35 | { 36 | this.nameMappingStorageProvider = nameMappingStorageProvider; 37 | } 38 | 39 | /// 40 | /// returns the messaging extension attachment of all answers. 41 | /// 42 | /// all qnaDocuments. 43 | /// returns the list of all answered questions. 44 | public async Task> MessagingExtensionCardListAsync(IList qnaDocuments) 45 | { 46 | var messagingExtensionAttachments = new List(); 47 | 48 | foreach (var qnaDoc in qnaDocuments) 49 | { 50 | DateTime createdAt = (qnaDoc.Metadata.Count > 1) ? new DateTime(long.Parse(qnaDoc.Metadata.Where(s => s.Name == Constants.MetadataCreatedAt).First().Value)) : default; 51 | string dateString = string.Empty; 52 | string createdBy = string.Empty; 53 | string conversationId = string.Empty; 54 | 55 | if (qnaDoc.Metadata?.Count > 1) 56 | { 57 | string objectId = qnaDoc.Metadata.Where(x => x.Name == Constants.MetadataCreatedBy).First().Value; 58 | createdBy = await this.nameMappingStorageProvider.GetNameAsync(objectId); 59 | conversationId = qnaDoc.Metadata.Where(s => s.Name == Constants.MetadataConversationId).First().Value; 60 | dateString = string.Format(Strings.DateFormat, "{{DATE(" + createdAt.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'") + ", SHORT)}}", "{{TIME(" + createdAt.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") + ")}}"); 61 | } 62 | 63 | string answer = qnaDoc.Answer.Equals(Constants.Unanswered) ? string.Empty : qnaDoc.Answer; 64 | 65 | var card = new AdaptiveCard("1.0") 66 | { 67 | Body = new List 68 | { 69 | new AdaptiveTextBlock 70 | { 71 | Text = $"**{Strings.QuestionTitle}**: {qnaDoc.Questions[0]}", 72 | Size = AdaptiveTextSize.Default, 73 | Wrap = true, 74 | }, 75 | new AdaptiveTextBlock 76 | { 77 | Text = string.IsNullOrEmpty(answer) ? string.Empty : $"**{Strings.AnswerTitle}**: {answer}", 78 | Size = AdaptiveTextSize.Default, 79 | Wrap = true, 80 | }, 81 | new AdaptiveTextBlock 82 | { 83 | Text = $"{createdBy} | {dateString}", 84 | Wrap = true, 85 | }, 86 | }, 87 | }; 88 | 89 | // If conversation id is not "#" whose url decode value is "%23" then create go to thread url. 90 | if (!conversationId.Equals("%23")) 91 | { 92 | conversationId = HttpUtility.UrlDecode(conversationId); 93 | string[] threadAndMessageId = conversationId.Split(";"); 94 | var threadId = threadAndMessageId[0]; 95 | var messageId = threadAndMessageId[1].Split("=")[1]; 96 | 97 | card.Actions.Add( 98 | new AdaptiveOpenUrlAction() 99 | { 100 | Title = Strings.GoToThread, 101 | Url = new Uri($"https://teams.microsoft.com/l/message/{threadId}/{messageId}"), 102 | }); 103 | } 104 | 105 | string truncatedAnswer = answer.Length <= TruncateThresholdLength ? answer : answer.Substring(0, 45) + "..."; 106 | 107 | ThumbnailCard previewCard = new ThumbnailCard 108 | { 109 | Title = $"{HttpUtility.HtmlEncode(qnaDoc.Questions[0])}", 110 | Text = $"{HttpUtility.HtmlEncode(truncatedAnswer)}
{HttpUtility.HtmlEncode(createdBy)} | {HttpUtility.HtmlEncode(createdAt)}
", 111 | }; 112 | 113 | messagingExtensionAttachments.Add(new Attachment 114 | { 115 | ContentType = AdaptiveCard.ContentType, 116 | Content = card, 117 | }.ToMessagingExtensionAttachment(previewCard.ToAttachment())); 118 | } 119 | 120 | return messagingExtensionAttachments; 121 | } 122 | 123 | /// 124 | /// no answer found card. 125 | /// 126 | /// question. 127 | /// attachment. 128 | public Attachment NoAnswerCard(string question) 129 | { 130 | AdaptiveCard card = new AdaptiveCard("1.0"); 131 | var container = new AdaptiveContainer() 132 | { 133 | Items = new List 134 | { 135 | new AdaptiveTextBlock 136 | { 137 | Size = AdaptiveTextSize.Default, 138 | Wrap = true, 139 | Text = Strings.AnswerNotFound, 140 | }, 141 | }, 142 | }; 143 | card.Body.Add(container); 144 | card.Actions.Add( 145 | new AdaptiveShowCardAction() 146 | { 147 | Title = Strings.AddEntryTitle, 148 | Card = this.UpdateEntry(question), 149 | }); 150 | var adaptiveCardAttachment = new Attachment() 151 | { 152 | ContentType = AdaptiveCard.ContentType, 153 | Content = card, 154 | }; 155 | 156 | return adaptiveCardAttachment; 157 | } 158 | 159 | /// 160 | /// welcome card. 161 | /// 162 | /// attachment. 163 | public Attachment WelcomeCard() 164 | { 165 | var card = new AdaptiveCard("1.0") 166 | { 167 | Body = new List 168 | { 169 | new AdaptiveTextBlock 170 | { 171 | Text = Strings.WelcomeMessage, 172 | Size = AdaptiveTextSize.Default, 173 | Wrap = true, 174 | }, 175 | }, 176 | }; 177 | 178 | card.Actions.Add( 179 | new AdaptiveSubmitAction() 180 | { 181 | Title = Strings.AskQuestion, 182 | Data = new AdaptiveSubmitActionData 183 | { 184 | MsTeams = new CardAction 185 | { 186 | Type = "task/fetch", 187 | }, 188 | }, 189 | }); 190 | 191 | return new Attachment 192 | { 193 | ContentType = AdaptiveCard.ContentType, 194 | Content = card, 195 | }; 196 | } 197 | 198 | /// 199 | /// updated answer card. 200 | /// 201 | /// question. 202 | /// answer. 203 | /// editedby. 204 | /// boolean environment. 205 | /// attachment. 206 | public Attachment AddedAnswer(string question, string answer, string editedBy, bool isTest) 207 | { 208 | if (!string.IsNullOrWhiteSpace(answer)) 209 | { 210 | answer = answer.Equals(Constants.Unanswered) ? string.Empty : answer; 211 | } 212 | 213 | AdaptiveCard card = new AdaptiveCard("1.0"); 214 | var container = new AdaptiveContainer() 215 | { 216 | Items = new List 217 | { 218 | new AdaptiveTextBlock 219 | { 220 | Size = AdaptiveTextSize.Default, 221 | Wrap = true, 222 | Text = $"**{Strings.QuestionTitle}:** {question}", 223 | }, 224 | new AdaptiveTextBlock 225 | { 226 | Size = AdaptiveTextSize.Default, 227 | Wrap = true, 228 | Text = string.IsNullOrWhiteSpace(answer) ? answer : $"**{Strings.AnswerTitle}:** {answer}", 229 | }, 230 | new AdaptiveTextBlock 231 | { 232 | Size = AdaptiveTextSize.Small, 233 | Wrap = true, 234 | Text = string.Format(Strings.LastEdited, editedBy), 235 | }, 236 | }, 237 | }; 238 | 239 | if (isTest) 240 | { 241 | container.Items.Add(new AdaptiveTextBlock 242 | { 243 | Size = AdaptiveTextSize.Small, 244 | Wrap = true, 245 | Text = Strings.WaitMessageAnswer, 246 | }); 247 | } 248 | 249 | card.Body.Add(container); 250 | 251 | card.Actions.Add( 252 | new AdaptiveShowCardAction() 253 | { 254 | Title = Strings.UpdateEntryTitle, 255 | Card = this.UpdateEntry(question, answer), 256 | }); 257 | 258 | card.Actions.Add( 259 | new AdaptiveShowCardAction() 260 | { 261 | Title = Strings.DeleteEntryTitle, 262 | Card = this.DeleteEntry(question), 263 | }); 264 | 265 | var adaptiveCardAttachment = new Attachment() 266 | { 267 | ContentType = AdaptiveCard.ContentType, 268 | Content = card, 269 | }; 270 | 271 | return adaptiveCardAttachment; 272 | } 273 | 274 | /// 275 | /// update toggle card. 276 | /// 277 | /// question. 278 | /// card. 279 | public AdaptiveCard AddQuestionAnswer() 280 | { 281 | AdaptiveCard card = new AdaptiveCard("1.0"); 282 | var container = new AdaptiveContainer() 283 | { 284 | Items = new List 285 | { 286 | new AdaptiveTextBlock 287 | { 288 | Text = Strings.QuestionTitle, 289 | Size = AdaptiveTextSize.Small, 290 | }, 291 | new AdaptiveTextInput 292 | { 293 | Id = "question", 294 | Placeholder = Strings.PlaceholderQuestion, 295 | MaxLength = QuestionMaxInputLength, 296 | Style = AdaptiveTextInputStyle.Text, 297 | }, 298 | new AdaptiveTextBlock 299 | { 300 | Text = Strings.AnswerTitle, 301 | Size = AdaptiveTextSize.Small, 302 | }, 303 | new AdaptiveTextInput 304 | { 305 | Id = "answer", 306 | Placeholder = Strings.PlaceholderAnswer, 307 | IsMultiline = true, 308 | MaxLength = AnswerMaxInputLength, 309 | Style = AdaptiveTextInputStyle.Text, 310 | }, 311 | }, 312 | }; 313 | card.Body.Add(container); 314 | 315 | card.Actions.Add( 316 | new AdaptiveSubmitAction() 317 | { 318 | Title = Strings.Save, 319 | Data = new AdaptiveSubmitActionData 320 | { 321 | MsTeams = new CardAction 322 | { 323 | Type = ActionTypes.MessageBack, 324 | Text = Constants.SubmitAddCommand, 325 | }, 326 | }, 327 | }); 328 | 329 | return card; 330 | } 331 | 332 | /// 333 | /// update toggle card. 334 | /// 335 | /// question. 336 | /// answer. 337 | /// card. 338 | public AdaptiveCard UpdateEntry(string question, string answer = "") 339 | { 340 | AdaptiveCard card = new AdaptiveCard("1.0"); 341 | var container = new AdaptiveContainer() 342 | { 343 | Items = new List 344 | { 345 | new AdaptiveTextBlock 346 | { 347 | Text = Strings.QuestionTitle, 348 | Size = AdaptiveTextSize.Small, 349 | }, 350 | new AdaptiveTextInput 351 | { 352 | Id = "question", 353 | MaxLength = QuestionMaxInputLength, 354 | Placeholder = Strings.PlaceholderQuestion, 355 | Style = AdaptiveTextInputStyle.Text, 356 | Value = question, 357 | }, 358 | new AdaptiveTextBlock 359 | { 360 | Text = Strings.AnswerTitle, 361 | Size = AdaptiveTextSize.Small, 362 | }, 363 | new AdaptiveTextInput 364 | { 365 | Id = "answer", 366 | Placeholder = Strings.PlaceholderAnswer, 367 | IsMultiline = true, 368 | MaxLength = AnswerMaxInputLength, 369 | Style = AdaptiveTextInputStyle.Text, 370 | Value = answer, 371 | }, 372 | }, 373 | }; 374 | card.Body.Add(container); 375 | 376 | card.Actions.Add( 377 | new AdaptiveSubmitAction() 378 | { 379 | Title = Strings.Save, 380 | Data = new AdaptiveSubmitActionData 381 | { 382 | MsTeams = new CardAction 383 | { 384 | Type = ActionTypes.MessageBack, 385 | Text = Constants.SaveCommand, 386 | }, 387 | Details = new Details() { Question = question }, 388 | }, 389 | }); 390 | 391 | return card; 392 | } 393 | 394 | /// 395 | /// delete toggle card. 396 | /// 397 | /// question. 398 | /// card. 399 | public AdaptiveCard DeleteEntry(string question) 400 | { 401 | AdaptiveCard card = new AdaptiveCard("1.0"); 402 | var container = new AdaptiveContainer() 403 | { 404 | Items = new List 405 | { 406 | new AdaptiveTextBlock 407 | { 408 | Text = Strings.DeleteConfirmation, 409 | Wrap = true, 410 | }, 411 | }, 412 | }; 413 | card.Body.Add(container); 414 | 415 | card.Actions.Add( 416 | new AdaptiveSubmitAction() 417 | { 418 | Title = Strings.Yes, 419 | Data = new AdaptiveSubmitActionData 420 | { 421 | MsTeams = new CardAction 422 | { 423 | Type = ActionTypes.MessageBack, 424 | Text = Constants.DeleteCommand, 425 | }, 426 | Details = new Details() { Question = question }, 427 | }, 428 | }); 429 | 430 | card.Actions.Add( 431 | new AdaptiveSubmitAction() 432 | { 433 | Title = Strings.No, 434 | Data = new AdaptiveSubmitActionData 435 | { 436 | MsTeams = new CardAction 437 | { 438 | Type = ActionTypes.MessageBack, 439 | Text = Constants.NoCommand, 440 | }, 441 | }, 442 | }); 443 | 444 | return card; 445 | } 446 | 447 | /// 448 | /// deleted item card. 449 | /// 450 | /// question. 451 | /// answer. 452 | /// deleted by user. 453 | /// card. 454 | public Attachment DeletedEntry(string question, string answer, string deletedBy) 455 | { 456 | AdaptiveCard card = new AdaptiveCard("1.0"); 457 | var container = new AdaptiveContainer() 458 | { 459 | Items = new List 460 | { 461 | new AdaptiveTextBlock 462 | { 463 | Size = AdaptiveTextSize.Default, 464 | Wrap = true, 465 | Text = $"**{Strings.QuestionTitle}:** {question}", 466 | }, 467 | new AdaptiveTextBlock 468 | { 469 | Size = AdaptiveTextSize.Default, 470 | Wrap = true, 471 | Text = $"**{Strings.AnswerTitle}:** {answer}", 472 | }, 473 | new AdaptiveTextBlock 474 | { 475 | Size = AdaptiveTextSize.Small, 476 | Wrap = true, 477 | Text = string.Format(Strings.DeletedQnaPair, deletedBy), 478 | }, 479 | new AdaptiveTextBlock 480 | { 481 | Size = AdaptiveTextSize.Small, 482 | Wrap = true, 483 | Text = Strings.WaitMessageAnswer, 484 | }, 485 | }, 486 | }; 487 | card.Body.Add(container); 488 | 489 | var adaptiveCardAttachment = new Attachment() 490 | { 491 | ContentType = AdaptiveCard.ContentType, 492 | Content = card, 493 | }; 494 | 495 | return adaptiveCardAttachment; 496 | } 497 | 498 | /// 499 | /// Add question card task module. 500 | /// 501 | /// validation flag. 502 | /// card. 503 | public Attachment AddQuestionActionCard(bool isValid) 504 | { 505 | AdaptiveCard card = new AdaptiveCard("1.0"); 506 | var container = new AdaptiveContainer() 507 | { 508 | Items = new List 509 | { 510 | new AdaptiveTextBlock 511 | { 512 | Text = Strings.QuestionTitle, 513 | Size = AdaptiveTextSize.Small, 514 | }, 515 | new AdaptiveTextInput 516 | { 517 | Id = "question", 518 | Placeholder = Strings.PlaceholderQuestion, 519 | MaxLength = QuestionMaxInputLength, 520 | Style = AdaptiveTextInputStyle.Text, 521 | }, 522 | new AdaptiveTextBlock 523 | { 524 | Text = Strings.AnswerTitle, 525 | Size = AdaptiveTextSize.Small, 526 | }, 527 | new AdaptiveTextInput 528 | { 529 | Id = "answer", 530 | Placeholder = Strings.PlaceholderAnswer, 531 | IsMultiline = true, 532 | MaxLength = AnswerMaxInputLength, 533 | Style = AdaptiveTextInputStyle.Text, 534 | }, 535 | }, 536 | }; 537 | 538 | if (!isValid) 539 | { 540 | container.Items.Add(new AdaptiveTextBlock 541 | { 542 | Text = Strings.EmptyQnaValidation, 543 | Size = AdaptiveTextSize.Small, 544 | Color = AdaptiveTextColor.Attention, 545 | }); 546 | } 547 | 548 | card.Body.Add(container); 549 | 550 | card.Actions.Add( 551 | new AdaptiveSubmitAction() 552 | { 553 | Title = Strings.SubmitTitle, 554 | }); 555 | 556 | var adaptiveCardAttachment = new Attachment() 557 | { 558 | ContentType = AdaptiveCard.ContentType, 559 | Content = card, 560 | }; 561 | 562 | return adaptiveCardAttachment; 563 | } 564 | } 565 | } 566 | -------------------------------------------------------------------------------- /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 | "minLength": 1, 7 | "type": "string", 8 | "metadata": { 9 | "description": "The base name to use for the resources that will be provisioned." 10 | } 11 | }, 12 | "botClientId": { 13 | "minLength": 36, 14 | "maxLength": 36, 15 | "type": "string", 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 | "minLength": 1, 22 | "type": "SecureString", 23 | "metadata": { 24 | "description": "The client secret of the bot Azure AD app." 25 | } 26 | }, 27 | "appDisplayName": { 28 | "defaultValue": "Crowdsourcer", 29 | "minLength": 1, 30 | "type": "string", 31 | "metadata": { 32 | "description": "The app (and bot) display name." 33 | } 34 | }, 35 | "appDescription": { 36 | "defaultValue": "QnA bot that works on the concept of crowdsourcing information in teams.", 37 | "minLength": 1, 38 | "type": "string", 39 | "metadata": { 40 | "description": "The app (and bot) description." 41 | } 42 | }, 43 | "appIconUrl": { 44 | "defaultValue": "https://raw.githubusercontent.com/OfficeDev/microsoft-teams-crowdsourcer-app/master/Manifest/color.png", 45 | "minLength": 1, 46 | "type": "string", 47 | "metadata": { 48 | "description": "The link to the icon for the app. It must resolve to a PNG file." 49 | } 50 | }, 51 | "tenantId": { 52 | "defaultValue": "[subscription().tenantId]", 53 | "minLength": 1, 54 | "maxLength": 36, 55 | "type": "string", 56 | "metadata": { 57 | "description": "The ID of the tenant to which the app will be deployed." 58 | } 59 | }, 60 | "sku": { 61 | "defaultValue": "Standard", 62 | "allowedValues": [ 63 | "Standard", 64 | "Premium" 65 | ], 66 | "type": "string", 67 | "metadata": { 68 | "description": "The pricing tier for the hosting plan." 69 | } 70 | }, 71 | "planSize": { 72 | "defaultValue": "1", 73 | "allowedValues": [ 74 | "1", 75 | "2", 76 | "3" 77 | ], 78 | "type": "string", 79 | "metadata": { 80 | "description": "The size of the hosting plan (small, medium, or large)." 81 | } 82 | }, 83 | "location": { 84 | "defaultValue": "[resourceGroup().location]", 85 | "type": "string", 86 | "metadata": { 87 | "description": "Location for all resources." 88 | } 89 | }, 90 | "qnaMakerSku": { 91 | "defaultValue": "S0 ($10 per month for unlimited documents, 3 transactions per second, 100 transactions per minute)", 92 | "allowedValues": [ 93 | "F0 (3 managed documents per month, 3 transactions per second, 100 transactions per minute, 50K transactions per month)", 94 | "S0 ($10 per month for unlimited documents, 3 transactions per second, 100 transactions per minute)" 95 | ], 96 | "type": "string", 97 | "metadata": { 98 | "description": "The pricing tier for the QnAMaker service." 99 | } 100 | }, 101 | "searchServiceSku": { 102 | "defaultValue": "B (15 indexes)", 103 | "allowedValues": [ 104 | "F (3 indexes)", 105 | "B (15 indexes)" 106 | ], 107 | "type": "string", 108 | "metadata": { 109 | "description": "The pricing tier for the Azure Search service." 110 | } 111 | }, 112 | "gitRepoUrl": { 113 | "defaultValue": "https://github.com/OfficeDev/microsoft-teams-crowdsourcer-app.git", 114 | "type": "string", 115 | "metadata": { 116 | "description": "The URL to the GitHub repository to deploy." 117 | } 118 | }, 119 | "gitBranch": { 120 | "defaultValue": "master", 121 | "type": "string", 122 | "metadata": { 123 | "description": "The branch of the GitHub repository to deploy." 124 | } 125 | } 126 | }, 127 | "variables": { 128 | "uniqueString": "[uniquestring(subscription().subscriptionId, resourceGroup().id, parameters('baseResourceName'))]", 129 | "botName": "[parameters('baseResourceName')]", 130 | "botAppDomain": "[concat(variables('botName'), '.azurewebsites.net')]", 131 | "botAppUrl": "[concat('https://', variables('botAppDomain'))]", 132 | "hostingPlanName": "[parameters('baseResourceName')]", 133 | "hostingPlanNameQnAMaker": "[concat('qnamaker-',parameters('baseResourceName'))]", 134 | "storageAccountName": "[variables('uniqueString')]", 135 | "botAppInsightsName": "[parameters('baseResourceName')]", 136 | "functionAppName": "[concat(parameters('baseResourceName'), '-function')]", 137 | "qnaMakerAccountName": "[concat(parameters('baseResourceName'),'-', variables('uniqueString'))]", 138 | "qnaMakerAppServiceName": "[concat('qnamaker-', variables('uniqueString'))]", 139 | "qnaMakerAppInsightsName": "[concat('qnamaker-', variables('uniqueString'))]", 140 | "qnaMakerSkuValue": "[substring(parameters('qnaMakerSku'), 0, 2)]", 141 | "azureSearchName": "[concat('search-', variables('uniqueString'))]", 142 | "azureSearchSkus": { 143 | "F ": "free", 144 | "B ": "basic" 145 | }, 146 | "azureSearchSkuValue": "[variables('azureSearchSkus')[toUpper(substring(parameters('searchServiceSku'), 0, 2))]]", 147 | "skuFamily": "[take(parameters('sku'), 1)]" 148 | }, 149 | "resources": [ 150 | { 151 | "type": "Microsoft.Storage/storageAccounts", 152 | "apiVersion": "2018-02-01", 153 | "name": "[variables('storageAccountName')]", 154 | "location": "[parameters('location')]", 155 | "sku": { 156 | "name": "Standard_LRS" 157 | }, 158 | "kind": "Storage" 159 | }, 160 | { 161 | "type": "Microsoft.Web/serverfarms", 162 | "apiVersion": "2016-09-01", 163 | "name": "[variables('hostingPlanName')]", 164 | "location": "[parameters('location')]", 165 | "sku": { 166 | "name": "[concat(variables('skuFamily'),parameters('planSize'))]", 167 | "tier": "[parameters('sku')]", 168 | "size": "[concat(variables('skuFamily'), parameters('planSize'))]", 169 | "family": "[variables('skuFamily')]", 170 | "capacity": 0 171 | }, 172 | "properties": { 173 | "name": "[variables('hostingPlanName')]", 174 | "hostingEnvironment": "", 175 | "numberOfWorkers": 1 176 | } 177 | }, 178 | { 179 | "type": "Microsoft.Web/serverfarms", 180 | "apiVersion": "2016-09-01", 181 | "name": "[variables('hostingPlanNameQnAMaker')]", 182 | "location": "[parameters('location')]", 183 | "sku": { 184 | "name": "[concat(variables('skuFamily'),parameters('planSize'))]", 185 | "tier": "[parameters('sku')]", 186 | "size": "[concat(variables('skuFamily'), parameters('planSize'))]", 187 | "family": "[variables('skuFamily')]", 188 | "capacity": 0 189 | }, 190 | "properties": { 191 | "name": "[variables('hostingPlanNameQnAMaker')]", 192 | "hostingEnvironment": "", 193 | "numberOfWorkers": 1 194 | } 195 | }, 196 | { 197 | "type": "Microsoft.Web/sites", 198 | "apiVersion": "2016-08-01", 199 | "name": "[variables('botName')]", 200 | "location": "[parameters('location')]", 201 | "dependsOn": [ 202 | "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", 203 | "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", 204 | "[resourceId('Microsoft.CognitiveServices/accounts/', variables('qnaMakerAccountName'))]", 205 | "[resourceId('Microsoft.Search/searchServices/', variables('azureSearchName'))]", 206 | "[resourceId('Microsoft.Insights/components/', variables('botAppInsightsName'))]" 207 | ], 208 | "kind": "app", 209 | "properties": { 210 | "name": "[variables('botName')]", 211 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", 212 | "enabled": true, 213 | "reserved": false, 214 | "clientAffinityEnabled": true, 215 | "clientCertEnabled": false, 216 | "hostNamesDisabled": false, 217 | "containerSize": 0, 218 | "dailyMemoryTimeQuota": 0, 219 | "httpsOnly": true, 220 | "siteConfig": { 221 | "alwaysOn": true, 222 | "appSettings": [ 223 | { 224 | "name": "SITE_ROLE", 225 | "value": "bot" 226 | }, 227 | { 228 | "name": "MicrosoftAppId", 229 | "value": "[parameters('botClientId')]" 230 | }, 231 | { 232 | "name": "MicrosoftAppPassword", 233 | "value": "[parameters('botClientSecret')]" 234 | }, 235 | { 236 | "name": "StorageConnectionString", 237 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')),'2015-05-01-preview').key1)]" 238 | }, 239 | { 240 | "name": "TenantId", 241 | "value": "[parameters('tenantId')]" 242 | }, 243 | { 244 | "name": "AppBaseUri", 245 | "value": "[concat('https://', variables('botAppDomain'))]" 246 | }, 247 | { 248 | "name": "QnAMakerApiUrl", 249 | "value": "https://westus.api.cognitive.microsoft.com" 250 | }, 251 | { 252 | "name": "QnAMakerSubscriptionKey", 253 | "value": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts/', variables('qnaMakerAccountName')), '2017-04-18').key1]" 254 | }, 255 | { 256 | "name": "APPINSIGHTS_INSTRUMENTATIONKEY", 257 | "value": "[reference(resourceId('Microsoft.Insights/components/', variables('botAppInsightsName')), '2015-05-01').InstrumentationKey]" 258 | }, 259 | { 260 | "name": "ScoreThreshold", 261 | "value": "50" 262 | }, 263 | { 264 | "name": "SearchServiceName", 265 | "value": "[variables('azureSearchName')]" 266 | }, 267 | { 268 | "name": "SearchServiceKey", 269 | "value": "[listAdminKeys(resourceId('Microsoft.Search/searchServices/', variables('azureSearchName')), '2015-08-19').primaryKey]" 270 | }, 271 | { 272 | "name": "SearchServiceQueryApiKey", 273 | "value": "[listQueryKeys(resourceId('Microsoft.Search/searchServices/', variables('azureSearchName')), '2015-08-19').value[0].key]" 274 | }, 275 | { 276 | "name": "AccessCacheExpiryInDays", 277 | "value": "5" 278 | }, 279 | { 280 | "name": "SearchIndexingIntervalInMinutes", 281 | "value": "15" 282 | }, 283 | { 284 | "name": "QnAMakerHostUrl", 285 | "value": "[concat('https://', reference(resourceId('Microsoft.Web/sites', variables('qnaMakerAppServiceName'))).hostNames[0])]" 286 | } 287 | ] 288 | } 289 | }, 290 | "resources": [ 291 | { 292 | "type": "sourcecontrols", 293 | "apiVersion": "2016-08-01", 294 | "name": "web", 295 | "dependsOn": [ 296 | "[resourceId('Microsoft.Web/sites', variables('botName'))]" 297 | ], 298 | "properties": { 299 | "RepoUrl": "[parameters('gitRepoUrl')]", 300 | "branch": "[parameters('gitBranch')]", 301 | "IsManualIntegration": true 302 | }, 303 | "condition": "[not(empty(parameters('gitRepoUrl')))]" 304 | } 305 | ] 306 | }, 307 | { 308 | "type": "Microsoft.Insights/components", 309 | "apiVersion": "2015-05-01", 310 | "name": "[variables('botAppInsightsName')]", 311 | "location": "[parameters('location')]", 312 | "tags": { 313 | "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', variables('botName'))]": "Resource" 314 | }, 315 | "properties": { 316 | "Application_Type": "web", 317 | "Request_Source": "rest" 318 | } 319 | }, 320 | { 321 | "type": "Microsoft.BotService/botServices", 322 | "apiVersion": "2018-07-12", 323 | "name": "[variables('botName')]", 324 | "location": "global", 325 | "dependsOn": [ 326 | "[resourceId('Microsoft.Web/sites', variables('botName'))]", 327 | "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]" 328 | ], 329 | "sku": { 330 | "name": "F0" 331 | }, 332 | "kind": "sdk", 333 | "properties": { 334 | "displayName": "[parameters('appDisplayName')]", 335 | "description": "[parameters('appDescription')]", 336 | "iconUrl": "[parameters('appIconUrl')]", 337 | "msaAppId": "[parameters('botClientId')]", 338 | "endpoint": "[concat(variables('botAppUrl'), '/api/messages')]" 339 | }, 340 | "resources": [ 341 | { 342 | "type": "Microsoft.BotService/botServices/channels", 343 | "apiVersion": "2018-07-12", 344 | "name": "[concat(variables('botName'), '/MsTeamsChannel')]", 345 | "location": "global", 346 | "dependsOn": [ 347 | "[resourceId('Microsoft.Web/sites', variables('botName'))]", 348 | "[concat('Microsoft.BotService/botServices/', variables('botName'))]" 349 | ], 350 | "sku": { 351 | "name": "F0" 352 | }, 353 | "properties": { 354 | "channelName": "MsTeamsChannel", 355 | "location": "global", 356 | "properties": { 357 | "isEnabled": true 358 | } 359 | } 360 | } 361 | ] 362 | }, 363 | { 364 | "type": "Microsoft.Search/searchServices", 365 | "apiVersion": "2015-08-19", 366 | "name": "[variables('azureSearchName')]", 367 | "location": "[parameters('location')]", 368 | "tags": { 369 | "isqnamaker": "true" 370 | }, 371 | "sku": { 372 | "name": "[toLower(variables('azureSearchSkuValue'))]" 373 | }, 374 | "properties": { 375 | "replicaCount": 1, 376 | "partitionCount": 1, 377 | "hostingMode": "default" 378 | } 379 | }, 380 | { 381 | "type": "Microsoft.Web/sites", 382 | "apiVersion": "2016-08-01", 383 | "name": "[variables('functionAppName')]", 384 | "location": "[parameters('location')]", 385 | "dependsOn": [ 386 | "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", 387 | "[resourceId('Microsoft.CognitiveServices/accounts/', variables('qnaMakerAccountName'))]", 388 | "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" 389 | ], 390 | "kind": "functionapp", 391 | "properties": { 392 | "name": "[variables('functionAppName')]", 393 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", 394 | "hostingEnvironment": "", 395 | "clientAffinityEnabled": false, 396 | "siteConfig": { 397 | "alwaysOn": true, 398 | "appSettings": [ 399 | { 400 | "name": "PROJECT", 401 | "value": "Source/Microsoft.Teams.Apps.CrowdSourcer.AzureFunction/Microsoft.Teams.Apps.CrowdSourcer.AzureFunction.csproj" 402 | }, 403 | { 404 | "name": "SITE_ROLE", 405 | "value": "function" 406 | }, 407 | { 408 | "name": "AzureWebJobsStorage", 409 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2015-05-01-preview').key1,';')]" 410 | }, 411 | { 412 | "name": "AzureWebJobsDashboard", 413 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2015-05-01-preview').key1,';')]" 414 | }, 415 | { 416 | "name": "FUNCTIONS_EXTENSION_VERSION", 417 | "value": "~2" 418 | }, 419 | { 420 | "name": "FUNCTIONS_WORKER_RUNTIME", 421 | "value": "dotnet" 422 | }, 423 | { 424 | "name": "QnAMakerApiUrl", 425 | "value": "https://westus.api.cognitive.microsoft.com" 426 | }, 427 | { 428 | "name": "QnAMakerSubscriptionKey", 429 | "value": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts/', variables('qnaMakerAccountName')), '2017-04-18').key1]" 430 | }, 431 | { 432 | "name": "SearchServiceName", 433 | "value": "[variables('azureSearchName')]" 434 | }, 435 | { 436 | "name": "SearchServiceKey", 437 | "value": "[listAdminKeys(resourceId('Microsoft.Search/searchServices/', variables('azureSearchName')), '2015-08-19').primaryKey]" 438 | }, 439 | { 440 | "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", 441 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')),'2015-05-01-preview').key1)]" 442 | }, 443 | { 444 | "name": "WEBSITE_CONTENTSHARE", 445 | "value": "[toLower(variables('functionAppName'))]" 446 | }, 447 | { 448 | "name": "StorageConnectionString", 449 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')),'2015-05-01-preview').key1)]" 450 | }, 451 | { 452 | "name": "WEBSITE_NODE_DEFAULT_VERSION", 453 | "value": "10.14.1" 454 | } 455 | ] 456 | } 457 | }, 458 | "resources": [ 459 | { 460 | "type": "sourcecontrols", 461 | "apiVersion": "2015-08-01", 462 | "name": "web", 463 | "dependsOn": [ 464 | "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]" 465 | ], 466 | "properties": { 467 | "RepoUrl": "[parameters('gitRepoUrl')]", 468 | "branch": "[parameters('gitBranch')]", 469 | "IsManualIntegration": true 470 | }, 471 | "condition": "[not(empty(parameters('gitRepoUrl')))]" 472 | } 473 | ] 474 | }, 475 | { 476 | "type": "Microsoft.Web/sites", 477 | "apiVersion": "2016-08-01", 478 | "name": "[variables('qnaMakerAppServiceName')]", 479 | "location": "[parameters('location')]", 480 | "dependsOn": [ 481 | "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanNameQnAMaker'))]" 482 | ], 483 | "tags": { 484 | "isqnamaker": "true", 485 | "[concat('hidden-related:', '/subscriptions/', subscription().subscriptionId,'/resourcegroups/', resourceGroup().name, '/providers/Microsoft.Web/serverfarms/', variables('hostingPlanNameQnAMaker'))]": "empty" 486 | }, 487 | "properties": { 488 | "enabled": true, 489 | "siteConfig": { 490 | "cors": { 491 | "allowedOrigins": [ 492 | "*" 493 | ] 494 | } 495 | }, 496 | "name": "[variables('qnaMakerAppServiceName')]", 497 | "serverFarmId": "[concat('/subscriptions/', subscription().subscriptionId,'/resourcegroups/', resourceGroup().name, '/providers/Microsoft.Web/serverfarms/', variables('hostingPlanNameQnAMaker'))]", 498 | "hostingEnvironment": "" 499 | }, 500 | "resources": [ 501 | { 502 | "type": "microsoft.insights/components", 503 | "apiVersion": "2015-05-01", 504 | "name": "[variables('qnaMakerAppInsightsName')]", 505 | "location": "[parameters('location')]", 506 | "dependsOn": [ 507 | "[resourceId('Microsoft.Web/sites/', variables('qnaMakerAppServiceName'))]" 508 | ], 509 | "tags": { 510 | "[concat('hidden-link:', resourceId('Microsoft.Web/sites/', variables('qnaMakerAppServiceName')))]": "Resource" 511 | }, 512 | "kind": "web", 513 | "properties": { 514 | "ApplicationId": "[variables('qnaMakerAppServiceName')]" 515 | } 516 | }, 517 | { 518 | "type": "config", 519 | "apiVersion": "2015-08-01", 520 | "name": "appsettings", 521 | "dependsOn": [ 522 | "[resourceId('Microsoft.Web/Sites', variables('qnaMakerAppServiceName'))]", 523 | "[resourceId('Microsoft.Search/searchServices/', variables('azureSearchName'))]" 524 | ], 525 | "properties": { 526 | "AzureSearchName": "[variables('azureSearchName')]", 527 | "AzureSearchAdminKey": "[listAdminKeys(resourceId('Microsoft.Search/searchServices/', variables('azureSearchName')), '2015-08-19').primaryKey]", 528 | "UserAppInsightsKey": "[reference(resourceId('Microsoft.Insights/components/', variables('qnaMakerAppInsightsName')), '2015-05-01').InstrumentationKey]", 529 | "UserAppInsightsName": "[variables('qnaMakerAppInsightsName')]", 530 | "UserAppInsightsAppId": "[reference(resourceId('Microsoft.Insights/components/', variables('qnaMakerAppInsightsName')), '2015-05-01').AppId]", 531 | "PrimaryEndpointKey": "[concat(variables('qnaMakerAppServiceName'), '-PrimaryEndpointKey')]", 532 | "SecondaryEndpointKey": "[concat(variables('qnaMakerAppServiceName'), '-SecondaryEndpointKey')]", 533 | "QNAMAKER_EXTENSION_VERSION": "latest" 534 | } 535 | } 536 | ] 537 | }, 538 | { 539 | "type": "Microsoft.CognitiveServices/accounts", 540 | "apiVersion": "2017-04-18", 541 | "name": "[variables('qnaMakerAccountName')]", 542 | "location": "westus", 543 | "dependsOn": [ 544 | "[resourceId('Microsoft.Web/Sites', variables('qnaMakerAppServiceName'))]", 545 | "[resourceId('Microsoft.Search/searchServices/', variables('azureSearchName'))]", 546 | "[resourceId('microsoft.insights/components/', variables('qnaMakerAppInsightsName'))]" 547 | ], 548 | "sku": { 549 | "name": "[variables('qnaMakerSkuValue')]" 550 | }, 551 | "kind": "QnAMaker", 552 | "properties": { 553 | "apiProperties": { 554 | "qnaRuntimeEndpoint": "[concat('https://', reference(resourceId('Microsoft.Web/sites', variables('qnaMakerAppServiceName'))).hostNames[0])]" 555 | } 556 | } 557 | } 558 | ], 559 | "outputs": { 560 | "botId": { 561 | "type": "String", 562 | "value": "[parameters('botClientId')]" 563 | }, 564 | "appDomain": { 565 | "type": "String", 566 | "value": "[variables('botAppDomain')]" 567 | } 568 | } 569 | } --------------------------------------------------------------------------------