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