├── .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 | 
12 | 
13 | 
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 | }
--------------------------------------------------------------------------------