├── .gitignore ├── CODE_OF_CONDUCT.md ├── Deployment ├── ZipDeploy │ ├── azuredeploy.json │ ├── build-package.ps1 │ └── parameters.json ├── cleanup-deployment.ps1 ├── deploy-spo.ps1 ├── deploy.ps1 └── warmup.ps1 ├── FunctionApp └── CQS │ ├── CsvInputParser │ ├── function.json │ └── run.ps1 │ ├── GetCallQueueById │ ├── function.json │ └── run.ps1 │ ├── GetCallQueues │ ├── function.json │ └── run.ps1 │ ├── ListCallQueues │ ├── function.json │ └── run.ps1 │ ├── Modules │ ├── GetAuthenticationToken │ │ ├── GetAuthenticationToken.psd1 │ │ └── GetAuthenticationToken.psm1 │ ├── GetCallQueues │ │ ├── GetCallQueues.psd1 │ │ └── GetCallQueues.psm1 │ ├── GetGroupInfo │ │ ├── GetGroupInfo.psd1 │ │ └── GetGroupInfo.psm1 │ ├── GetSchedulesManifest │ │ ├── GetSchedulesByTime.psd1 │ │ └── GetSchedulesByTime.psm1 │ ├── GetUserInfo │ │ ├── GetUserInfo.psd1 │ │ └── GetUserInfo.psm1 │ ├── JSON-Schemas │ │ ├── JSON-Schemas.psd1 │ │ └── JSON-Schemas.psm1 │ └── UpdateSchedulesManifest │ │ ├── UpdateScheduleEntry.psd1 │ │ └── UpdateScheduleEntry.psm1 │ ├── ProcessShifts │ ├── function.json │ ├── readme.md │ └── run.ps1 │ ├── UpdateCallQueueAgents │ ├── function.json │ └── run.ps1 │ ├── host.json │ ├── profile.ps1 │ └── requirements.psd1 ├── LICENSE ├── Media ├── AzFunc-Consent.png ├── AzureAD-CA-config.png ├── CQS-AI-1.png ├── CQS-AI-2.png ├── CQS-AI-3.png ├── CQS-AgentsMgr.png ├── CQS-High-Level-Design.png ├── CQS-Home.png ├── CQS-PP-Connections.png ├── CQS-PPS-Import-1.png ├── CQS-PPS-Import-2.png ├── CQS-Schedule.png ├── CQS-Share.png ├── CQS-callqueueowners.png ├── DataFlowDiagram.vsdx ├── High-Level-Design.jpg ├── PP-access-1.png ├── PP-access-2.png ├── PowerApp_ImportingSolution.jpg ├── PowerApp_SolutionImported.jpg ├── SPOList_AA.jpg ├── SPOList_AAHolidays.jpg ├── SPOList_CQ.jpg ├── SPOList_Holidays.jpg ├── SPOList_UserManagement.jpg ├── SPOLists.jpg ├── SPOLists.png ├── custom-connector-01.png ├── custom-connector-02.png ├── custom-connector-import.png ├── taco-logo-transparent.png ├── taco-logo.png └── taco-white-logo-transparent.png ├── Pkgs ├── CQSCustomConnectorSolution_1_0_0_2.zip ├── CQSPowerPlatSolution_1_0_0_10.zip ├── CQSPowerPlatSolution_1_0_0_8.zip ├── CQSPowerPlatSolution_1_0_0_9.zip ├── TDME-function-artifact.zip ├── cqs-customconnector.yaml ├── cqs-function-artifact.zip ├── cqs-manager-site-lists.xml └── sample.csv ├── README.md ├── SECURITY.md └── SUPPORT.md /.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 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /Deployment/ZipDeploy/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "resourcePrefix": { 6 | "type": "string", 7 | "minLength": 3, 8 | "maxLength": 11, 9 | "metadata": { 10 | "description": "Prefix to your Azure resourcecs names" 11 | } 12 | }, 13 | "serviceAccountUPN": { 14 | "type": "string", 15 | "metadata": { 16 | "description": "email of your Microsoft Teams Service Account" 17 | } 18 | }, 19 | "serviceAccountSecret": { 20 | "type": "securestring", 21 | "metadata": { 22 | "description": "Password of your Microsoft Teams Service Account" 23 | } 24 | }, 25 | "clientID": { 26 | "type": "string", 27 | "metadata": { 28 | "description": "AzureAD application ID - aka clientID" 29 | } 30 | }, 31 | "appSecret": { 32 | "type": "string", 33 | "metadata": { 34 | "description": "AzureAD application secret - To be stored in Azure KeyVault" 35 | } 36 | }, 37 | "packageURI": { 38 | "type": "string", 39 | "metadata": { 40 | "description": "Location of the ZIP files with the Azure Functions definition" 41 | }, 42 | "defaultValue": "https://github.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/raw/main/Pkgs/cqs-function-artifact.zip" 43 | } 44 | }, 45 | "variables": { 46 | "randomize" : "[substring(uniqueString(resourceGroup().id),0,5)]", 47 | "stoAccountName": "[toLower(concat(parameters('resourcePrefix'),'stoaccnt',variables('randomize')))]", 48 | "serverFarmName" : "[concat(parameters('resourcePrefix'),'-appsvcplan-',variables('randomize'))]", 49 | "functionAppName" : "[concat(parameters('resourcePrefix'),'-func-',variables('randomize'))]", 50 | "keyVaultName" : "[concat(parameters('resourcePrefix'),'-kv-',variables('randomize'))]" 51 | }, 52 | "resources": [ 53 | { 54 | "name": "[variables('stoAccountName')]", 55 | "type": "Microsoft.Storage/storageAccounts", 56 | "apiVersion": "2021-06-01", 57 | "location": "[resourceGroup().location]", 58 | "kind": "StorageV2", 59 | "sku": { 60 | "name": "Standard_LRS" 61 | }, 62 | "properties": { 63 | "supportsHttpsTrafficOnly": true, 64 | "allowBlobPublicAccess" : true, 65 | "allowSharedKeyAccess" : true, 66 | "minimumTlsVersion" : "TLS1_2" 67 | } 68 | }, 69 | { 70 | "name": "[variables('serverFarmName')]", 71 | "type": "Microsoft.Web/serverfarms", 72 | "apiVersion": "2021-02-01", 73 | "location": "[resourceGroup().location]", 74 | "sku": { 75 | "name": "B1", 76 | "tier": "Basic", 77 | "size": "B1", 78 | "family": "B", 79 | "capacity": 1 80 | }, 81 | "kind": "app", 82 | "properties": { 83 | "reserved": false 84 | } 85 | }, 86 | { 87 | "name": "[variables('functionAppName')]", 88 | "type": "Microsoft.Web/sites", 89 | "apiVersion": "2021-02-01", 90 | "location": "[resourceGroup().location]", 91 | "kind": "functionapp", 92 | "dependsOn": [ 93 | "[resourceId('Microsoft.Web/serverfarms', variables('serverFarmName'))]", 94 | "[resourceId('Microsoft.Storage/storageAccounts', variables('stoAccountName'))]" 95 | ], 96 | "identity" :{ 97 | "type" : "SystemAssigned" 98 | }, 99 | "properties": { 100 | "name" : "[variables('functionAppName')]", 101 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('serverFarmName'))]" 102 | }, 103 | "resources": [ 104 | { 105 | "apiVersion": "2021-02-01", 106 | "name": "web", 107 | "type": "config", 108 | "dependsOn": [ 109 | "[resourceId('Microsoft.Web/Sites', variables('functionAppName'))]" 110 | ], 111 | "properties": { 112 | "alwaysOn": true, 113 | "powerShellVersion": "~7", 114 | "use32BitWorkerProcess": false 115 | } 116 | }, 117 | { 118 | "apiVersion": "2021-02-01", 119 | "name": "authsettingsV2", 120 | "type": "config", 121 | "dependsOn": [ 122 | "[resourceId('Microsoft.Web/Sites', variables('functionAppName'))]", 123 | "[resourceId('Microsoft.Web/sites/config', variables('functionAppName'), 'web')]" 124 | ], 125 | "properties": { 126 | "globalValidation": { 127 | "requireAuthentication": true, 128 | "unauthenticatedClientAction": "Return401" 129 | }, 130 | "identityProviders":{ 131 | "azureActiveDirectory": { 132 | "registration": { 133 | "clientId": "[parameters('clientID')]", 134 | "openIdIssuer": "[concat('https://sts.windows.net/', subscription().tenantId , '/v2.0')]" 135 | }, 136 | "validation": { 137 | "allowedAudiences": [ 138 | "[concat('api://azfunc-',parameters('clientID'))]" 139 | ] 140 | } 141 | } 142 | } 143 | } 144 | }, 145 | { 146 | "apiVersion": "2021-02-01", 147 | "name": "appsettings", 148 | "type": "config", 149 | "dependsOn": [ 150 | "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]", 151 | "[resourceId('Microsoft.Web/sites/config', variables('functionAppName'), 'web')]", 152 | "[resourceId('Microsoft.Web/sites/config', variables('functionAppName'), 'authsettingsV2')]" 153 | ], 154 | "properties": { 155 | "ShiftsMgrSvcAccountId" : "[concat('@Microsoft.KeyVault(VaultName=', variables('keyVaultName') ,';SecretName=CQS-service-account-name)')]", 156 | "ShiftsMgrSvcAccountPwd" : "[concat('@Microsoft.KeyVault(VaultName=', variables('keyVaultName') ,';SecretName=CQS-service-account-secret)')]", 157 | "ShiftsMgrSPOSiteId" : "Placed holder for Shifts Manager SPO Site Id", 158 | "ShiftsMgrManifestListId" : "Placed holder for Shifts Manager Manifest List Id", 159 | "ShiftsMgrChangeLogListId" : "Placed holder for Shifts Manager Change Log List Id", 160 | "ShiftsMgrCallQueueOwnersListId" : "Placed holder for Shifts Manager Call Queue Owners List Id", 161 | "AzureWebJobsStorage" : "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('stoAccountName'), ';EndpointSuffix=', environment().suffixes.storage, ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('stoAccountName')), '2019-06-01').keys[0].value)]", 162 | "FUNCTIONS_EXTENSION_VERSION" : "~4", 163 | "FUNCTIONS_WORKER_PROCESS_COUNT": "3", 164 | "FUNCTIONS_WORKER_RUNTIME": "powershell", 165 | "FUNCTIONS_WORKER_RUNTIME_VERSION": "~7", 166 | "WEBSITE_RUN_FROM_PACKAGE": "[parameters('packageURI')]" 167 | } 168 | } 169 | ] 170 | }, 171 | { 172 | "name": "[variables('keyVaultName')]", 173 | "type": "Microsoft.KeyVault/vaults", 174 | "apiVersion": "2019-09-01", 175 | "location": "[resourceGroup().location]", 176 | "dependsOn" :[ 177 | "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]" 178 | ], 179 | "properties": { 180 | "enabledForDeployment": false, 181 | "enabledForTemplateDeployment": false, 182 | "enabledForDiskEncryption": false, 183 | "tenantId": "[subscription().tenantId]", 184 | "accessPolicies": [ 185 | { 186 | "tenantId": "[reference(resourceId('Microsoft.Web/sites/', variables('functionAppName')), '2020-12-01', 'Full').identity.tenantId]", 187 | "objectId": "[reference(resourceId('Microsoft.Web/sites/', variables('functionAppName')), '2020-12-01', 'Full').identity.principalId]", 188 | "permissions": { 189 | "secrets": [ 190 | "get" 191 | ] 192 | } 193 | } 194 | ], 195 | "sku": { 196 | "name": "standard", 197 | "family": "A" 198 | } 199 | }, 200 | "resources": [ 201 | { 202 | "type": "Microsoft.KeyVault/vaults/secrets", 203 | "name": "[concat(variables('keyVaultName'), '/CQS-service-account-name')]", 204 | "apiVersion": "2021-11-01-preview", 205 | "dependsOn": [ 206 | "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]" 207 | ], 208 | "properties": { 209 | "value": "[parameters('serviceAccountUPN')]", 210 | "contentType": "text/plain", 211 | "attributes": { 212 | "enabled": true 213 | } 214 | } 215 | }, 216 | { 217 | "type": "Microsoft.KeyVault/vaults/secrets", 218 | "name": "[concat(variables('keyVaultName'), '/CQS-service-account-secret')]", 219 | "apiVersion": "2021-11-01-preview", 220 | "dependsOn": [ 221 | "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]" 222 | ], 223 | "properties": { 224 | "value": "[parameters('serviceAccountSecret')]", 225 | "contentType": "text/plain", 226 | "attributes": { 227 | "enabled": true 228 | } 229 | } 230 | }, 231 | { 232 | "type": "Microsoft.KeyVault/vaults/secrets", 233 | "name": "[concat(variables('keyVaultName'), '/CQS-appSecret')]", 234 | "apiVersion": "2021-11-01-preview", 235 | "dependsOn": [ 236 | "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]" 237 | ], 238 | "properties": { 239 | "value": "[parameters('appSecret')]", 240 | "contentType": "text/plain", 241 | "attributes": { 242 | "enabled": true 243 | } 244 | } 245 | } 246 | ] 247 | } 248 | ], 249 | "outputs": { 250 | "AzFuncAppName": { 251 | "type": "string", 252 | "value": "[variables('functionAppName')]" 253 | }, 254 | "AzFuncAppCode": { 255 | "type": "string", 256 | "value": "[listkeys(concat(resourceId('Microsoft.Web/sites', variables('functionAppName')), '/host/default/'),'2016-08-01').masterKey]" 257 | } , 258 | "AzFuncHostName": { 259 | "type": "string", 260 | "value": "[reference(resourceId('Microsoft.Web/sites', variables('functionAppName')), '2021-03-01', 'Full').properties.defaultHostName]" 261 | }, 262 | "outboundIpAddresses": { 263 | "type": "string", 264 | "value": "[reference(resourceId('Microsoft.Web/sites', variables('functionAppName')), '2021-03-01', 'Full').properties.outboundIpAddresses]" 265 | }, 266 | "AzKeyVaultName": { 267 | "type": "string", 268 | "value": "[variables('keyVaultName')]" 269 | } 270 | } 271 | } -------------------------------------------------------------------------------- /Deployment/ZipDeploy/build-package.ps1: -------------------------------------------------------------------------------- 1 | # Use this script if you need to generate a new ZIP package 2 | $rootfolder = "$PSScriptRoot\..\..\" 3 | 4 | # Make sure you update and save the MicrosofttTeams module as Azure Function custom modules 5 | <# 6 | Write-Host "Check if Microsoft Teams PowerShell module is installed and up-to-date" 7 | $TeamsPSModuleVersion = $(Find-Module -Name MicrosoftTeams).Version 8 | $TeamsPSModuleInstalled = $(Get-ChildItem -Path $($rootfolder + "FunctionApp\Modules\MicrosoftTeams")) 9 | 10 | If($TeamsPSModuleInstalled.Name -ne $TeamsPSModuleVersion -And $TeamsPSModuleInstalled -ne $null) 11 | { 12 | Write-Host "New Microsoft Teams PowerShell module found, download started" 13 | Remove-Item $TeamsPSModuleInstalled -Force -Con 14 | Save-Module -Path $($rootfolder + "FunctionApp\Modules") -Name MicrosoftTeams -Repository PSGallery -MinimumVersion 4.0.0 15 | } 16 | ElseIf($TeamsPSModuleInstalled -eq $null) 17 | { 18 | Write-Host "Downloading Microsoft Teams PowerShell module" 19 | Save-Module -Path $($rootfolder + "FunctionApp\Modules") -Name MicrosoftTeams -Repository PSGallery -MinimumVersion 4.0.0 20 | } 21 | #> 22 | 23 | # List in the ZIP package all the function app you need to deploy 24 | $packageFiles = @( 25 | "$($rootfolder)FunctionApp\CQS\CsvInputParser", 26 | "$($rootfolder)FunctionApp\CQS\GetCallQueueById", 27 | "$($rootfolder)FunctionApp\CQS\GetCallQueues", 28 | "$($rootfolder)FunctionApp\CQS\ListCallQueues", 29 | "$($rootfolder)FunctionApp\CQS\ProcessShifts", 30 | "$($rootfolder)FunctionApp\CQS\UpdateCallQueueAgents", 31 | "$($rootfolder)FunctionApp\CQS\Modules", 32 | "$($rootfolder)FunctionApp\CQS\host.json", 33 | "$($rootfolder)FunctionApp\CQS\profile.ps1", 34 | "$($rootfolder)FunctionApp\CQS\requirements.psd1" 35 | ) 36 | $destinationPath = $rootfolder + "Pkgs\cqs-function-artifact.zip" 37 | 38 | Write-Host "Creating Azure artifact" 39 | Compress-Archive -Path $packageFiles -DestinationPath $destinationPath -CompressionLevel optimal -Force 40 | 41 | Write-Host "Completed creating new deployment package" -------------------------------------------------------------------------------- /Deployment/ZipDeploy/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "resourcePrefix": { 6 | "value": "Teams" 7 | }, 8 | "serviceAccountUPN": { 9 | "value": "service@tenant.onmicrosoft.com" 10 | }, 11 | "serviceAccountSecret": { 12 | "value": "XXXXXXXXXX" 13 | }, 14 | "clientID": { 15 | "value": "XXXXXXXXXX" 16 | }, 17 | "appSecret": { 18 | "value": "XXXXXXXXXX" 19 | }, 20 | "packageURI" : { 21 | "value": "https://github.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/raw/main/Pkgs/cqs-function-artifact.zip" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Deployment/cleanup-deployment.ps1: -------------------------------------------------------------------------------- 1 | Param ( 2 | [parameter(mandatory = $true)] [string]$displayName, # Display name for your application registered in Azure AD 3 | [parameter(mandatory = $true)] [ValidateLength(3, 24)] [string]$rgName # Name of the resource group for Azure 4 | ) 5 | 6 | Connect-AzAccount -DeviceCode 7 | 8 | Remove-AzResourceGroup -Name $rgName -Force 9 | Remove-AzADApplication -DisplayName $displayName 10 | 11 | $CustomConnectorAADappName = "$displayName-customconnector" 12 | Remove-AzADApplication -DisplayName $CustomConnectorAADappName 13 | 14 | Write-Host "Cleanup completed" 15 | Write-Host "ALERT: The Key Vault is soft deleted. To avoid any conflicts provide a unique name for the parameters for your next deployment" -ForegroundColor Yellow 16 | 17 | Disconnect-AzAccount 18 | -------------------------------------------------------------------------------- /Deployment/deploy-spo.ps1: -------------------------------------------------------------------------------- 1 | Param ( 2 | [parameter(mandatory = $true)] [string]$SPOSiteTitle, # Title of the SharePoint site to be created 3 | [parameter(mandatory = $true)] [string]$AzFunctionMID, # Azure Function Managed Identity ID 4 | [parameter(mandatory = $true)] [string]$SPOHostUrl # Host url for the SPO site e.x: "moderncomms996974.sharepoint.com" 5 | ) 6 | Connect-PnPOnline -Url $SPOHostUrl -DeviceLogin 7 | <# 8 | $ctx = Get-PnPContext 9 | $ctx.Load($ctx.Web.CurrentUser) 10 | $ctx.ExecuteQuery() 11 | $userEmail = $ctx.Web.CurrentUser.Email 12 | #> 13 | 14 | # Creating SPO Site 15 | Write-Host "Provisioning SPO site..."; 16 | $site = New-PnPSite -Type TeamSite -Title $SPOSiteTitle -Alias $SPOSiteTitle.Replace(' ','') -Description "SPO site used as backend for the Call Queue Scheduler app" -Wait 17 | Write-Host "SPO site created: $site"; 18 | Write-Host "" 19 | 20 | # Granting write permissions to the Azure Function Managed Identity using Site.Selected scope 21 | Write-Host "Granting write permissions to the Azure Function Managed Identity using Site.Selected scope" 22 | Grant-PnPAzureADAppSitePermission -AppId $AzFunctionMID -DisplayName "cqs-azfunction-mid" -Permissions Write -Site $site 23 | Write-Host "" 24 | 25 | # Deploy the site template 26 | Write-Host "Deploying site template to create the required lists and libraries" 27 | Connect-PnPOnline -Url $site -DeviceLogin 28 | Invoke-PnPSiteTemplate -Path ..\Pkgs\cqs-manager-site-lists.xml -Verbose -IgnoreDuplicateDataRowErrors -Handlers Lists 29 | $RootWeb = Get-PnPSite -Includes ID 30 | $Web = Get-PnPWeb -Includes ID 31 | Write-Host "" 32 | 33 | Write-Host -ForegroundColor magenta "Here is the information you'll need to deploy and configure while following the remaining steps" 34 | Write-Host "SPO Site Url: " $site 35 | Write-Host "SPO Site ID: $SPOHostUrl,$($RootWeb.Id),$($Web.Id)" 36 | Get-PnPList | ?{$_.Title -like "Shifts*" -or $_.Title -like "CallQueue*"} | FT 37 | 38 | # Disconnecting session 39 | Disconnect-PnPOnline 40 | -------------------------------------------------------------------------------- /Deployment/deploy.ps1: -------------------------------------------------------------------------------- 1 | Param ( 2 | [parameter(mandatory = $true)] [string]$displayName, # Display name for your application registered in Azure AD 3 | [parameter(mandatory = $true)] [ValidateLength(3, 24)] [string]$rgName, # Name of the resource group for Azure 4 | [parameter(mandatory = $true)] [ValidateLength(3, 11)] [string]$resourcePrefix, # Prefix for the resources deployed on your Azure subscription (should be less than 11 characters) 5 | [parameter(mandatory = $true)] [ValidateSet('Australia Central','Australia East','Australia Southeast','Brazil South','Canada Central','Canada East','Central India','Central US','East Asia','East US 2','East US','France Central','Germany West Central','Japan East','Japan West','Korea Central','Korea South','North Central US','North Europe','Norway East','South Africa North','South Central US','South India','Southeast Asia','Sweden Central','Switzerland North','UAE North','UK South','UK West','West Central US','West Europe','West India','West US 2','West US 3','West US')] [string]$location, # Location (region) where the Azure resource are deployed 6 | [parameter(mandatory = $true)] [string]$serviceAccountUPN, # AzureAD Service Account UPN 7 | [parameter(mandatory = $true)] $serviceAccountSecret, # AzureAD Service Account password 8 | [parameter(mandatory = $false)] $teamsPSModuleVersion = "4.4.1", # Microsoft Teams PowerShell module version 9 | [parameter(mandatory = $false)] $subscriptionID # Microsoft Azure Subscription id 10 | ) 11 | 12 | $base = $PSScriptRoot 13 | Set-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings "true" 14 | 15 | # Import required PowerShell modules for the deployment 16 | If($PSVersionTable.PSVersion.Major -ne 7) { 17 | Write-Error "Please install and use PowerShell v7.2.1 to run this script" 18 | Write-Error "Follow the instruction to install PowerShell on Windows here" 19 | Write-Error "https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.2" 20 | return 21 | } 22 | 23 | Try 24 | { 25 | Import-Module Az.Accounts, Az.Resources, Az.KeyVault -ErrorAction Stop # Required to deploy the Azure resource 26 | } 27 | Catch 28 | { 29 | Write-Error "Azure PowerShell module missing, please run Install-Module Az to install the required model" 30 | } 31 | 32 | Try 33 | { 34 | Import-Module Microsoft.Graph.Applications -ErrorAction Stop # Required to configure Graph permissions 35 | } 36 | Catch 37 | { 38 | Write-Error "Microsoft.Graph.Applications module missing, please run Install-Module Microsoft.Graph to install the required model" 39 | } 40 | 41 | 42 | 43 | # Connect to AzureAD and Azure using modern authentication 44 | Write-Host -ForegroundColor blue "Azure sign-in request - Please check the sign-in window opened in your web browser" 45 | 46 | Try 47 | { 48 | Connect-AzAccount -DeviceCode #-WarningAction Ignore -ErrorAction Stop |Out-Null #-TenantId 8977abe7-f60c-4560-a93c-a506f2711c6a 49 | } 50 | Catch 51 | { 52 | Write-Error "An error occured connecting to Azure using the Azure PowerShell module" 53 | $_.Exception.Message 54 | } 55 | 56 | # Validating if multiple Azure Subscriptions are active 57 | If($subscriptionID -eq $null) 58 | { 59 | [array]$AzSubscriptions = Get-AzSubscription |Where-Object {$_.State -eq "Enabled"} 60 | $menu = @{} 61 | If($(Get-AzSubscription |Where-Object {$_.State -eq "Enabled"}).Count -gt 1) 62 | { 63 | Write-Host "Multiple active Azure Subscriptions found, please select a subscription from the list below:" 64 | for ($i=1;$i -le $AzSubscriptions.count; $i++) 65 | { 66 | Write-Host "$i. $($AzSubscriptions[$i-1].Id)" 67 | $menu.Add($i,($AzSubScriptions[$i-1].Id)) 68 | } 69 | [int]$AZSelectedSubscription = Read-Host 'Enter selection' 70 | $selection = $menu.Item($AZSelectedSubscription) ; 71 | Select-AzSubscription -Subscription $selection | Out-Null 72 | } 73 | } 74 | else 75 | { 76 | Select-AzSubscription -Subscription $subscriptionID | Out-Null 77 | } 78 | 79 | Write-Host -ForegroundColor blue "Checking if app '$displayName' is already registered" 80 | $AADapp = Get-AzADServicePrincipal -DisplayName $displayName 81 | $AppIdURI = $null 82 | If ($AADapp.Count -gt 0) { 83 | Write-Warning "The Azure AD App name which was provided '$displayName' does already exist!" 84 | $ResetAADapp = Read-Host "Do you want to reset the credentials the existing Azure AD App registration? `r`nDoing this will impact any other application using this Azure AD App registration. (Answer with yes or no) " 85 | 86 | If($ResetAADapp -eq "yes") 87 | { 88 | Try 89 | { 90 | Remove-AzADSpCredential -DisplayName $displayName -ErrorAction Stop 91 | } 92 | Catch 93 | { 94 | Write-Error "An issue occured removing the credentials from the Azure AD application" 95 | $_.Exception.Message 96 | } 97 | 98 | Try 99 | { 100 | $newCredential = $AADapp|New-AzADSpCredential -ErrorAction Stop 101 | } 102 | Catch 103 | { 104 | Write-Error "An issue occured creating new credentials for the Azure AD application" 105 | $_.Exception.Message 106 | } 107 | 108 | # 109 | # Get the AppID and AppSecret from the newly registered App 110 | $clientID = $AADapp.AppId 111 | $clientsecret = $newCredential.SecretText 112 | $AppIdURI = "api://azfunc-" + $AADapp.AppId 113 | 114 | # Get the tenantID from current AzureAD PowerShell session 115 | $tenantID = $(Get-AzTenant).Id 116 | Write-Host -ForegroundColor blue "New app '$displayName' registered into AzureAD" 117 | 118 | } 119 | ElseIf($ResetAADapp -eq "no") 120 | { 121 | write-host "user answered no" 122 | throw "Please rerun the deployment script by providing a different name for the displayname parameter" 123 | } 124 | } 125 | ElseIf([string]::IsNullOrEmpty($AADapp)){ 126 | Write-Host -ForegroundColor blue "Register a new app in Azure AD using Azure Function app name" 127 | 128 | Try 129 | { 130 | $AADapp = New-AzADServicePrincipal -DisplayName $displayName -ErrorAction Stop 131 | } 132 | Catch 133 | { 134 | Write-Error "An issue occured creating registering the application" 135 | $_.Exception.Message 136 | } 137 | 138 | # Expose an API and create an Application ID URI 139 | $AppIdURI = "api://azfunc-" + $AADapp.AppId 140 | 141 | 142 | [Microsoft.Azure.PowerShell.Cmdlets.Resources.MSGraph.Models.ApiV10.IMicrosoftGraphApiApplication]$apiProperties = @{ 143 | Oauth2PermissionScope = [Microsoft.Azure.PowerShell.Cmdlets.Resources.MSGraph.Models.ApiV10.IMicrosoftGraphPermissionScope]@{ 144 | AdminConsentDescription = "Allow the application to access $displayName on behalf of the signed-in user." 145 | AdminConsentDisplayName = "Access $displayName" 146 | IsEnabled = $true 147 | Type = "User" 148 | UserConsentDescription = "Allow the application to access $displayName on your behalf." 149 | UserConsentDisplayName = "Access $displayName" 150 | Value = "user_impersonation" 151 | Id = (New-Guid).Guid 152 | } 153 | } 154 | 155 | Try { 156 | Get-AzAdApplication -DisplayName $displayName | Update-AzADApplication -IdentifierUri $AppIdURI -Api $apiProperties -ErrorAction Stop 157 | Write-Host -ForegroundColor blue "New app '$displayName' registered into AzureAD" 158 | } 159 | Catch { 160 | Write-Error "Azure AD application registration error - Please check your permissions in Azure AD and review detailed error description below" 161 | $_.Exception.Message 162 | } 163 | 164 | # 165 | # Get the AppID and AppSecret from the newly registered App 166 | $clientID = $AADapp.AppId 167 | $clientsecret = $AADapp.PasswordCredentials.SecretText 168 | 169 | # Get the tenantID from current AzureAD PowerShell session 170 | $tenantID = $(Get-AzTenant).Id 171 | } 172 | 173 | 174 | 175 | Write-Host -ForegroundColor blue "Checking if app '$displayName-customconnector' is already registered" 176 | $CustomConnectorAADappName = "$displayName-customconnector" 177 | $AADapp = Get-AzADServicePrincipal -DisplayName $CustomConnectorAADappName 178 | If ($AADapp.Count -gt 0) { 179 | Write-Warning "The Azure AD App name which was provided '$CustomConnectorAADappName' does already exist!" 180 | $ResetAADapp = Read-Host "Do you want to reset the credentials the existing Azure AD App registration? `r`nDoing this will impact any other application using this Azure AD App registration. (Answer with yes or no) " 181 | 182 | If($ResetAADapp -eq "yes") 183 | { 184 | Try 185 | { 186 | Remove-AzADSpCredential -DisplayName $CustomConnectorAADappName -ErrorAction Stop 187 | } 188 | Catch 189 | { 190 | Write-Error "An issue occured removing the credentials from the Azure AD application" 191 | $_.Exception.Message 192 | } 193 | 194 | Try 195 | { 196 | $newCredential = $AADapp|New-AzADSpCredential -ErrorAction Stop 197 | } 198 | Catch 199 | { 200 | Write-Error "An issue occured creating new credentials for the Azure AD application" 201 | $_.Exception.Message 202 | } 203 | 204 | # 205 | # Get the AppID, ObjectID and AppSecret from the newly registered App 206 | $AADapp = Get-AzAdApplication -DisplayName $CustomConnectorAADappName 207 | $customConnAppclientID = $AADapp.AppId 208 | $customConnAppObjectID = $AADapp.Id 209 | $customConnAppclientsecret = $newCredential.SecretText 210 | 211 | # Get the tenantID from current AzureAD PowerShell session 212 | $tenantID = $(Get-AzTenant).Id 213 | Write-Host -ForegroundColor blue "New app '$CustomConnectorAADappName' registered into AzureAD" 214 | 215 | } 216 | ElseIf($ResetAADapp -eq "no") 217 | { 218 | write-host "user answered no" 219 | throw "Please rerun the deployment script by providing a different name for the displayname parameter" 220 | } 221 | } 222 | ElseIf([string]::IsNullOrEmpty($AADapp)){ 223 | Write-Host -ForegroundColor blue "Register a new app in Azure AD using '$CustomConnectorAADappName' name for the custom connector" 224 | 225 | Try 226 | { 227 | $AADapp = New-AzADServicePrincipal -DisplayName $CustomConnectorAADappName -ErrorAction Stop 228 | } 229 | Catch 230 | { 231 | Write-Error "An issue occured creating registering the application" 232 | $_.Exception.Message 233 | } 234 | 235 | $webProperties = [Microsoft.Azure.PowerShell.Cmdlets.Resources.MSGraph.Models.ApiV10.IMicrosoftGraphWebApplication]@{ 236 | RedirectUri = @("https://global.consent.azure-apim.net/redirect") 237 | ImplicitGrantSetting = [Microsoft.Azure.PowerShell.Cmdlets.Resources.MSGraph.Models.ApiV10.IMicrosoftGraphImplicitGrantSettings]@{ 238 | EnableAccessTokenIssuance = $true 239 | EnableIdTokenIssuance = $true 240 | } 241 | } 242 | Try { 243 | Get-AzAdApplication -DisplayName $CustomConnectorAADappName | Update-AzADApplication -Web $webProperties -AvailableToOtherTenants $true -ErrorAction Stop #-ReplyUrl "https://global.consent.azure-apim.net/redirect" 244 | Write-Host -ForegroundColor blue "New app '$CustomConnectorAADappName' registered into AzureAD" 245 | } 246 | Catch { 247 | Write-Error "Azure AD application registration error - Please check your permissions in Azure AD and review detailed error description below" 248 | $_.Exception.Message 249 | } 250 | 251 | Try 252 | { 253 | $newCredential = Get-AzADServicePrincipal -DisplayName $CustomConnectorAADappName | New-AzADSpCredential -ErrorAction Stop 254 | } 255 | Catch 256 | { 257 | Write-Error "An issue occured creating new credentials for the Azure AD application. You can ignore this and generate new credentials manually in the Azure AD portal." 258 | $_.Exception.Message 259 | } 260 | 261 | # 262 | # Get the AppID, ObjectID and AppSecret from the newly registered App 263 | $AADapp = Get-AzAdApplication -DisplayName $CustomConnectorAADappName 264 | $customConnAppclientID = $AADapp.AppId 265 | $customConnAppObjectID = $AADapp.Id 266 | $customConnAppclientsecret = $newCredential.SecretText 267 | 268 | # Get the tenantID from current AzureAD PowerShell session 269 | $tenantID = $(Get-AzTenant).Id 270 | } 271 | 272 | 273 | Write-Host -ForegroundColor blue "Deploy resource to Azure subscription" 274 | Try { 275 | New-AzResourceGroup -Name $rgName -Location $location -Force -ErrorAction Stop | Out-Null 276 | } 277 | Catch { 278 | Write-Error "Azure Resource Group creation failed - Please verify your permissions on the subscription and review detailed error description below" 279 | $_.Exception.Message 280 | } 281 | 282 | Write-Host -ForegroundColor blue "Resource Group $rgName created in location $location - Now initiating Azure resource deployments..." 283 | $deploymentName = 'deploy-' + (Get-Date -Format "yyyyMMdd-hhmm") 284 | $parameters = @{ 285 | resourcePrefix = $resourcePrefix 286 | serviceAccountUPN = $serviceAccountUPN 287 | serviceAccountSecret = $serviceAccountSecret 288 | clientID = $clientID 289 | appSecret = $clientSecret 290 | } 291 | 292 | $outputs = New-AzResourceGroupDeployment -ResourceGroupName $rgName -TemplateFile $base\ZipDeploy\azuredeploy.json -TemplateParameterObject $parameters -Name $deploymentName 293 | If ($outputs.provisioningState -ne 'Succeeded') { 294 | Write-Error "ARM deployment failed with error" 295 | $retry = Read-Host "Do you want to retry the deployment (yes/no)?" 296 | 297 | while($retry -eq "yes") 298 | { 299 | Write-Host "Retrying deployment Azure resource deployments" 300 | $outputs = New-AzResourceGroupDeployment -ResourceGroupName $rgName -TemplateFile $base\ZipDeploy\azuredeploy.json -TemplateParameterObject $parameters -Name $deploymentName 301 | 302 | If ($outputs.provisioningState -ne 'Succeeded') 303 | { 304 | Write-Error "ARM deployment failed with error" 305 | $retry = Read-Host "Do you want to retry the deployment (yes/no)?" 306 | } 307 | If ($outputs.provisioningState -eq 'Succeeded') 308 | { 309 | $retry = "no" 310 | } 311 | } 312 | 313 | if($retry -eq "no" -and $outputs.provisioningState -ne 'Succeeded') 314 | { 315 | throw "Deployment of the Azure resources failed, please review the error messages and review the logs available in the Azure Portal" 316 | } 317 | } 318 | 319 | Write-Host -ForegroundColor blue "ARM template deployed successfully" 320 | 321 | 322 | $CurrentUserId = Get-AzContext | ForEach-Object account | ForEach-Object Id 323 | if($CurrentUserId -ne $serviceAccountUPN) 324 | { 325 | # Assign current user with the permissions to list and read Azure KeyVault secrets (to enable the connection with the Power Automate flow) 326 | Write-Host -ForegroundColor blue "Assigning 'Secrets List & Get' policy on Azure KeyVault for user $CurrentUserId" 327 | Try { 328 | Set-AzKeyVaultAccessPolicy -VaultName $outputs.Outputs.azKeyVaultName.Value -ResourceGroupName $rgName -UserPrincipalName $CurrentUserId -PermissionsToSecrets list,get,delete,set 329 | } 330 | Catch { 331 | Write-Error "Error - Couldn't assign user permissions to get,list the KeyVault secrets - Please review detailed error message below" 332 | $_.Exception.Message 333 | } 334 | 335 | <# 336 | # Assign service account with the permissions to list and read Azure KeyVault secrets (to enable the connection with the Power Automate flow) 337 | Write-Host -ForegroundColor blue "Assigning 'Secrets List & Get' policy on Azure KeyVault for user $serviceAccountUPN" 338 | Try { 339 | Set-AzKeyVaultAccessPolicy -VaultName $outputs.Outputs.azKeyVaultName.Value -ResourceGroupName $rgName -UserPrincipalName $serviceAccountUPN -PermissionsToSecrets list,get 340 | } 341 | Catch { 342 | Write-Error "Error - Couldn't assign user permissions to get,list the KeyVault secrets - Please review detailed error message below" 343 | $_.Exception.Message 344 | } 345 | #> 346 | } 347 | else 348 | { 349 | <# 350 | # Assign service account with the permissions to list and read Azure KeyVault secrets (to enable the connection with the Power Automate flow) 351 | Write-Host -ForegroundColor blue "Assigning 'Secrets List & Get' policy on Azure KeyVault for user $serviceAccountUPN" 352 | Try { 353 | Set-AzKeyVaultAccessPolicy -VaultName $outputs.Outputs.azKeyVaultName.Value -ResourceGroupName $rgName -UserPrincipalName $CurrentUserId -PermissionsToSecrets list,get 354 | } 355 | Catch { 356 | Write-Error "Error - Couldn't assign user permissions to get,list the KeyVault secrets - Please review detailed error message below" 357 | $_.Exception.Message 358 | } 359 | #> 360 | } 361 | 362 | 363 | Write-Host -ForegroundColor blue "Getting the Azure Function App key for warm-up test" 364 | ## lookup the resource id for your Azure Function App ## 365 | $azFuncResourceId = (Get-AzResource -ResourceGroupName $rgName -ResourceName $outputs.Outputs.azFuncAppName.Value -ResourceType "Microsoft.Web/sites").ResourceId 366 | 367 | ## compose the operation path for listing keys ## 368 | $path = "$azFuncResourceId/host/default/listkeys?api-version=2021-02-01" 369 | $result = Invoke-AzRestMethod -Path $path -Method POST 370 | 371 | if($result -and $result.StatusCode -eq 200) 372 | { 373 | ## Retrieve result from Content body as a JSON object ## 374 | $contentBody = $result.Content | ConvertFrom-Json 375 | $code = $contentBody.masterKey 376 | } 377 | else { 378 | Write-Error "Couldn't retrieve the Azure Function app master key - Warm-up tests not executed" 379 | } 380 | 381 | #Write-Host -ForegroundColor blue "Waiting 2 min to let the Azure function app to start" 382 | #Start-Sleep -Seconds 120 383 | 384 | #Write-Host -ForegroundColor blue "Warming-up Azure Function apps - This will take a few minutes" 385 | #& $base\warmup.ps1 -hostname $outputs.Outputs.azFuncHostName.Value -code $code -tenantID $tenantID -clientID $clientID -secret $clientSecret 386 | Write-Host -ForegroundColor blue "Deployment script completed" 387 | 388 | 389 | 390 | 391 | ## Assigning the correct permissions to the Managed Identity of the App 392 | $MSI = Get-AzFunctionApp -Name $outputs.Outputs.azFuncAppName.Value -ResourceGroup $rgName 393 | $AzFuncMSIServicePrincipal = Get-AzADServicePrincipal -Id $MSI.IdentityPrincipalId 394 | 395 | ## Add redirect URI and enable ID token issuance for the AAD app associated with Azure Function 396 | $webProperties = [Microsoft.Azure.PowerShell.Cmdlets.Resources.MSGraph.Models.ApiV10.IMicrosoftGraphWebApplication]@{ 397 | RedirectUri = @("https://$($MSI.DefaultHostName)/.auth/login/aad/callback") 398 | ImplicitGrantSetting = [Microsoft.Azure.PowerShell.Cmdlets.Resources.MSGraph.Models.ApiV10.IMicrosoftGraphImplicitGrantSettings]@{ 399 | EnableIdTokenIssuance = $true 400 | } 401 | } 402 | Try { 403 | Get-AzAdApplication -DisplayName $displayName | Update-AzADApplication -Web $webProperties -ErrorAction Stop 404 | Write-Host -ForegroundColor blue "Added redirect URI and enabled ID token issuance for the AAD app associated with Azure Function" 405 | } 406 | Catch { 407 | Write-Error "Azure AD application registration error - Please check your permissions in Azure AD and review detailed error description below" 408 | $_.Exception.Message 409 | } 410 | 411 | 412 | ## Connect to Graph API and retrieving Graph API id 413 | $token = Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com" 414 | Connect-MgGraph -AccessToken $token.Token #Connect-MgGraph -DeviceCode -Scopes "AppRoleAssignment.ReadWrite.All","Application.Read.All", "Directory.ReadWrite.All" 415 | $GraphServicePrincipal = Get-MgServicePrincipal -Filter "startswith(DisplayName,'Microsoft Graph')" | Select-Object -first 1 416 | 417 | # Assigning sites selected permission 418 | $PermissionName = "Sites.Selected" 419 | $AppRole = $GraphServicePrincipal.AppRoles | Where-Object {$_.Value -eq $PermissionName -and $_.AllowedMemberTypes -contains "Application"} 420 | New-MgServicePrincipalAppRoleAssignment -AppRoleId $AppRole.Id -ServicePrincipalId $MSI.identityprincipalid -ResourceId $GraphServicePrincipal.Id -PrincipalId $MSI.IdentityPrincipalId | Out-Null 421 | 422 | # Assigning group read all permissions 423 | $PermissionName = "User.Read.All" 424 | $AppRole = $GraphServicePrincipal.AppRoles | Where-Object {$_.Value -eq $PermissionName -and $_.AllowedMemberTypes -contains "Application"} 425 | New-MgServicePrincipalAppRoleAssignment -AppRoleId $AppRole.Id -ServicePrincipalId $MSI.IdentityPrincipalId -ResourceId $GraphServicePrincipal.Id -PrincipalId $MSI.IdentityPrincipalId | Out-Null 426 | 427 | # Assign delegated permissions for Custom Connector AAD App to the Azure Function App 428 | # Admin consent is required (manual step) 429 | $ScopeName = "user_impersonation" 430 | $AzFuncServicePrincipal = Get-MgServicePrincipal -Filter "startswith(DisplayName, '$DisplayName')" | Select-Object -first 1 431 | $Scope = $AzFuncServicePrincipal.Oauth2PermissionScopes | Where-Object {$_.Value -eq $ScopeName} 432 | $params = @{ 433 | RequiredResourceAccess = @( 434 | @{ 435 | ResourceAppId = $ClientId 436 | ResourceAccess = @( 437 | @{ 438 | Id = $Scope.Id 439 | Type = "Scope" 440 | } 441 | ) 442 | } 443 | ) 444 | } 445 | Update-MgApplication -ApplicationId $customConnAppObjectID -BodyParameter $params 446 | 447 | 448 | # Generating outputs 449 | $outputsData = [ordered]@{ 450 | FunctionApp = 'https://'+ $outputs.Outputs.azFuncHostName.value 451 | #FunctionKey = $outputs.Outputs.azFuncAppCode.Value 452 | #Tenant = $tenantName 453 | TenantID = $(Get-AzTenant).Id 454 | AzFunctionAADApplicationID = $clientID 455 | AzFunctionAADAppIDURI = $AppIdURI 456 | AzFunctionManagedID = $AzFuncMSIServicePrincipal.AppId 457 | CustomConnectorAADApplicationID = $customConnAppclientID 458 | CustomConnectorAADApplicationSecret = $customConnAppclientSecret 459 | KeyVaultName = $outputs.Outputs.azKeyVaultName.Value 460 | #AzFunctionIPs = $outputs.Outputs.outboundIpAddresses.Value 461 | } 462 | 463 | Write-Host -ForegroundColor magenta "Here is the information you'll need to deploy and configure the Power Application" 464 | 465 | # Disconnecting sessions 466 | disconnect-MgGraph 467 | disconnect-AzAccount 468 | 469 | $outputsData | FT -------------------------------------------------------------------------------- /Deployment/warmup.ps1: -------------------------------------------------------------------------------- 1 | Param( 2 | [Parameter(Mandatory=$true,HelpMessage="You must enter function hostname with argument -hostname")][string]$hostname, 3 | [Parameter(Mandatory=$true,HelpMessage="You must enter function code with argument -code")][string]$code, 4 | [Parameter(Mandatory=$false,HelpMessage="You must enter Azure AD tenant ID with argument -tenantID")][string]$tenantID, 5 | [Parameter(Mandatory=$false,HelpMessage="You must enter Azure AD client ID with argument -clientID")][string]$clientID, 6 | [Parameter(Mandatory=$false,HelpMessage="You must enter Azure AD secret with argument -secret")][string]$secret, 7 | [Parameter(Mandatory=$false)][int]$workers = 3, 8 | [Parameter(Mandatory=$false)][int]$maxRetry = 3 9 | ) 10 | 11 | Write-Host "Azure Function warm-up using API call" 12 | $echoUri = 'https://' + $hostname + '/api/Get-CsTeamsCallingPolicy' 13 | Write-Host $echoUri 14 | 15 | # Check if parameter to use OAuth are provided 16 | If ([string]::IsNullOrEmpty($tenantID) -OR [string]::IsNullOrEmpty($clientID) -OR [string]::IsNullOrEmpty($secret)) 17 | { 18 | Write-Error "Azure AD tenantID, clientID and secret can't be empty" 19 | return 20 | } 21 | 22 | # Get access token to query Azure Function 23 | $uri = "https://login.microsoftonline.com/" + $tenantID + "/oauth2/v2.0/token" 24 | $body = @{ 25 | 'client_id' = $clientID 26 | 'scope' = 'api://azfunc-' + $clientID + '/.default' 27 | 'client_secret' = $secret 28 | 'grant_type' = 'client_credentials' 29 | } 30 | Try { 31 | $access_token = (Invoke-RestMethod -Uri $Uri -Method 'Post' -Body $body -ContentType "application/x-www-form-urlencoded").access_token 32 | $securedToken = ConvertTo-SecureString -String $access_token -AsPlainText -Force 33 | } 34 | Catch { 35 | Write-Host $_ 36 | return 37 | } 38 | Write-Host "Access token request success" 39 | 40 | function generateConfig ([string]$hostname,[string]$code,[int]$workers,$token) { 41 | $config = @() 42 | $uri = 'https://' + $hostname + '/api/Get-CsTeamsCallingPolicy?code=' + $code 43 | for($i = 0; $i -lt $workers; $i++){ 44 | $config += New-Object -TypeName psobject -Property @{ID= $i+1; URI= $uri; token= $token} 45 | } 46 | return $config 47 | } 48 | 49 | function checkStatus($jobStatus) { 50 | $check = $true 51 | foreach ($item in $jobStatus) { 52 | if ( ($item.StatusCode -ne 200) -OR ([string]::IsNullOrEmpty($item.StatusCode))) { 53 | $check = $false 54 | } 55 | } 56 | Return $check 57 | } 58 | 59 | $retries = 0 60 | $jobresults = @() 61 | Do 62 | { 63 | Write-Host "Function warm-up started at" $(Get-Date) "- Attempt #" ($retries+1) 64 | $job = generateConfig $hostname $code $workers $securedToken | ForEach-Object -ThrottleLimit $workers -Parallel { 65 | $timeout = 180 66 | $start = Get-Date 67 | Try { 68 | $Result = Invoke-WebRequest -URI $_.URI -Method 'Get' -TimeoutSec $timeout -MaximumRetryCount 1 -Authentication OAuth -Token $_.token 69 | } 70 | Catch { 71 | If ($_.Exception.Message -notlike '*HttpClient.Timeout*') { 72 | $_.Exception.Message 73 | } 74 | } 75 | $finish = Get-Date 76 | $duration = ($finish - $start).TotalSeconds 77 | $Resp = New-Object -TypeName psobject -Property @{Duration= [Math]::Round($duration,2); StatusCode= $Result.StatusCode; StatusDescription= If($duration -gt $timeout) {"Request timed out ($timeout sec)"} Else {$Result.StatusDescription};TriggerTime= (Get-Date -DisplayHint Time);WorkerId=$_.ID} 78 | return $Resp 79 | } -AsJob 80 | $jobresult = $job | Wait-Job -Timeout 200 | Receive-Job 81 | $jobresults += $jobresult 82 | 83 | $test = checkStatus($jobresult) 84 | If ($test -EQ $FALSE) { 85 | Write-Host "Results - Attempt #" ($retries+1) 86 | $jobresult | Sort-Object TriggerTime | Format-Table TriggerTime,WorkerId,Duration,StatusCode,StatusDescription 87 | Write-Host "Sleeping for 5 min before retrying" 88 | Start-Sleep -Seconds 300 89 | } 90 | 91 | $job | Remove-Job 92 | $retries +=1 93 | } 94 | until( ($test -EQ $TRUE) -OR ($retries -ge $maxRetry)) 95 | 96 | If ($retries -ge $maxRetry) { 97 | Write-Host "Reached max retries - Function app still not warmed up - Please restart the script or check error messages" 98 | } 99 | $jobresults | Sort-Object TriggerTime | Format-Table TriggerTime,WorkerId,Duration,StatusCode,StatusDescription 100 | 101 | 102 | -------------------------------------------------------------------------------- /FunctionApp/CQS/CsvInputParser/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "Request", 8 | "methods": [ 9 | "get", 10 | "post" 11 | ] 12 | }, 13 | { 14 | "type": "http", 15 | "direction": "out", 16 | "name": "Response" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /FunctionApp/CQS/CsvInputParser/run.ps1: -------------------------------------------------------------------------------- 1 | ##PURPOSE: This functions gets called from flow when user uploads a CSV file. This functions get the CSV file content. Parses it, generates a manifest of operations (add and remove) for agents schedule. 2 | ##The schedules are added to SPO site. 3 | using namespace System.Net 4 | 5 | # Input bindings are passed in via param block. 6 | param($Request, $TriggerMetadata) 7 | $AuthentionModuleLocation = ".\Modules\GetAuthenticationToken\GetAuthenticationToken.psd1" 8 | $UpdateSchedules = ".\Modules\UpdateSchedulesManifest\UpdateScheduleEntry.psd1" 9 | Import-Module $AuthentionModuleLocation 10 | Import-Module $UpdateSchedules 11 | $authHeader = Get-AuthenticationToken 12 | Write-Host $authHeader.Authorization 13 | $runasUserUpn = $Request.Headers['X-MS-CLIENT-PRINCIPAL-NAME'] 14 | $userid = $Request.Headers['X-MS-CLIENT-PRINCIPAL-ID'] 15 | 16 | $SPOSiteId = $ENV:ShiftsMgrSPOSiteId 17 | $ManifestListId = $ENV:ShiftsMgrManifestListId 18 | $ChangeLogListId = $ENV:ShiftsMgrChangeLogListId 19 | 20 | # Write to the Azure Functions log stream. 21 | Write-Host "PowerShell HTTP trigger function processed a request." 22 | 23 | 24 | $filename = $Request.Body.filename 25 | $filecontent = $Request.Body.filecontent 26 | $body = "Filename: $filename. Content: $filecontent" 27 | $shifts = $filecontent | ConvertFrom-Csv -Delimiter ',' 28 | #[Text.Encoding]::Utf8.GetString([Convert]::FromBase64String($fc.Trim())) | ConvertFrom-Csv -Delimiter ',' 29 | $h = $shifts[0] 30 | $totalDays = $h.PSObject.Properties.Name.Length-1 31 | Write-Host "Total days: $totalDays" 32 | 33 | 34 | $allShifts = @() 35 | 1..$totalDays | % { 36 | Write-Host "Day $_ >>> $(([DateTime] $h.PSObject.Properties.Name[$_]).ToString("MM/dd/yyyy"))" 37 | $currentDay = $_ 38 | #New-Variable -Name "shiftsOfDay$currentDay" -Value @() -Force 39 | 40 | $shifts | %{ 41 | $currentAssignment = $_.Assignment 42 | $agentUpn = $_.$($h.PSObject.Properties.Name[$currentDay]) 43 | $currentDt = ([DateTime] $h.PSObject.Properties.Name[$currentDay]).ToString("MM/dd/yyyy") 44 | $nextDt = ([DateTime] $h.PSObject.Properties.Name[$currentDay]).AddDays(1).ToString("MM/dd/yyyy") 45 | <# 46 | $startTime = ([DateTime]($_.Assignment.Split(" ")[2].Split("-")[0].Trim() + "m")).ToString("HH:mm") 47 | $endTime = ([DateTime]($_.Assignment.Split(" ")[2].Split("-")[1].Trim() + "m")).ToString("HH:mm") 48 | $startHr = [int32] ([DateTime]($_.Assignment.Split(" ")[2].Split("-")[0].Trim() + "m")).ToString("HH") 49 | $endHr = [int32] ([DateTime]($_.Assignment.Split(" ")[2].Split("-")[1].Trim() + "m")).ToString("HH") 50 | #> 51 | $startTime = ([DateTime]($_.Assignment.Split("-")[0].Trim())).ToString("HH:mm") 52 | $endTime = ([DateTime]($_.Assignment.Split("-")[1].Trim())).ToString("HH:mm") 53 | $startHr = [int32] ([DateTime]($_.Assignment.Split("-")[0].Trim())).ToString("HH") 54 | $endHr = [int32] ([DateTime]($_.Assignment.Split("-")[1].Trim())).ToString("HH") 55 | $endDt = if(($endHr -gt $startHr) -or ($endHr -eq $startHr)) { 56 | $currentDt 57 | } else { 58 | $nextDt 59 | } 60 | # Process the row only if an agent is present 61 | if($agentUpn -ne "") { 62 | Write-Host $currentAssignment " " $agentUpn 63 | $shiftLinkGuid = (New-Guid).Guid #This is to associate add and remove entries 64 | 65 | #Create the 'Add agent' record 66 | $shiftInfo = @{ 67 | "Date" = $currentDt 68 | "Time" = $startTime 69 | "UPN" = $agentUpn 70 | "ActionType" = "Add" 71 | "EndDate" = $endDt 72 | "EndTime" = $endTime 73 | "StartHour" = $startHr 74 | "EndHour" = $endHr 75 | "ShiftLink" = $shiftLinkGuid 76 | } 77 | $allShifts += $shiftInfo 78 | 79 | #Create the 'Remove agent' record 80 | $shiftInfo = @{ 81 | "Date" = $endDt 82 | "Time" = $endTime 83 | "UPN" = $agentUpn 84 | "ActionType" = "Remove" 85 | "EndDate" = $endDt 86 | "EndTime" = $endTime 87 | "StartHour" = $startHr 88 | "EndHour" = $endHr 89 | "ShiftLink" = $shiftLinkGuid 90 | } 91 | $allShifts += $shiftInfo 92 | $agentUpn = "" 93 | } 94 | } 95 | } 96 | 97 | $r = @{ requests = @() } 98 | $i = 0 99 | $totalEntries = $allShifts.Length 100 | Write-Host "Total rows $totalEntries" 101 | $allShifts | %{ 102 | #Write-Host "Iteration $i" 103 | $date = $_.Date 104 | $time = $_.Time 105 | $email = $_.UPN 106 | $actionType = $_.ActionType 107 | $endDate = $_.EndDate 108 | $endTime = $_.EndTime 109 | $startHour = $_.StartHour 110 | $endHour = $_.EndHour 111 | $shiftLink = $_.ShiftLink 112 | 113 | $r.requests += @{ 114 | id = $i+1 115 | #url = "sites/m365x229910.sharepoint.com,4d5a27d4-5891-420f-8822-e29376ca4eed,b2648eb8-4d00-4bc3-b3bb-f5c96ec3ad7d/lists/ade46535-7cd3-4418-b872-5b752c830dfa/items" 116 | url = "sites/$SPOSiteId/lists/$ManifestListId/items" 117 | method = "POST" 118 | body = @{fields = @{ Title="ShiftEntry"; 119 | AgentShiftDate="$date"; 120 | Time="$time"; 121 | AgentEmail="$email"; 122 | ActionType="$actionType"; 123 | EndDate="$endDate"; 124 | EndTime="$endTime"; 125 | StartHour="$startHour"; 126 | EndHour="$endHour"; 127 | AddedVia="$filename"; 128 | AddModBy="$runasUserUpn"; 129 | ShiftLink="$shiftLink" 130 | }} 131 | headers = @{ "content-type" = "application/json" } 132 | } 133 | if ($r.requests.Count -eq 20 -or $i -eq $totalEntries-1) { 134 | $payload = ConvertTo-Json $r -Depth 4 135 | Write-Host "Batch run id $i" 136 | Write-Host $payload 137 | $result = Add-SchedulesBatch -Token $authHeader -Payload $payload -BatchId $i 138 | Write-Host $result.StatusCode 139 | Write-Host $result.Content 140 | $r.requests = @() 141 | } 142 | $i += 1 143 | } 144 | 145 | 146 | # Associate values to output bindings by calling 'Push-OutputBinding'. 147 | Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ 148 | StatusCode = [HttpStatusCode]::OK 149 | Body = $body 150 | }) 151 | -------------------------------------------------------------------------------- /FunctionApp/CQS/GetCallQueueById/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "Request", 8 | "methods": [ 9 | "get", 10 | "post" 11 | ] 12 | }, 13 | { 14 | "type": "http", 15 | "direction": "out", 16 | "name": "Response" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /FunctionApp/CQS/GetCallQueueById/run.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Net 2 | 3 | # Input bindings are passed in via param block. 4 | param($Request, $TriggerMetadata) 5 | 6 | $secpasswd = ConvertTo-SecureString -String $ENV:ShiftsMgrSvcAccountPwd -AsPlainText -Force 7 | $mycreds = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $ENV:ShiftsMgrSvcAccountId, $secpasswd 8 | 9 | $cqid = $Request.Query.Identity 10 | 11 | <# 12 | ## Use this block to test with hard-coded user id and pwd 13 | $SvcAcctPwd = "****" 14 | $SvcAcctId = "shiftmgr@mcontoso.onmicrosoft.com" 15 | $secpasswd = ConvertTo-SecureString -String $SvcAcctPwd -AsPlainText -Force 16 | $mycreds = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $SvcAcctId, $secpasswd 17 | #> 18 | 19 | Connect-MicrosoftTeams -Credential $mycreds | Out-Null 20 | $body = Get-CsCallQueue -Identity $cqid | Select Name, Identity, Agents | ConvertTo-Json 21 | Write-output "PS Result>" 22 | Write-output `n$body 23 | Disconnect-MicrosoftTeams 24 | 25 | # Associate values to output bindings by calling 'Push-OutputBinding'. 26 | Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ 27 | StatusCode = [HttpStatusCode]::OK 28 | Body = $body 29 | }) 30 | -------------------------------------------------------------------------------- /FunctionApp/CQS/GetCallQueues/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "name": "Request", 5 | "authLevel": "anonymous", 6 | "methods": [ 7 | "get", 8 | "post" 9 | ], 10 | "direction": "in", 11 | "type": "httpTrigger" 12 | }, 13 | { 14 | "type": "http", 15 | "direction": "out", 16 | "name": "Response" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /FunctionApp/CQS/GetCallQueues/run.ps1: -------------------------------------------------------------------------------- 1 | ##PURPOSE: This function does below things: 2 | ##1. Get list of Call Queue ID's that the current user (invoking this function) owns or manages. This is stored in the SPO list called CallQueueOwners. 3 | ##2. Then it connects to MS Teams, get all the call queue's along with agents. Filters the list based on #1 and returns it as JSON 4 | using namespace System.Net 5 | # Input bindings are passed in via param block. 6 | param($Request, $TriggerMetadata) 7 | 8 | $AuthentionModuleLocation = ".\Modules\GetAuthenticationToken\GetAuthenticationToken.psd1" 9 | $CallQueuesModule = ".\Modules\GetCallQueues\GetCallQueues.psd1" 10 | Import-Module $AuthentionModuleLocation 11 | Import-Module $CallQueuesModule 12 | $authHeader = Get-AuthenticationToken 13 | $runasUserUpn = $Request.Headers['X-MS-CLIENT-PRINCIPAL-NAME'] 14 | $userid = $Request.Headers['X-MS-CLIENT-PRINCIPAL-ID'] 15 | Write-output $runasUserUpn 16 | 17 | $SPOSiteId = $ENV:ShiftsMgrSPOSiteId 18 | $CallQueueOwnersListId = $ENV:ShiftsMgrCallQueueOwnersListId 19 | #https://graph.microsoft.com/v1.0/sites/{siteid}/lists/{listid}/items?expand=fields(select=OwnerEmail,CallQueueId)&$filter=fields/OwnerEmail eq 'admin@contoso.OnMicrosoft.com' 20 | $ListUrl = "https://graph.microsoft.com/v1.0/sites/$SPOSiteId/lists/$CallQueueOwnersListId/items?expand=fields(select=OwnerEmail,CallQueueId)&`$filter=fields/OwnerEmail eq '$runasUserUpn'" 21 | Write-output $ListUrl 22 | #Get list of call queues the current user must manage 23 | $GetUsersCallQueues = Get-CallQueues -Token $authHeader -ListUrlWithFilter $ListUrl 24 | $callqueueIds = @() 25 | $GetUsersCallQueues.value | %{ 26 | $callqueueIds += $_.fields.CallQueueId 27 | } 28 | Write-output "# of call queues this user manages " $callqueueIds.length 29 | $secpasswd = ConvertTo-SecureString -String $ENV:ShiftsMgrSvcAccountPwd -AsPlainText -Force 30 | $mycreds = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $ENV:ShiftsMgrSvcAccountId, $secpasswd 31 | Connect-MicrosoftTeams -Credential $mycreds | Out-Null 32 | #$body = Get-CsCallQueue | Select Name, Identity, Agents | ?{$_.Identity -in $callqueueIds} | ConvertTo-Json 33 | if($callqueueIds.length -le 1) 34 | { 35 | $body = Get-CsCallQueue | Select Name, Identity, Agents | ?{$_.Identity -in $callqueueIds} 36 | $body = ConvertTo-Json -InputObject @($body) 37 | } 38 | else 39 | { 40 | $body = Get-CsCallQueue | Select Name, Identity, Agents | ?{$_.Identity -in $callqueueIds} | ConvertTo-Json 41 | } 42 | $result = $body.Replace(", OptIn","") 43 | Write-output "PS Result>" 44 | Write-output `n$result 45 | Disconnect-MicrosoftTeams 46 | 47 | # Associate values to output bindings by calling 'Push-OutputBinding'. 48 | Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ 49 | StatusCode = [HttpStatusCode]::OK 50 | Body = $result 51 | }) 52 | -------------------------------------------------------------------------------- /FunctionApp/CQS/ListCallQueues/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "Request", 8 | "methods": [ 9 | "get", 10 | "post" 11 | ] 12 | }, 13 | { 14 | "type": "http", 15 | "direction": "out", 16 | "name": "Response" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /FunctionApp/CQS/ListCallQueues/run.ps1: -------------------------------------------------------------------------------- 1 | ## Connect to MS Teams and get all the call queue's along with agents info 2 | using namespace System.Net 3 | 4 | # Input bindings are passed in via param block. 5 | param($Request, $TriggerMetadata) 6 | 7 | $secpasswd = ConvertTo-SecureString -String $ENV:ShiftsMgrSvcAccountPwd -AsPlainText -Force 8 | $mycreds = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $ENV:ShiftsMgrSvcAccountId, $secpasswd 9 | 10 | <# 11 | #Use this block to test with hard coded credentials 12 | $SvcAcctPwd = "***" 13 | $SvcAcctId = "shiftmgr@contoso.onmicrosoft.com" 14 | $secpasswd = ConvertTo-SecureString -String $SvcAcctPwd -AsPlainText -Force 15 | $mycreds = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $SvcAcctId, $secpasswd 16 | #> 17 | 18 | Connect-MicrosoftTeams -Credential $mycreds | Out-Null 19 | $body = Get-CsCallQueue | Select Name, Identity, Agents | ConvertTo-Json 20 | #$body = $ENV:ShiftMgrCallQueueId 21 | Write-output "PS Result>" 22 | Write-output `n$body 23 | Disconnect-MicrosoftTeams 24 | 25 | 26 | # Associate values to output bindings by calling 'Push-OutputBinding'. 27 | Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ 28 | StatusCode = [HttpStatusCode]::OK 29 | Body = $body 30 | }) -------------------------------------------------------------------------------- /FunctionApp/CQS/Modules/GetAuthenticationToken/GetAuthenticationToken.psd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/FunctionApp/CQS/Modules/GetAuthenticationToken/GetAuthenticationToken.psd1 -------------------------------------------------------------------------------- /FunctionApp/CQS/Modules/GetAuthenticationToken/GetAuthenticationToken.psm1: -------------------------------------------------------------------------------- 1 | function Get-AuthenticationToken 2 | { 3 | param ($Scope) 4 | 5 | #If parameter "Scope" has not been provided, we assume that graph.microsoft.com is the target resource 6 | If (!$Scope) 7 | { 8 | $Scope = "https://graph.microsoft.com" 9 | } 10 | 11 | $tokenAuthUri = $env:IDENTITY_ENDPOINT + "?resource=$Scope&api-version=2019-08-01" 12 | $response = Invoke-RestMethod -Method Get -Headers @{"X-IDENTITY-HEADER"="$env:IDENTITY_HEADER"} -Uri $tokenAuthUri -UseBasicParsing 13 | $accessToken = $response.access_token 14 | 15 | $authHeader = @{ 16 | 'Content-Type'='application/json' 17 | 'Authorization'='Bearer ' + $accessToken 18 | } 19 | 20 | Return $authHeader 21 | } -------------------------------------------------------------------------------- /FunctionApp/CQS/Modules/GetCallQueues/GetCallQueues.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'GetGroupInfo' 3 | # 4 | 5 | @{ 6 | 7 | # Script module or binary module file associated with this manifest. 8 | RootModule = 'GetCallQueues.psm1' 9 | 10 | # Version number of this module. 11 | ModuleVersion = '1.0.0' 12 | 13 | # Copyright statement for this module 14 | Copyright = 'MIT License - Copyright (c) Microsoft Corporation 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy 17 | of this software and associated documentation files (the "Software"), to deal 18 | in the Software without restriction, including without limitation the rights 19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the Software is 21 | furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all 24 | copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | SOFTWARE. 33 | ' 34 | 35 | } -------------------------------------------------------------------------------- /FunctionApp/CQS/Modules/GetCallQueues/GetCallQueues.psm1: -------------------------------------------------------------------------------- 1 | function Get-CallQueues 2 | { 3 | #param ($Token,$Date,$Time) 4 | param ($Token, $ListUrlWithFilter) 5 | 6 | #Write-Host "Getting schedules on $Date at $Time" 7 | #$uri = "https://graph.microsoft.com/v1.0/sites/m365x229910.sharepoint.com,4d5a27d4-5891-420f-8822-e29376ca4eed,b2648eb8-4d00-4bc3-b3bb-f5c96ec3ad7d/lists/ade46535-7cd3-4418-b872-5b752c830dfa/items?expand=fields(select=Id, Title, AgentShiftDate, Time, AgentEmail, AgentUserId, ActionType, IsComplete)&`$filter=fields/AgentShiftDate eq '$Date' and fields/Time eq '$Time' and fields/IsComplete eq 0 and fields/Removed eq 0" 8 | 9 | 10 | $Token.add('Prefer', 'HonorNonIndexedQueriesWarningMayFailRandomly') 11 | $CallQueuesInfo = Invoke-RestMethod -Uri $ListUrlWithFilter -Headers $Token -Method Get 12 | 13 | Return $CallQueuesInfo 14 | } 15 | 16 | <# 17 | function Get-CallQueues 18 | { 19 | param ($Token,$Id,$IsComplete,$SPOSiteId,$ManifestListId) 20 | 21 | Write-Host "Update the item setting the IsComplete column" 22 | #$uri = "https://graph.microsoft.com/v1.0/sites/m365x229910.sharepoint.com,4d5a27d4-5891-420f-8822-e29376ca4eed,b2648eb8-4d00-4bc3-b3bb-f5c96ec3ad7d/lists/ade46535-7cd3-4418-b872-5b752c830dfa/items/$Id/fields" 23 | $uri = "https://graph.microsoft.com/v1.0/sites/$SPOSiteId/lists/$ManifestListId/items/$Id/fields" 24 | $postBody =@" 25 | { 26 | "IsComplete": $IsComplete 27 | } 28 | "@ 29 | 30 | $SchedulesInfo = Invoke-RestMethod -Uri $uri -Headers $Token -Method Patch -Body $postBody 31 | 32 | Return $SchedulesInfo 33 | } 34 | #> -------------------------------------------------------------------------------- /FunctionApp/CQS/Modules/GetGroupInfo/GetGroupInfo.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'GetGroupInfo' 3 | # 4 | 5 | @{ 6 | 7 | # Script module or binary module file associated with this manifest. 8 | RootModule = 'GetGroupInfo.psm1' 9 | 10 | # Version number of this module. 11 | ModuleVersion = '1.0.0' 12 | 13 | # Copyright statement for this module 14 | Copyright = 'MIT License - Copyright (c) Microsoft Corporation 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy 17 | of this software and associated documentation files (the "Software"), to deal 18 | in the Software without restriction, including without limitation the rights 19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the Software is 21 | furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all 24 | copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | SOFTWARE. 33 | ' 34 | 35 | } -------------------------------------------------------------------------------- /FunctionApp/CQS/Modules/GetGroupInfo/GetGroupInfo.psm1: -------------------------------------------------------------------------------- 1 | function Get-GroupObjectInfo 2 | { 3 | param ($Token,$DisplayName,$ObjectId) 4 | 5 | #$FilePath = "/audio prompts/$FileName" 6 | 7 | If($Displayname -ne $null) 8 | { 9 | $uri = "https://graph.microsoft.com/v1.0/groups?`$filter=startswith(displayName, '$DisplayName')" 10 | $ObjectId = Invoke-RestMethod -Uri $uri -Headers $Token -Method Get 11 | 12 | Return $ObjectId 13 | } 14 | ElseIf($ObjectId -ne $null) 15 | { 16 | Write-Host "Getting ObjectId: $ObjectId" 17 | $uri = "https://graph.microsoft.com/v1.0/groups/$ObjectId" 18 | $DisplayName = Invoke-RestMethod -Uri $uri -Headers $Token -Method Get 19 | 20 | Return $Displayname 21 | } 22 | } -------------------------------------------------------------------------------- /FunctionApp/CQS/Modules/GetSchedulesManifest/GetSchedulesByTime.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'GetGroupInfo' 3 | # 4 | 5 | @{ 6 | 7 | # Script module or binary module file associated with this manifest. 8 | RootModule = 'GetSchedulesByTime.psm1' 9 | 10 | # Version number of this module. 11 | ModuleVersion = '1.0.0' 12 | 13 | # Copyright statement for this module 14 | Copyright = 'MIT License - Copyright (c) Microsoft Corporation 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy 17 | of this software and associated documentation files (the "Software"), to deal 18 | in the Software without restriction, including without limitation the rights 19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the Software is 21 | furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all 24 | copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | SOFTWARE. 33 | ' 34 | 35 | } -------------------------------------------------------------------------------- /FunctionApp/CQS/Modules/GetSchedulesManifest/GetSchedulesByTime.psm1: -------------------------------------------------------------------------------- 1 | function Get-SchedulesByTime 2 | { 3 | #param ($Token,$Date,$Time) 4 | param ($Token, $ListUrlWithFilter) 5 | 6 | #Write-Host "Getting schedules on $Date at $Time" 7 | #$uri = "https://graph.microsoft.com/v1.0/sites/m365x229910.sharepoint.com,4d5a27d4-5891-420f-8822-e29376ca4eed,b2648eb8-4d00-4bc3-b3bb-f5c96ec3ad7d/lists/ade46535-7cd3-4418-b872-5b752c830dfa/items?expand=fields(select=Id, Title, AgentShiftDate, Time, AgentEmail, AgentUserId, ActionType, IsComplete)&`$filter=fields/AgentShiftDate eq '$Date' and fields/Time eq '$Time' and fields/IsComplete eq 0 and fields/Removed eq 0" 8 | 9 | 10 | $Token.add('Prefer', 'HonorNonIndexedQueriesWarningMayFailRandomly') 11 | $SchedulesInfo = Invoke-RestMethod -Uri $ListUrlWithFilter -Headers $Token -Method Get 12 | 13 | Return $SchedulesInfo 14 | } -------------------------------------------------------------------------------- /FunctionApp/CQS/Modules/GetUserInfo/GetUserInfo.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'GetGroupInfo' 3 | # 4 | 5 | @{ 6 | 7 | # Script module or binary module file associated with this manifest. 8 | RootModule = 'GetUserInfo.psm1' 9 | 10 | # Version number of this module. 11 | ModuleVersion = '1.0.0' 12 | 13 | # Copyright statement for this module 14 | Copyright = 'MIT License - Copyright (c) Microsoft Corporation 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy 17 | of this software and associated documentation files (the "Software"), to deal 18 | in the Software without restriction, including without limitation the rights 19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the Software is 21 | furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all 24 | copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | SOFTWARE. 33 | ' 34 | 35 | } -------------------------------------------------------------------------------- /FunctionApp/CQS/Modules/GetUserInfo/GetUserInfo.psm1: -------------------------------------------------------------------------------- 1 | function Get-UserInfo 2 | { 3 | param ($Token,$UserId) 4 | 5 | If($UserId -ne $null) 6 | { 7 | Write-Host "Getting userid: $UserId" 8 | $uri = "https://graph.microsoft.com/v1.0/users/$UserId" 9 | $UserInfo = Invoke-RestMethod -Uri $uri -Headers $Token -Method Get 10 | 11 | Return $UserInfo 12 | } 13 | } -------------------------------------------------------------------------------- /FunctionApp/CQS/Modules/JSON-Schemas/JSON-Schemas.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'JSON-Schemas' 3 | # 4 | 5 | @{ 6 | 7 | # Script module or binary module file associated with this manifest. 8 | RootModule = 'JSON-Schemas.psm1' 9 | 10 | # Version number of this module. 11 | ModuleVersion = '1.0.0' 12 | 13 | # Copyright statement for this module 14 | Copyright = 'MIT License - Copyright (c) Microsoft Corporation 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy 17 | of this software and associated documentation files (the "Software"), to deal 18 | in the Software without restriction, including without limitation the rights 19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the Software is 21 | furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all 24 | copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | SOFTWARE. 33 | ' 34 | 35 | } -------------------------------------------------------------------------------- /FunctionApp/CQS/Modules/JSON-Schemas/JSON-Schemas.psm1: -------------------------------------------------------------------------------- 1 | Function Get-jsonSchema (){ 2 | [CmdletBinding()] 3 | Param ( 4 | [Parameter(Mandatory)] 5 | [string[]]$schemaName 6 | ) 7 | 8 | Switch ($schemaName) { 9 | 10 | # JSON schema definition for Set-CallQueueTimeOutAction 11 | 'Set-CallQueueTimeOutAction' { Return @' 12 | { 13 | "type": "object", 14 | "title": "Set-CallQueueTimeOutAction API JSON body definition", 15 | "required": [ 16 | "Identity" 17 | ], 18 | "properties": { 19 | "Identity": { 20 | "type": "string", 21 | "title": "Specifies the identity of the call queue", 22 | "examples": [ 23 | "contoso it helpdesk queue" 24 | ] 25 | }, 26 | "TimeoutAction": { 27 | "type": "string", 28 | "title": "Specifies the action which needs to take place when the queue timed out", 29 | "examples": [ 30 | "Person in Organization", 31 | "Voice App", 32 | "External Phone Number", 33 | "Voicemail" 34 | ] 35 | }, 36 | "TimeoutTarget": { 37 | "type": ["string","number","null"], 38 | "title": "Specifies where to send the call to, this can be either a UPN or phonenumber", 39 | "examples": [ 40 | "+31301234567", 41 | "contoso-it-helpdesk@contoso.com" 42 | ] 43 | }, 44 | "TimeOutThreshold": { 45 | "type": "int", 46 | "title": "timeout threshold in seconds", 47 | "examples": [ 48 | "60" 49 | ] 50 | }, 51 | "TimeoutVoicemailTarget": { 52 | "type": "string", 53 | "title": "Specifies the O365 group where the voicemail needs to be delivered", 54 | "examples": [ 55 | "it-helpdesk@contoso.com" 56 | ] 57 | }, 58 | "TimeoutVoiceMailTranscription": { 59 | "type": "boolean", 60 | "title": "Specifies if voicemail transcription is enabled or not", 61 | "examples": [ 62 | true, 63 | false 64 | ] 65 | }, 66 | "TimeoutVoicemailTTSPrompt": { 67 | "type": "string", 68 | "title": "Specifies the voicemail greeting TTS", 69 | "examples": [ 70 | "Welcome to the second line helpdesk, since the expected wait time is longer as expected you will now have the option to leave a voicemail" 71 | ] 72 | }, 73 | "TimeoutVoicemailAudioPrompt": { 74 | "type": "string", 75 | "title": "Specifies the voicemail greeting audio file", 76 | "examples": [ 77 | "prompt.mp3" 78 | ] 79 | }, 80 | "SPSite": { 81 | "type": "string", 82 | "title": "SharePoint site containing audio files", 83 | "examples": [ 84 | "TeamsAAandCQManagement" 85 | ] 86 | } 87 | } 88 | } 89 | '@ } 90 | 91 | # JSON schema definition for Set-CallQueueOverFlowAction 92 | 'Set-CallQueueOverFlowAction' { Return @' 93 | { 94 | "type": "object", 95 | "title": "Set-CallQueueOverFlowAction API JSON body definition", 96 | "required": [ 97 | "Identity" 98 | ], 99 | "properties": { 100 | "Identity": { 101 | "type": "string", 102 | "title": "Specifies the identity of the call queue", 103 | "examples": [ 104 | "contoso it helpdesk queue" 105 | ] 106 | }, 107 | "OverflowAction": { 108 | "type": "string", 109 | "title": "Specifies the action which needs to take place when the queue timed out", 110 | "examples": [ 111 | "Person in Organization", 112 | "Voice App", 113 | "External Phone Number", 114 | "Voicemail" 115 | ] 116 | }, 117 | "OverflowTarget": { 118 | "type": ["string","number","null"], 119 | "title": "Specifies where to send the call to, this can be either a UPN or phonenumber", 120 | "examples": [ 121 | "+31301234567", 122 | "contoso-it-helpdesk@contoso.com" 123 | ] 124 | }, 125 | "OverflowThreshold": { 126 | "type": "int", 127 | "title": "overflow threshold", 128 | "examples": [ 129 | "10" 130 | ] 131 | }, 132 | "OverflowVoicemailTarget": { 133 | "type": "string", 134 | "title": "Specifies the O365 group where the voicemail needs to be delivered", 135 | "examples": [ 136 | "it-helpdesk@contoso.com" 137 | ] 138 | }, 139 | "OverflowVoicemailTranscription": { 140 | "type": "boolean", 141 | "title": "Specifies voice transcription should be on or off", 142 | "examples": [ 143 | true, 144 | false 145 | ] 146 | }, 147 | "OverflowVoicemailTTSPrompt": { 148 | "type": "string", 149 | "title": "Specifies the voicemail greeting", 150 | "examples": [ 151 | "Welcome to the second line helpdesk, since the expected wait time is longer as expected you will now have the option to leave a voicemail" 152 | ] 153 | }, 154 | "OverflowVoicemailAudioPrompt": { 155 | "type": "string", 156 | "title": "Specifies the voicemail greeting audio file", 157 | "examples": [ 158 | "greeting.mp3" 159 | ] 160 | }, 161 | "SPSite": { 162 | "type": "string", 163 | "title": "SharePoint site containing audio files", 164 | "examples": [ 165 | "TeamsAAandCQManagement" 166 | ] 167 | } 168 | } 169 | } 170 | '@ } 171 | 172 | # JSON schema definition for Set-CallQueueOverFlowThreshold 173 | 'Set-CallQueueOverFlowThreshold' { Return @' 174 | { 175 | "type": "object", 176 | "title": "Set-CallQueueTimeOutAction API JSON body definition", 177 | "required": [ 178 | "Identity" 179 | ], 180 | "properties": { 181 | "Identity": { 182 | "type": "string", 183 | "title": "Specifies the identity of the call queue", 184 | "examples": [ 185 | "contoso it helpdesk queue" 186 | ] 187 | }, 188 | "OverflowThreshold": { 189 | "type": "int", 190 | "title": " Any integer value between 0 and 200, inclusive. A value of 0 causes calls not to reach agents and the overflow action to be taken immediately.", 191 | "examples": [ 192 | "20" 193 | ] 194 | } 195 | } 196 | } 197 | '@ } 198 | 199 | # JSON schema definition for Set-CallQueueTimeOutThreshold 200 | 'Set-CallQueueTimeOutThreshold' { Return @' 201 | { 202 | "type": "object", 203 | "title": "Set-CallQueueTimeOutAction API JSON body definition", 204 | "required": [ 205 | "Identity" 206 | ], 207 | "properties": { 208 | "Identity": { 209 | "type": "string", 210 | "title": "Specifies the identity of the call queue", 211 | "examples": [ 212 | "contoso it helpdesk queue" 213 | ] 214 | }, 215 | "TimeOutThreshold": { 216 | "type": "int", 217 | "title": "Any integer value between 0 and 2700 seconds (inclusive), and is rounded to the nearest 15th interval", 218 | "examples": [ 219 | "20" 220 | ] 221 | } 222 | } 223 | } 224 | '@ } 225 | 226 | # JSON schema definition for Set-CallQueueAgentAlertTime 227 | 'Set-CallQueueAgentAlertTime' { Return @' 228 | { 229 | "type": "object", 230 | "title": "Set-CallQueueAgentAlertTime API JSON body definition", 231 | "required": [ 232 | "Identity" 233 | ], 234 | "properties": { 235 | "Identity": { 236 | "type": "string", 237 | "title": "Specifies the identity of the call queue", 238 | "examples": [ 239 | "contoso it helpdesk queue" 240 | ] 241 | }, 242 | "AgentAlertTime": { 243 | "type": "integer", 244 | "title": "Agent Alert Time", 245 | "examples": [ 246 | "30" 247 | ] 248 | } 249 | } 250 | } 251 | '@ } 252 | 253 | # JSON schema definition for Set-CallQueueGreeting 254 | 'Set-CallQueueGreeting' { Return @' 255 | { 256 | "type": "object", 257 | "title": "Set-CallQueueGreeting API JSON body definition", 258 | "required": [ 259 | "Identity" 260 | ], 261 | "properties": { 262 | "Identity": { 263 | "type": "string", 264 | "title": "Specifies the identity of the call queue", 265 | "examples": [ 266 | "contoso it helpdesk queue" 267 | ] 268 | }, 269 | "Greeting": { 270 | "type": "string", 271 | "title": "Filename of greeting prompt", 272 | "examples": [ 273 | "greeting.mp3" 274 | ] 275 | } , 276 | "GreetingType": { 277 | "type": "string", 278 | "title": "Either default or custom prompt", 279 | "examples": [ 280 | "Default", 281 | "Custom" 282 | ] 283 | }, 284 | "SPSite": { 285 | "type": "string", 286 | "title": "SharePoint site containing audio files", 287 | "examples": [ 288 | "TeamsAAandCQManagement" 289 | ] 290 | } 291 | } 292 | } 293 | '@ } 294 | 295 | # JSON schema definition for Set-CallQueueMusicOnHold 296 | 'Set-CallQueueMusicOnHold' { Return @' 297 | { 298 | "type": "object", 299 | "title": " Set-CallQueueMusicOnHold API JSON body definition", 300 | "required": [ 301 | "Identity" 302 | ], 303 | "properties": { 304 | "Identity": { 305 | "type": "string", 306 | "title": "Specifies the identity of the call queue", 307 | "examples": [ 308 | "contoso it helpdesk queue" 309 | ] 310 | }, 311 | "MoH": { 312 | "type": "string", 313 | "title": "Filename of MoH", 314 | "examples": [ 315 | "MoH.mp3" 316 | ] 317 | } , 318 | "MoHType": { 319 | "type": "string", 320 | "title": "Either default or custom", 321 | "examples": [ 322 | "Default", 323 | "Custom" 324 | ] 325 | }, 326 | "SPSite": { 327 | "type": "string", 328 | "title": "SharePoint site containing audio files", 329 | "examples": [ 330 | "TeamsAAandCQManagement" 331 | ] 332 | } 333 | } 334 | } 335 | '@ } 336 | 337 | # JSON schema definition for Set-AutoAttendantCallRouting 338 | 'Set-AutoAttendantCallRouting' { Return @' 339 | { 340 | "type": "object", 341 | "title": " Set-AutoAttendantCallRouting API JSON body definition", 342 | "required": [ 343 | "Identity" 344 | ], 345 | "properties": { 346 | "Identity": { 347 | "type": "string", 348 | "title": "Specifies the identity of the auto attendant", 349 | "examples": [ 350 | "contoso it helpdesk auto attendant" 351 | ] 352 | }, 353 | "RedirectTarget": { 354 | "type": "string", 355 | "title": "Target to redirect call to", 356 | "examples": [ 357 | "adele.vance@contoso.com", 358 | "it-helpdesk-call-queue@contoso.com" 359 | ] 360 | }, 361 | "RedirectTargetType": { 362 | "type": "string", 363 | "title": "Target type to redirect call to", 364 | "examples": [ 365 | "Disconnect", 366 | "Redirect: Person in organization" 367 | ] 368 | }, 369 | "RedirectTargetVoicemailPromptSuppression": { 370 | "type": "boolean", 371 | "title": "Target type to redirect call to", 372 | "examples": [ 373 | true, 374 | false 375 | ] 376 | }, 377 | "RoutingHours": { 378 | "type": "string", 379 | "title": "timeframe for which to change routing business or after business hours", 380 | "examples": [ 381 | "business hours", 382 | "after business hours" 383 | ] 384 | } 385 | } 386 | } 387 | '@ } 388 | 389 | # JSON schema definition for Set-AutoAttendantGreeting 390 | 'Set-AutoAttendantGreeting' { Return @' 391 | { 392 | "type": "object", 393 | "title": " Set-AutoAttendantGreeting API JSON body definition", 394 | "required": [ 395 | "Identity" 396 | ], 397 | "properties": { 398 | "Identity": { 399 | "type": "string", 400 | "title": "Specifies the identity of the auto attendant", 401 | "examples": [ 402 | "contoso it helpdesk auto attendant" 403 | ] 404 | }, 405 | "GreetingHours": { 406 | "type": "string", 407 | "title": "Either during business hours or outside of business hours", 408 | "examples": [ 409 | "business hours" 410 | ] 411 | }, 412 | "GreetingTypeBusinessHours": { 413 | "type": "string", 414 | "title": "Type of greeting", 415 | "examples": [ 416 | "audio", 417 | "text" 418 | ] 419 | }, 420 | "GreetingTextBusinessHours": { 421 | "type": "string", 422 | "title": "Text in case greeting type is set to text", 423 | "examples": [ 424 | "Welcome to Contoso IT" 425 | ] 426 | } , 427 | "GreetingAudioBusinessHours": { 428 | "type": "string", 429 | "title": "Audio file name in case greeting type is set to audio", 430 | "examples": [ 431 | "contoso-it.mp3" 432 | ] 433 | }, 434 | "GreetingTypeAfterBusinessHours": { 435 | "type": "string", 436 | "title": "Type of greeting", 437 | "examples": [ 438 | "audio", 439 | "text" 440 | ] 441 | }, 442 | "GreetingTextAfterBusinessHours": { 443 | "type": "string", 444 | "title": "Text in case greeting type is set to text", 445 | "examples": [ 446 | "Welcome to Contoso IT" 447 | ] 448 | } , 449 | "GreetingAudioAfterBusinessHours": { 450 | "type": "string", 451 | "title": "Audio file name in case greeting type is set to audio", 452 | "examples": [ 453 | "contoso-it.mp3" 454 | ] 455 | }, 456 | "GreetingHours": { 457 | "type": "string", 458 | "title": "timeframe for which to change the greeting business or after business hours", 459 | "examples": [ 460 | "business hours", 461 | "after business hours" 462 | ] 463 | }, 464 | "SPSite": { 465 | "type": "string", 466 | "title": "SharePoint site containing audio files", 467 | "examples": [ 468 | "TeamsAAandCQManagement" 469 | ] 470 | } 471 | } 472 | } 473 | '@ } 474 | 475 | # JSON schema definition for Set-AutoAttendantBusinessHours 476 | 'Set-AutoAttendantBusinessHours' { Return @' 477 | { 478 | "type": "object", 479 | "title": " Set-AutoAttendantBusinessHours API JSON body definition", 480 | "required": [ 481 | "Identity" 482 | ], 483 | "properties": { 484 | "Identity": { 485 | "type": "string", 486 | "title": "Specifies the identity of the auto attendant", 487 | "examples": [ 488 | "contoso it helpdesk auto attendant" 489 | ] 490 | }, 491 | "days": { 492 | "Monday": { 493 | "StartTime1" : 494 | { 495 | "type": "string", 496 | "title": "Specifies the starttime of timerange 1", 497 | "examples": [ 498 | "12:00" 499 | ] 500 | }, 501 | "EndTime1" : 502 | { 503 | "type": "string", 504 | "title": "Specifies the starttime of timerange 1", 505 | "examples": [ 506 | "12:00" 507 | ] 508 | }, 509 | "StartTime2" : 510 | { 511 | "type": "string", 512 | "title": "Specifies the starttime of timerange 2", 513 | "examples": [ 514 | "12:00" 515 | ] 516 | }, 517 | "EndTime2" : 518 | { 519 | "type": "string", 520 | "title": "Specifies the starttime of timerange 2", 521 | "examples": [ 522 | "12:00" 523 | ] 524 | } 525 | }, 526 | "Tuesday": { 527 | "StartTime1" : 528 | { 529 | "type": "string", 530 | "title": "Specifies the starttime of timerange 1", 531 | "examples": [ 532 | "12:00" 533 | ] 534 | }, 535 | "EndTime1" : 536 | { 537 | "type": "string", 538 | "title": "Specifies the starttime of timerange 1", 539 | "examples": [ 540 | "12:00" 541 | ] 542 | }, 543 | "StartTime2" : 544 | { 545 | "type": "string", 546 | "title": "Specifies the starttime of timerange 2", 547 | "examples": [ 548 | "12:00" 549 | ] 550 | }, 551 | "EndTime2" : 552 | { 553 | "type": "string", 554 | "title": "Specifies the starttime of timerange 2", 555 | "examples": [ 556 | "12:00" 557 | ] 558 | } 559 | }, 560 | "Wednesday": { 561 | "StartTime1" : 562 | { 563 | "type": "string", 564 | "title": "Specifies the starttime of timerange 1", 565 | "examples": [ 566 | "12:00" 567 | ] 568 | }, 569 | "EndTime1" : 570 | { 571 | "type": "string", 572 | "title": "Specifies the starttime of timerange 1", 573 | "examples": [ 574 | "12:00" 575 | ] 576 | }, 577 | "StartTime2" : 578 | { 579 | "type": "string", 580 | "title": "Specifies the starttime of timerange 2", 581 | "examples": [ 582 | "12:00" 583 | ] 584 | }, 585 | "EndTime2" : 586 | { 587 | "type": "string", 588 | "title": "Specifies the starttime of timerange 2", 589 | "examples": [ 590 | "12:00" 591 | ] 592 | } 593 | }, 594 | "Thursday": { 595 | "StartTime1" : 596 | { 597 | "type": "string", 598 | "title": "Specifies the starttime of timerange 1", 599 | "examples": [ 600 | "12:00" 601 | ] 602 | }, 603 | "EndTime1" : 604 | { 605 | "type": "string", 606 | "title": "Specifies the starttime of timerange 1", 607 | "examples": [ 608 | "12:00" 609 | ] 610 | }, 611 | "StartTime2" : 612 | { 613 | "type": "string", 614 | "title": "Specifies the starttime of timerange 2", 615 | "examples": [ 616 | "12:00" 617 | ] 618 | }, 619 | "EndTime2" : 620 | { 621 | "type": "string", 622 | "title": "Specifies the starttime of timerange 2", 623 | "examples": [ 624 | "12:00" 625 | ] 626 | }, 627 | "Friday": { 628 | "StartTime1" : 629 | { 630 | "type": "string", 631 | "title": "Specifies the starttime of timerange 1", 632 | "examples": [ 633 | "12:00" 634 | ] 635 | }, 636 | "EndTime1" : 637 | { 638 | "type": "string", 639 | "title": "Specifies the starttime of timerange 1", 640 | "examples": [ 641 | "12:00" 642 | ] 643 | }, 644 | "StartTime2" : 645 | { 646 | "type": "string", 647 | "title": "Specifies the starttime of timerange 2", 648 | "examples": [ 649 | "12:00" 650 | ] 651 | }, 652 | "EndTime2" : 653 | { 654 | "type": "string", 655 | "title": "Specifies the starttime of timerange 2", 656 | "examples": [ 657 | "12:00" 658 | ] 659 | }, 660 | "Saturday": { 661 | "StartTime1" : 662 | { 663 | "type": "string", 664 | "title": "Specifies the starttime of timerange 1", 665 | "examples": [ 666 | "12:00" 667 | ] 668 | }, 669 | "EndTime1" : 670 | { 671 | "type": "string", 672 | "title": "Specifies the starttime of timerange 1", 673 | "examples": [ 674 | "12:00" 675 | ] 676 | }, 677 | "StartTime2" : 678 | { 679 | "type": "string", 680 | "title": "Specifies the starttime of timerange 2", 681 | "examples": [ 682 | "12:00" 683 | ] 684 | }, 685 | "EndTime2" : 686 | { 687 | "type": "string", 688 | "title": "Specifies the starttime of timerange 2", 689 | "examples": [ 690 | "12:00" 691 | ] 692 | }, 693 | "Sunday": { 694 | "StartTime1" : 695 | { 696 | "type": "string", 697 | "title": "Specifies the starttime of timerange 1", 698 | "examples": [ 699 | "12:00" 700 | ] 701 | }, 702 | "EndTime1" : 703 | { 704 | "type": "string", 705 | "title": "Specifies the starttime of timerange 1", 706 | "examples": [ 707 | "12:00" 708 | ] 709 | }, 710 | "StartTime2" : 711 | { 712 | "type": "string", 713 | "title": "Specifies the starttime of timerange 2", 714 | "examples": [ 715 | "12:00" 716 | ] 717 | }, 718 | "EndTime2" : 719 | { 720 | "type": "string", 721 | "title": "Specifies the starttime of timerange 2", 722 | "examples": [ 723 | "12:00" 724 | ] 725 | }, 726 | } 727 | } 728 | } 729 | } 730 | '@ } 731 | 732 | # JSON schema definition for Add-AutoAttendantHoliday 733 | 'Add-AutoAttendantHoliday' { Return @' 734 | { 735 | "type": "object", 736 | "title": " Add-AutoAttendantHoliday API JSON body definition", 737 | "required": [ 738 | "Identity" 739 | ], 740 | "properties": { 741 | "Identity": { 742 | "type": "string", 743 | "title": "Specifies the identity of the auto attendant", 744 | "examples": [ 745 | "contoso it helpdesk auto attendant" 746 | ] 747 | }, 748 | "HolidayName": { 749 | "type": "string", 750 | "title": "Name of holidau", 751 | "examples": [ 752 | "1st Christmas day" 753 | ] 754 | }, 755 | "HolidayStartDate": { 756 | "type": "string", 757 | "title": "Start date of holiday", 758 | "examples": [ 759 | "25-12-2022" 760 | ] 761 | }, 762 | "HolidayEndDate": { 763 | "type": "string", 764 | "title": "End date of holiday", 765 | "examples": [ 766 | "26-12-2022" 767 | ] 768 | }, 769 | "HolidayGreetingType": { 770 | "type": "string", 771 | "title": "Type of greeting", 772 | "examples": [ 773 | "audio", 774 | "text" 775 | ] 776 | }, 777 | "HolidayGreetingAudio": { 778 | "type": "string", 779 | "title": "Greeting audio file", 780 | "examples": [ 781 | "christmas.mp3" 782 | ] 783 | }, 784 | "HolidayGreetingText": { 785 | "type": "string", 786 | "title": "Greeting text", 787 | "examples": [ 788 | "We wish you a merry Christmas" 789 | ] 790 | }, 791 | "HolidayRedirectTarget": { 792 | "type": "string", 793 | "title": "Target to which to forward the call", 794 | "examples": [ 795 | "adele.vance@myuclab.nl" 796 | ] 797 | }, 798 | "HolidayRedirectType": { 799 | "type": "string", 800 | "title": "Target type to which to forward the call", 801 | "examples": [ 802 | "Disconnect", 803 | "Redirect: Person in organization" 804 | ] 805 | }, 806 | "SPSite": { 807 | "type": "string", 808 | "title": "SharePoint site containing audio files", 809 | "examples": [ 810 | "TeamsAAandCQManagement" 811 | ] 812 | } 813 | } 814 | } 815 | '@ } 816 | 817 | # JSON schema definition for Set-AutoAttendantHoliday 818 | 'Set-AutoAttendantHoliday' { Return @' 819 | { 820 | "type": "object", 821 | "title": " Set-AutoAttendantHoliday API JSON body definition", 822 | "required": [ 823 | "Identity" 824 | ], 825 | "properties": { 826 | "Identity": { 827 | "type": "string", 828 | "title": "Specifies the identity of the auto attendant", 829 | "examples": [ 830 | "contoso it helpdesk auto attendant" 831 | ] 832 | }, 833 | "HolidayName": { 834 | "type": "string", 835 | "title": "Name of holidau", 836 | "examples": [ 837 | "1st Christmas day" 838 | ] 839 | }, 840 | "HolidayStartDate": { 841 | "type": "string", 842 | "title": "Start date of holiday", 843 | "examples": [ 844 | "25-12-2022" 845 | ] 846 | }, 847 | "HolidayEndDate": { 848 | "type": "string", 849 | "title": "End date of holiday", 850 | "examples": [ 851 | "26-12-2022" 852 | ] 853 | }, 854 | "HolidayGreetingType": { 855 | "type": "string", 856 | "title": "Type of greeting", 857 | "examples": [ 858 | "audio", 859 | "text" 860 | ] 861 | }, 862 | "HolidayGreetingAudio": { 863 | "type": "string", 864 | "title": "Greeting audio file", 865 | "examples": [ 866 | "christmas.mp3" 867 | ] 868 | }, 869 | "HolidayGreetingText": { 870 | "type": "string", 871 | "title": "Greeting text", 872 | "examples": [ 873 | "We wish you a merry Christmas" 874 | ] 875 | }, 876 | "HolidayRedirectTarget": { 877 | "type": "string", 878 | "title": "Target to which to forward the call", 879 | "examples": [ 880 | "adele.vance@myuclab.nl" 881 | ] 882 | }, 883 | "HolidayRedirectType": { 884 | "type": "string", 885 | "title": "Target type to which to forward the call", 886 | "examples": [ 887 | "Disconnect", 888 | "Redirect: Person in organization" 889 | ] 890 | }, 891 | "SPSite": { 892 | "type": "string", 893 | "title": "SharePoint site containing audio files", 894 | "examples": [ 895 | "TeamsAAandCQManagement" 896 | ] 897 | } 898 | } 899 | } 900 | '@ } 901 | 902 | # JSON schema definition for Remove-AutoAttendantHoliday 903 | 'Remove-AutoAttendantHoliday' { Return @' 904 | { 905 | "type": "object", 906 | "title": "Set-CallQueueAgentAlertTime API JSON body definition", 907 | "required": [ 908 | "Identity" 909 | ], 910 | "properties": { 911 | "Identity": { 912 | "type": "string", 913 | "title": "Specifies the identity of the call queue", 914 | "examples": [ 915 | "contoso it helpdesk queue" 916 | ] 917 | }, 918 | "HolidayName": { 919 | "type": "string", 920 | "title": "Holiday name", 921 | "examples": [ 922 | "1st Christmas day" 923 | ] 924 | } 925 | } 926 | } 927 | '@ } 928 | 929 | # JSON schema definition for Export-AutoAttendant 930 | 'Export-AutoAttendant' { Return @' 931 | { 932 | "type": "object", 933 | "title": "Export-AutoAttendant API JSON body definition", 934 | "required": [ 935 | "Identity" 936 | ], 937 | "properties": { 938 | "Identity": { 939 | "type": "string", 940 | "title": "Specifies the identity of the auto attendant", 941 | "examples": [ 942 | "contoso it" 943 | ] 944 | } 945 | } 946 | } 947 | '@ } 948 | 949 | # JSON schema definition for Export-CallQueue 950 | 'Export-CallQueue' { Return @' 951 | { 952 | "type": "object", 953 | "title": "Export-CallQueue API JSON body definition", 954 | "required": [ 955 | "Identity" 956 | ], 957 | "properties": { 958 | "Identity": { 959 | "type": "string", 960 | "title": "Specifies the identity of the call queue", 961 | "examples": [ 962 | "contoso it helpdesk queue" 963 | ] 964 | } 965 | } 966 | } 967 | '@ } 968 | 969 | # No match found - Return empty JSON definition 970 | Default { Return @' 971 | {} 972 | '@ } 973 | 974 | } 975 | } -------------------------------------------------------------------------------- /FunctionApp/CQS/Modules/UpdateSchedulesManifest/UpdateScheduleEntry.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'GetGroupInfo' 3 | # 4 | 5 | @{ 6 | 7 | # Script module or binary module file associated with this manifest. 8 | RootModule = 'UpdateScheduleEntry.psm1' 9 | 10 | # Version number of this module. 11 | ModuleVersion = '1.0.0' 12 | 13 | # Copyright statement for this module 14 | Copyright = 'MIT License - Copyright (c) Microsoft Corporation 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy 17 | of this software and associated documentation files (the "Software"), to deal 18 | in the Software without restriction, including without limitation the rights 19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the Software is 21 | furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all 24 | copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | SOFTWARE. 33 | ' 34 | 35 | } -------------------------------------------------------------------------------- /FunctionApp/CQS/Modules/UpdateSchedulesManifest/UpdateScheduleEntry.psm1: -------------------------------------------------------------------------------- 1 | function Update-ScheduleEntry 2 | { 3 | param ($Token,$Id,$IsComplete,$SPOSiteId,$ManifestListId) 4 | 5 | Write-Host "Update the item setting the IsComplete column" 6 | #$uri = "https://graph.microsoft.com/v1.0/sites/m365x229910.sharepoint.com,4d5a27d4-5891-420f-8822-e29376ca4eed,b2648eb8-4d00-4bc3-b3bb-f5c96ec3ad7d/lists/ade46535-7cd3-4418-b872-5b752c830dfa/items/$Id/fields" 7 | $uri = "https://graph.microsoft.com/v1.0/sites/$SPOSiteId/lists/$ManifestListId/items/$Id/fields" 8 | $postBody =@" 9 | { 10 | "IsComplete": $IsComplete 11 | } 12 | "@ 13 | 14 | $SchedulesInfo = Invoke-RestMethod -Uri $uri -Headers $Token -Method Patch -Body $postBody 15 | 16 | Return $SchedulesInfo 17 | } 18 | 19 | function Add-ShiftsChangeLog 20 | { 21 | param ($Token, $DateStr, $TimeStr, $ChangeBy, $AgentsBeforeChange, $AgentsAfterChange, $AgentsToAdd, $AgentsToRemove, $SPOSiteId, $ChangeLogListId, $CallQueueId) 22 | 23 | Write-Host "Add new entry to change log: $Date $Time" 24 | #$uri = "https://graph.microsoft.com/v1.0/sites/m365x229910.sharepoint.com,4d5a27d4-5891-420f-8822-e29376ca4eed,b2648eb8-4d00-4bc3-b3bb-f5c96ec3ad7d/lists/4a41bf5c-41a7-444b-9b0c-1d55d2a62a5e/items" 25 | $uri = "https://graph.microsoft.com/v1.0/sites/$SPOSiteId/lists/$ChangeLogListId/items" 26 | $postBody =@" 27 | { 28 | "fields": { 29 | "Title": "Timerjob|$CallQueueId", 30 | "ChangeRequestedBy": "Service Account", 31 | "Date": "$DateStr", 32 | "Time": "$TimeStr", 33 | "AgentsToAdd": "$AgentsToAdd", 34 | "AgentsToRemove": "$AgentsToRemove", 35 | "AgentsBeforeChange": "$AgentsBeforeChange", 36 | "AgentsAfterChange": "$AgentsAfterChange" 37 | } 38 | } 39 | "@ 40 | 41 | $NewLogInfo = Invoke-RestMethod -Uri $uri -Headers $Token -Method Post -Body $postBody 42 | 43 | Return $NewLogInfo 44 | } 45 | 46 | function Add-SchedulesBatch 47 | { 48 | param ($Token, $Payload, $BatchId) 49 | 50 | Write-Host "Working on batch id: $BatchId" 51 | $uri = "https://graph.microsoft.com/v1.0/`$batch" 52 | $BatchRunResult = Invoke-WebRequest -Uri $uri -Method Post -Body $Payload -Headers $Token 53 | Return $BatchRunResult 54 | } -------------------------------------------------------------------------------- /FunctionApp/CQS/ProcessShifts/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "name": "Timer", 5 | "schedule": "0 */55 * * * *", 6 | "direction": "in", 7 | "type": "timerTrigger" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /FunctionApp/CQS/ProcessShifts/readme.md: -------------------------------------------------------------------------------- 1 | # TimerTrigger - PowerShell 2 | 3 | The `TimerTrigger` makes it incredibly easy to have your functions executed on a schedule. This sample demonstrates a simple use case of calling your function every 5 minutes. 4 | 5 | ## How it works 6 | 7 | For a `TimerTrigger` to work, you provide a schedule in the form of a [cron expression](https://en.wikipedia.org/wiki/Cron#CRON_expression)(See the link for full details). A cron expression is a string with 6 separate expressions which represent a given schedule via patterns. The pattern we use to represent every 5 minutes is `0 */5 * * * *`. This, in plain text, means: "When seconds is equal to 0, minutes is divisible by 5, for any hour, day of the month, month, and day of the week". 8 | 9 | ## Learn more 10 | 11 | Documentation 12 | -------------------------------------------------------------------------------- /FunctionApp/CQS/ProcessShifts/run.ps1: -------------------------------------------------------------------------------- 1 | ##PURPOSE: This is the timer job that wakes up periodically and checks for any scheduling operations (add/remove of agents) to be executed. This done by checking the SPO list. 2 | ##Performs those operations and thus achieve the goal of managing agents without manual intervention 3 | ##Currently this function is configured to run schedules against only one call queue that is hardcoded in line#24. You must update it for it to work. 4 | ##To support multiple call queues, the code must be either updated or make a copy of this into a new function and set the call queue id 5 | # Input bindings are passed in via param block. 6 | param($Timer) 7 | 8 | # Get the current universal time in the default string format. 9 | #$currentUTCtime = Get-Date # (Get-Date).ToUniversalTime() 10 | 11 | $currentUTCtime = [System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), 'Pacific Standard Time').ToString('MM/dd/yyyy HH:mm') 12 | 13 | $AuthentionModuleLocation = ".\Modules\GetAuthenticationToken\GetAuthenticationToken.psd1" 14 | $GetSchedules = ".\Modules\GetSchedulesManifest\GetSchedulesByTime.psd1" 15 | $UpdateSchedules = ".\Modules\UpdateSchedulesManifest\UpdateScheduleEntry.psd1" 16 | Import-Module $AuthentionModuleLocation 17 | Import-Module $GetSchedules 18 | Import-Module $UpdateSchedules 19 | $authHeader = Get-AuthenticationToken 20 | 21 | #"https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com,4d5a27d4-5891-420f-8822-e29376ca4eed,b2648eb8-4d00-4bc3-b3bb-f5c96ec3ad7d/lists/ade46535-7cd3-4418-b872-5b752c830dfa/items?expand=fields(select=Id, Title, AgentShiftDate, Time, AgentEmail, AgentUserId, ActionType, IsComplete)&`$filter=fields/AgentShiftDate eq '$Date' and fields/Time eq '$Time' and fields/IsComplete eq 0 and fields/Removed eq 0" 22 | 23 | ## TODO: REPLACE WITH YOUR CALL QUEUE ID 24 | $CQid= "fc66a46c-fb4d-4e97-9931-ae52a8f47594" 25 | $SPOSiteId = $ENV:ShiftsMgrSPOSiteId 26 | $ManifestListId = $ENV:ShiftsMgrManifestListId 27 | $ChangeLogListId = $ENV:ShiftsMgrChangeLogListId 28 | $IsCompleted = "true" 29 | $Date=$currentUTCtime.split(" ")[0] 30 | $Time=$currentUTCtime.split(" ")[1] 31 | $ListUrl = "https://graph.microsoft.com/v1.0/sites/$SPOSiteId/lists/$ManifestListId/items?expand=fields(select=Id, Title, AgentShiftDate, Time, AgentEmail, AgentUserId, ActionType, IsComplete)&`$filter=fields/AgentShiftDate eq '$Date' and fields/Time eq '$Time' and fields/IsComplete eq 0 and fields/Removed eq 0 and fields/CallQueue eq '$CQid'" 32 | $SchedulesInfoObject = Get-SchedulesByTime -Token $authHeader -ListUrlWithFilter $ListUrl #-Date $Date -Time $Time -ListUrl 33 | $agentsToAdd = @() 34 | $agentsToRemvoe = @() 35 | $agentsUPNToAdd = @() 36 | $agentsUPNToRemvoe = @() 37 | $SchedulesInfoObject.value | %{ 38 | if($_.fields.ActionType -eq 'Add') { 39 | $agentsToAdd += $_.fields.AgentUserId 40 | $agentsUPNToAdd += $_.fields.AgentEmail 41 | } 42 | elseif($_.fields.ActionType -eq 'Remove') { 43 | $agentsToRemvoe += $_.fields.AgentUserId 44 | $agentsUPNToRemvoe += $_.fields.AgentEmail 45 | } 46 | } 47 | 48 | # Check if any agents must be added or removed. 49 | if ($agentsToAdd.Length + $agentsToRemvoe.Length -gt 0) 50 | { 51 | $secpasswd = ConvertTo-SecureString -String $ENV:ShiftsMgrSvcAccountPwd -AsPlainText -Force 52 | $mycreds = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $ENV:ShiftsMgrSvcAccountId, $secpasswd 53 | Connect-MicrosoftTeams -Credential $mycreds | Out-Null 54 | $cqAgentsResult = Get-CsCallQueue -Identity $CQid | Select Agents 55 | $newAgents = @() 56 | $currentAgents = @() 57 | if($cqAgentsResult.Agents.Length -gt 0) 58 | { 59 | Write-output "Consolidating with existing agents" 60 | $cqAgentsResult.Agents | % { $currentAgents += $_.ObjectId } 61 | $agentsContinuing = $currentAgents | ?{$_ -notin $agentsToRemvoe} 62 | #If agentsContinuing is empty, it means all existing agents to be removed. So just return the agentsToAdd. 63 | $newAgents = $agentsContinuing.Length -eq 0 ? $agentsToAdd : (Compare-Object $agentsContinuing $agentsToAdd -PassThru -IncludeEqual) 64 | } 65 | else 66 | { 67 | Write-output "No existing agents" 68 | $newAgents = $agentsToAdd 69 | } 70 | 71 | $agentsChangLog = [PSCustomObject]@{ 72 | CurrentAgents = $currentAgents 73 | AgentsToAdd = $agentsToAdd 74 | AgentsToRemove = $agentsToRemvoe 75 | NewAgents = $newAgents 76 | } 77 | Write-output $agentsChangLog 78 | $body = Set-CsCallQueue -Users $newAgents -Identity $CQid | Select Name, Identity, Agents | ConvertTo-Json 79 | Disconnect-MicrosoftTeams 80 | 81 | # SET THE ISCOMPELTE TO TRUE FOR THE PROCESSED ITEMS 82 | $SchedulesInfoObject.value | %{ Update-ScheduleEntry -Token $authHeader -Id $_.fields.id -IsComplete $IsCompleted -SPOSiteid $SPOSiteId -ManifestListId $ManifestListId} 83 | #Add-ShiftsChangeLog -Token $authHeader -Date $Date -Time $Time -ChangeBy "Service Account" -AgentsBeforeChange $agentsChangLog.CurrentAgents -AgentsToAdd $agentsChangLog.AgentsToAdd -AgentsToRemove $agentsChangLog.AgentsToRemove -AgentsAfterChange $agentsChangLog.NewAgents 84 | Add-ShiftsChangeLog -Token $authHeader -DateStr $Date -TimeStr $Time -ChangeBy "Service Account" -AgentsBeforeChange ($agentsChangLog.CurrentAgents -join ", ") -AgentsToAdd ($agentsChangLog.AgentsToAdd -join ", ") -AgentsToRemove ($agentsChangLog.AgentsToRemove -join ", ") -AgentsAfterChange ($agentsChangLog.NewAgents -join ", ") -SPOSiteid $SPOSiteId -ChangeLogListId $ChangeLogListId -CallQueueId $CQid 85 | } 86 | else 87 | { 88 | Write-output "No shifts entries found for the provided date time ($Date $Time) criteria. No action taken." 89 | } 90 | 91 | # The 'IsPastDue' property is 'true' when the current function invocation is later than scheduled. 92 | if ($Timer.IsPastDue) { 93 | Write-Host "PowerShell timer is running late!" 94 | } 95 | 96 | # Write an information log with the current time. 97 | Write-Host "PowerShell timer trigger function ran! TIME: $currentUTCtime" 98 | -------------------------------------------------------------------------------- /FunctionApp/CQS/UpdateCallQueueAgents/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "Request", 8 | "methods": [ 9 | "get", 10 | "post" 11 | ] 12 | }, 13 | { 14 | "type": "http", 15 | "direction": "out", 16 | "name": "Response" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /FunctionApp/CQS/UpdateCallQueueAgents/run.ps1: -------------------------------------------------------------------------------- 1 | ##PURPOSE: For the given call queue id and list of agent guid's, the script updates the call queue with the provided agents 2 | using namespace System.Net 3 | 4 | # Input bindings are passed in via param block. 5 | param($Request, $TriggerMetadata) 6 | 7 | # Write to the Azure Functions log stream. 8 | Write-Host "PowerShell HTTP trigger function processed a request." 9 | 10 | # Interact with query parameters or the body of the request. 11 | $cqid = $Request.Body.CQId 12 | $agents = $Request.Body.AgentsList 13 | 14 | $agentsList = If($agents.Trim().Length -eq 0) { $null } else { $agents.Split(", ") } 15 | 16 | $secpasswd = ConvertTo-SecureString -String $ENV:ShiftsMgrSvcAccountPwd -AsPlainText -Force 17 | $mycreds = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $ENV:ShiftsMgrSvcAccountId, $secpasswd 18 | 19 | Connect-MicrosoftTeams -Credential $mycreds | Out-Null 20 | try { 21 | Write-output "Agents List: $agentsList" 22 | $body = Set-CsCallQueue -Users $agentsList -Identity $cqid | Select Name, Identity, Agents | ConvertTo-Json 23 | } 24 | catch { 25 | Write-output $_.Exception.Message 26 | $body = "Duplicate agents list provided" 27 | } 28 | 29 | Write-output "PS Result>" 30 | Write-output `n$body 31 | Disconnect-MicrosoftTeams 32 | 33 | # Associate values to output bindings by calling 'Push-OutputBinding'. 34 | Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ 35 | StatusCode = [HttpStatusCode]::OK 36 | Body = $body 37 | }) -------------------------------------------------------------------------------- /FunctionApp/CQS/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "managedDependency": { 4 | "Enabled": true 5 | }, 6 | "extensionBundle": { 7 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 8 | "version": "[2.*, 3.0.0)" 9 | } 10 | } -------------------------------------------------------------------------------- /FunctionApp/CQS/profile.ps1: -------------------------------------------------------------------------------- 1 | # Azure Functions profile.ps1 2 | # 3 | # This profile.ps1 will get executed every "cold start" of your Function App. 4 | # "cold start" occurs when: 5 | # 6 | # * A Function App starts up for the very first time 7 | # * A Function App starts up after being de-allocated due to inactivity 8 | # 9 | # You can define helper functions, run commands, or specify environment variables 10 | # NOTE: any variables defined that are not environment variables will get reset after the first execution 11 | 12 | # Authenticate with Azure PowerShell using MSI. 13 | # Remove this if you are not planning on using MSI or Azure PowerShell. 14 | #if ($env:MSI_SECRET) { 15 | # Disable-AzContextAutosave -Scope Process | Out-Null 16 | # Connect-AzAccount -Identity 17 | #} 18 | 19 | # Uncomment the next line to enable legacy AzureRm alias in Azure PowerShell. 20 | # Enable-AzureRmAlias 21 | 22 | # You can also define functions or aliases that can be referenced in any of your PowerShell functions. -------------------------------------------------------------------------------- /FunctionApp/CQS/requirements.psd1: -------------------------------------------------------------------------------- 1 | # This file enables modules to be automatically managed by the Functions service. 2 | # See https://aka.ms/functionsmanageddependency for additional information. 3 | # 4 | @{ 5 | # For latest supported version, go to 'https://www.powershellgallery.com/packages/Az'. 6 | # To use the Az module in your function app, please uncomment the line below. 7 | # 'Az' = '8.*' 8 | 9 | # For https://www.powershellgallery.com/packages/MicrosoftTeams/4.6.1-preview 10 | 'MicrosoftTeams' = '4.6.0' 11 | } 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Media/AzFunc-Consent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/AzFunc-Consent.png -------------------------------------------------------------------------------- /Media/AzureAD-CA-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/AzureAD-CA-config.png -------------------------------------------------------------------------------- /Media/CQS-AI-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/CQS-AI-1.png -------------------------------------------------------------------------------- /Media/CQS-AI-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/CQS-AI-2.png -------------------------------------------------------------------------------- /Media/CQS-AI-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/CQS-AI-3.png -------------------------------------------------------------------------------- /Media/CQS-AgentsMgr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/CQS-AgentsMgr.png -------------------------------------------------------------------------------- /Media/CQS-High-Level-Design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/CQS-High-Level-Design.png -------------------------------------------------------------------------------- /Media/CQS-Home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/CQS-Home.png -------------------------------------------------------------------------------- /Media/CQS-PP-Connections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/CQS-PP-Connections.png -------------------------------------------------------------------------------- /Media/CQS-PPS-Import-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/CQS-PPS-Import-1.png -------------------------------------------------------------------------------- /Media/CQS-PPS-Import-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/CQS-PPS-Import-2.png -------------------------------------------------------------------------------- /Media/CQS-Schedule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/CQS-Schedule.png -------------------------------------------------------------------------------- /Media/CQS-Share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/CQS-Share.png -------------------------------------------------------------------------------- /Media/CQS-callqueueowners.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/CQS-callqueueowners.png -------------------------------------------------------------------------------- /Media/DataFlowDiagram.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/DataFlowDiagram.vsdx -------------------------------------------------------------------------------- /Media/High-Level-Design.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/High-Level-Design.jpg -------------------------------------------------------------------------------- /Media/PP-access-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/PP-access-1.png -------------------------------------------------------------------------------- /Media/PP-access-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/PP-access-2.png -------------------------------------------------------------------------------- /Media/PowerApp_ImportingSolution.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/PowerApp_ImportingSolution.jpg -------------------------------------------------------------------------------- /Media/PowerApp_SolutionImported.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/PowerApp_SolutionImported.jpg -------------------------------------------------------------------------------- /Media/SPOList_AA.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/SPOList_AA.jpg -------------------------------------------------------------------------------- /Media/SPOList_AAHolidays.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/SPOList_AAHolidays.jpg -------------------------------------------------------------------------------- /Media/SPOList_CQ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/SPOList_CQ.jpg -------------------------------------------------------------------------------- /Media/SPOList_Holidays.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/SPOList_Holidays.jpg -------------------------------------------------------------------------------- /Media/SPOList_UserManagement.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/SPOList_UserManagement.jpg -------------------------------------------------------------------------------- /Media/SPOLists.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/SPOLists.jpg -------------------------------------------------------------------------------- /Media/SPOLists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/SPOLists.png -------------------------------------------------------------------------------- /Media/custom-connector-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/custom-connector-01.png -------------------------------------------------------------------------------- /Media/custom-connector-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/custom-connector-02.png -------------------------------------------------------------------------------- /Media/custom-connector-import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/custom-connector-import.png -------------------------------------------------------------------------------- /Media/taco-logo-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/taco-logo-transparent.png -------------------------------------------------------------------------------- /Media/taco-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/taco-logo.png -------------------------------------------------------------------------------- /Media/taco-white-logo-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Media/taco-white-logo-transparent.png -------------------------------------------------------------------------------- /Pkgs/CQSCustomConnectorSolution_1_0_0_2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Pkgs/CQSCustomConnectorSolution_1_0_0_2.zip -------------------------------------------------------------------------------- /Pkgs/CQSPowerPlatSolution_1_0_0_10.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Pkgs/CQSPowerPlatSolution_1_0_0_10.zip -------------------------------------------------------------------------------- /Pkgs/CQSPowerPlatSolution_1_0_0_8.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Pkgs/CQSPowerPlatSolution_1_0_0_8.zip -------------------------------------------------------------------------------- /Pkgs/CQSPowerPlatSolution_1_0_0_9.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Pkgs/CQSPowerPlatSolution_1_0_0_9.zip -------------------------------------------------------------------------------- /Pkgs/TDME-function-artifact.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Pkgs/TDME-function-artifact.zip -------------------------------------------------------------------------------- /Pkgs/cqs-customconnector.yaml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | title: CQS Helper API 4 | description: '' 5 | version: '1.0' 6 | host: '@environmentVariables("cr8c9_CustomConnectorAzFunctionHostUrl")' 7 | basePath: / 8 | schemes: 9 | - https 10 | consumes: [] 11 | produces: [] 12 | paths: 13 | /api/ListCallQueues: 14 | get: 15 | responses: 16 | default: 17 | description: default 18 | schema: {} 19 | summary: ListCallQueues 20 | operationId: ListCallQueues 21 | x-ms-visibility: important 22 | description: List teams call queues 23 | /api/UpdateCallQueueAgents: 24 | post: 25 | responses: 26 | default: 27 | description: default 28 | schema: {} 29 | summary: UpdateSelectCallQueueAgents 30 | operationId: UpdateSelectCallQueueAgents 31 | parameters: 32 | - name: Accept 33 | in: header 34 | required: false 35 | type: string 36 | - name: body 37 | in: body 38 | required: false 39 | schema: 40 | type: object 41 | properties: 42 | CQId: 43 | type: string 44 | description: CQId 45 | AgentsList: 46 | type: string 47 | description: AgentsList 48 | description: Updates the call queue agents 49 | /api/CsvInputParser: 50 | post: 51 | responses: 52 | default: 53 | description: default 54 | schema: {} 55 | summary: InvokeCSVParser 56 | operationId: InvokeCSVParser 57 | description: InvokeCSVParser 58 | x-ms-visibility: important 59 | parameters: 60 | - name: Content-Type 61 | in: header 62 | required: false 63 | type: string 64 | - name: Accept 65 | in: header 66 | required: false 67 | type: string 68 | - name: body 69 | in: body 70 | required: false 71 | schema: 72 | type: object 73 | properties: 74 | filename: 75 | type: string 76 | description: filename 77 | filecontent: 78 | type: string 79 | description: filecontent 80 | /api/GetCallQueueById: 81 | get: 82 | responses: 83 | default: 84 | description: default 85 | schema: {} 86 | summary: GetCallQueueDetailsById 87 | operationId: GetCallQueueDetailsById 88 | parameters: 89 | - name: Identity 90 | in: query 91 | required: false 92 | type: string 93 | description: GetCallQueueDetailsById 94 | /api/TestAzFunction: {} 95 | /api/GetCallQueues: 96 | get: 97 | responses: 98 | default: 99 | description: default 100 | schema: 101 | type: array 102 | items: 103 | type: object 104 | properties: 105 | Name: 106 | type: string 107 | description: Name 108 | Identity: 109 | type: string 110 | description: Identity 111 | Agents: 112 | type: array 113 | items: 114 | type: string 115 | description: Agents 116 | summary: Get call queues details managed by current user 117 | operationId: GetCallQueuesForCurrentUser 118 | parameters: [] 119 | description: Get call queues details managed by current user 120 | definitions: {} 121 | parameters: {} 122 | responses: {} 123 | securityDefinitions: 124 | oauth2-auth: 125 | type: oauth2 126 | flow: accessCode 127 | authorizationUrl: https://login.windows.net/common/oauth2/authorize 128 | tokenUrl: https://login.windows.net/common/oauth2/authorize 129 | scopes: {} 130 | security: 131 | - oauth2-auth: [] 132 | tags: [] 133 | -------------------------------------------------------------------------------- /Pkgs/cqs-function-artifact.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/56397327405c33297c09c120ba396941f65e0eea/Pkgs/cqs-function-artifact.zip -------------------------------------------------------------------------------- /Pkgs/cqs-manager-site-lists.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 30 21 | clienttemplates.js 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 30 57 | clienttemplates.js 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 30 96 | clienttemplates.js 97 | 98 | 99 | 100 | 101 | 102 | 0 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 30 146 | 147 | clienttemplates.js 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | Add 158 | Remove 159 | 160 | 161 | 162 | 0 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 0 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 30 205 | clienttemplates.js 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /Pkgs/sample.csv: -------------------------------------------------------------------------------- 1 | "Assignment","02/17/2023","02/18/2023" 2 | "12:00PM-16:00PM","AllanD@MODERNCOMMS996974.OnMicrosoft.com","MeganB@MODERNCOMMS996974.OnMicrosoft.com" 3 | "13:00PM-19:00PM",,"MeganB@MODERNCOMMS996974.OnMicrosoft.com" 4 | "15:00PM-20:30PM","JoniS@MODERNCOMMS996974.OnMicrosoft.com", 5 | "19:00PM-23:00PM",,"DiegoS@MODERNCOMMS996974.OnMicrosoft.com" 6 | "22:00PM-05:00AM","MeganB@MODERNCOMMS996974.OnMicrosoft.com","DebraB@MODERNCOMMS996974.OnMicrosoft.com" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microsoft Teams Call Queue Scheduler 2 | 3 | | [Solution overview](https://github.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/wiki/1.-Solution-overview) |[Deployment guide](https://github.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/wiki/2.-Deployment) | [Configuration guide](https://github.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/wiki/3.-Configuration) | [FAQ](https://github.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/wiki/4.-FAQ) | [Support](https://github.com/OfficeDev/microsoft-teams-apps-call-queue-scheduler/blob/main/SUPPORT.md) | 4 | | ---- | ---- | ---- | ---- | ---- | 5 | 6 | This application allows a delegated administrator to manage the active agents for a Microsoft Teams Call Queue in real-time and/or through a defined shift schedule. 7 | 8 | [![Microsoft Teams Call Queue Scheduler screenshot](./Media/CQS-Home.png)](https://youtu.be/KQBKjXQ8_mY)(https://youtu.be/KQBKjXQ8_mY) 9 | 10 | ## About this application 11 | 12 | The Teams Admin Center (TAC) and PowerShell are the two admin interfaces to manage Call Queues. While both allow for an admin to full managability a Call Queue, this application has been developed to fill the following gaps: 13 | 14 | * ability to delegate administration of the the agents for a given Call Queue. 15 | * scheduling agents in the Call Queue for specific shifts in the future. 16 | 17 | This application provides the following capability: 18 | 19 | * Call Queue 20 | * Select a delegated Call Queue and view the list of named agents that are currently active. 21 | * Make real-time changes to the list of named agents defined in a Call Queue by a delegated administrator. 22 | * Schedule agents in shifts to allow for the automation of named agents in the Call Queue by a delegated administrator. 23 | * Schedule agents in advance by uploading a CSV file. 24 | * Modify an existing shift schedule 25 | * add an agent to the existing schedule 26 | * remove an agent from the existing schedule 27 | * modify the shift of a scheduled agent. 28 | 29 | This application works by manipulating the list of named agents in a Call Queue. This application does NOT support agents defined as part of a group or a Team channel. Since this application leverages named agents, the number of agents that can be active in the queue at the same time is limited to 20. The number of agents that could be potentially added to the queue can be much greater. 30 | 31 | The people picker presented in the app shows all the users in your organization. Due to our API limitation we cannot filter the users to show only those with enterprise voice enabled. Adding agents will be successful only if the selecter user(s) are enterprise voice enabled. You will not see any error thrown if the user is not enterprise voice enabled and you attempt to add them as an agent. 32 | 33 | See Step 3: Set up who will answer incoming calls [here](https://learn.microsoft.com/en-us/microsoftteams/create-a-phone-system-call-queue#steps-to-create-a-call-queue) for detail on the different ways agents can be defined to answer calls. 34 | 35 | > Note: This is a sample application. It does not have robust error handling to accomodate all exceptions. This includes error handling to account for the case where the 21st or greater agent is added to the queue, or to handle situations involving groups or Teams channels. 36 | 37 | The architecture of this solution can be adapted to support other scenarios that require delegated admin management of Teams phone system or any other feature accessible via PowerShell cmdlet or even MS Graph API. 38 | 39 | Here is the application running in Microsoft Teams 40 | 41 | 44 | 45 | ![Microsoft Teams Call Queue Scheduler screenshot](./Media/CQS-Schedule.png) 46 | 47 | If you want to start using the solution yourself review the Wiki for the deployment and configuration steps. 48 | 49 | ## Cost Estimates 50 | This is a costs estimates based on the public pricelist of January 2023. They do not include the costs for Office 365 & Microsoft Teams. 51 | 52 | **All prices are provided for information only.** 53 | | Service | Estimated usage | Unit price | Estimated cost / month | 54 | |---|---|---|---| 55 | | Power Platform | 20 admins (Premium connector) | $5 per user/app/month | $100 | 56 | | Azure App Service plan | EP1 SKU (1 core, 3.5GB RAM, 250GB storage) | $49.98 per instance/month | $50 | 57 | | Azure (other services) | storage and keyvault request | based on capacity (GB) & # requests | <$1 | 58 | | Total estimated ||| $150 | 59 | 60 | - [Power Apps pricing](https://powerapps.microsoft.com/en-us/pricing/) 61 | - [Azure Pricing calculator](https://azure.microsoft.com/en-us/pricing/calculator/) 62 | - [Azure AD pricing](https://azure.microsoft.com/en-us/pricing/details/active-directory/) 63 | - [Azure Functions Premium SKU pricing](https://learn.microsoft.com/en-us/azure/azure-functions/functions-premium-plan?tabs=portal) 64 | 65 | ## Contributing 66 | 67 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 68 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 69 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 70 | 71 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 72 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 73 | provided by the bot. You will only need to do this once across all repos using our CLA. 74 | 75 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 76 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 77 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 78 | 79 | ## Trademarks 80 | 81 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 82 | trademarks or logos is subject to and must follow 83 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). 84 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 85 | Any use of third-party trademarks or logos are subject to those third-party's policies. 86 | 87 | ## Legal Notice 88 | 89 | This app template is provided under the MIT License terms.  In addition to these terms, by using this app template you agree to the following: 90 | 91 | * You, not Microsoft, will license the use of your app to users or organization. 92 | * 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. 93 | * 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. 94 | * 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. 95 | * 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, and all associated requirements such as including your own privacy statement and terms of use for your app. 96 | -------------------------------------------------------------------------------- /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 definition of a security vulnerability](https://aka.ms/opensource/security/definition), 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://aka.ms/opensource/security/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 [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 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://aka.ms/opensource/security/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://aka.ms/opensource/security/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://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 6 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 7 | feature request as a new Issue. 8 | 9 | 10 | ## Contributing 11 | 12 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 13 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 14 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 15 | 16 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 17 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 18 | provided by the bot. You will only need to do this once across all repos using our CLA. 19 | 20 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 21 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 22 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 23 | 24 | ## Trademarks 25 | 26 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 27 | trademarks or logos is subject to and must follow 28 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). 29 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 30 | Any use of third-party trademarks or logos are subject to those third-party's policies. 31 | 32 | ## Legal Notice 33 | 34 | This app template is provided under the MIT License terms.  In addition to these terms, by using this app template you agree to the following: 35 | 36 | * You, not Microsoft, will license the use of your app to users or organization. 37 | * 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. 38 | * 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. 39 | * 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. 40 | * 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, and all associated requirements such as including your own privacy statement and terms of use for your app. 41 | --------------------------------------------------------------------------------