Functions/Public/IntegrationsOther.ps1

# WebRTC, Genesys, Cisco, and Cloud Agent functions

Function Get-NectarCiscoCluster {
    <#
        .SYNOPSIS
        Return base information about all Cisco clusters
         
        .DESCRIPTION
        Return base information about all Cisco clusters
 
        .PARAMETER ClusterName
        Return the cluster matching the given name
 
        .PARAMETER TimePeriod
        The time period to show event data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK'.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarCiscoCluster
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$ClusterName,        
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_DAY',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
                $TenantName = $Global:NectarTenantName 
            } ElseIf ($TenantName) {
                If ($TenantName -NotIn $Global:NectarTenantList) {
                    $TList = $Global:NectarTenantList -join ', '
                    Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
                }
            }
    
            $FilterSession = Set-NectarFilterParams -Scope DEFAULT -Platform CISCO -TenantName $TenantName -TimePeriod $TimePeriod

            $URI = "https://$Global:NectarCloud/dapi/platform/clusters?pageSize=$PageSize&platform=CISCO&tenant=$TenantName"
            Write-Verbose $URI        
            
            $Results = (Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader -WebSession $FilterSession).elements

            If ($TenantName) {$Results | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}

            If ($ClusterName) {
                $Results = $Results | Where-Object {$_.name -like "*$ClusterName*"}
            }
            
            Return $Results
        }
        Catch {
            Write-Error "Could not get platform items. $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}


Function Get-NectarCiscoClusterInventory {
    <#
        .SYNOPSIS
        Return inventory information for a given Cisco cluster
         
        .DESCRIPTION
        Return inventory information for a given Cisco cluster
 
        .PARAMETER ClusterID
        The Nectar-defined ClusterID for the given cluster. Accepts pipelined results
 
        .PARAMETER InventorySet
        What specified inventory set to pull information about
 
        .PARAMETER SearchQuery
        A string to search for. Will search for match against all fields
         
        .PARAMETER OrderByField
        Sort the output by the selected field
         
        .PARAMETER OrderDirection
        Sort ordered output in ascending or descending order
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
     
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 10000
         
        .PARAMETER ResultSize
        The total number of results to return. Maximum result size is 9,999,999 results
     
        .EXAMPLE
        Get-NectarCiscoClusterInventory -ClusterID 39_1 -InventorySet CallManagers
        Returns information about the call managers for the specified clusterID
 
        .EXAMPLE
        Get-NectarCiscoCluster | Get-NectarCiscoClusterInventory -InventorySet Gateways
        Returns information about gateways from all Cisco clusters
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$ClusterID,
        [Parameter(Mandatory=$True)]
        [ValidateSet('Annunciators','ApplicationServers','AssignedPresenceServers','CallManagers','ConferenceBridges','DevicePools','Gatekeepers','Gateways','GatewayEndpoints','H323Gateways','HuntLists','HuntPilots','InteractiveVoiceResponses','LicenseUsers','Locations','Mtp','MusicOnholdServers','Phones','RecordingProfiles','Services','SipTrunk','Transcoders','Users','VoiceMailPilots','VoiceMailPorts', IgnoreCase=$True)]
        [string]$InventorySet,
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,        
        [Parameter(Mandatory=$False)]
        [string]$OrderByField = 'idx',
        [Parameter(Mandatory=$False)]
        [ValidateSet('asc', 'desc', IgnoreCase=$True)]
        [string]$OrderDirection = 'asc',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,999999)]
        [int]$PageSize = 10000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,999999)]
        [int]$ResultSize,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Name
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
            $TenantName = $Global:NectarTenantName 
        }

        # The URLs for each inventory set is usually split on capital letters
        # This little function takes care of that
        # EG: MySampleTests turns into /my/sample/tests
        $InventoryURL = ($InventorySet -CReplace '([A-Z])', '/$1').ToLower()

        # Take care of any cases that don't match the above rule
        Switch ($InventorySet) {
            'HuntLists' { $InventoryURL = '/hunts'; Break}
        }

        # Set the page size to the result size if -ResultSize switch is used to limit the number of returned items
        # Otherwise, set page size (defaults to 1000)
        If ($ResultSize) { $PageSize = $ResultSize }

        $Params = @{
            'pageNumber'         = 1
            'pageSize'            = $PageSize
            'orderByField'        = $OrderByField
            'orderDirection'    = $OrderDirection
            'platform'            = 'CISCO'
            'tenant'            = $TenantName                
        }

        If ($SearchQuery) { $Params.Add('q', $SearchQuery) }

        $URI = "https://$Global:NectarCloud/dapi/platform/cluster/$ClusterID/inventory$InventoryURL"
        Write-Verbose $URI        
        
        $Results = Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader -Body $Params
        
        $TotalPages = $Results.totalPages

        If ($TenantName) { $Results.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty }

        If ($Name) { $Results.elements | Add-Member -Name 'ClusterName' -Value $Name -MemberType NoteProperty }
        $Results.elements

        If ($TotalPages -gt 1 -and !($ResultSize)) {
            $PageNum = 2
            Write-Verbose "Page size: $PageSize"
            While ($PageNum -le $TotalPages) {
                Write-Verbose "Working on page $PageNum of $TotalPages"
                $Params.PageNumber = $PageNum
                $Results = Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader -Body $Params
                If ($TenantName) {$Results.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
                If ($Name) { $Results.elements | Add-Member -Name 'ClusterName' -Value $Name -MemberType NoteProperty }
                $Results.elements
                $PageNum++
            }
        }
    }
}





#################################################################################################################################################
# #
# Platform Functions #
# #


Function New-NectarGenesysConfig {
    <#
        .SYNOPSIS
        Enables Genesys Cloud integration with Nectar DXP
         
        .DESCRIPTION
        Enables Genesys Cloud integration with Nectar DXP. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER DisplayName
        The display name for the configuration. Defaults to 'Genesys Cloud tenantname'. Can only contain letters, numbers and spaces.
         
        .PARAMETER AWSEventBus
        The AWS Event bus URL for the application.
         
        .PARAMETER AWSRegion
        The AWS cloud region where the event bus resides
         
        .PARAMETER GenesysOAuthClientID
        The OAuth Client ID of the Genesys Cloud application.
         
        .PARAMETER GenesysOAuthClientSecret
        The OAuth Client secret of the Genesys Cloud application.
                 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [ValidatePattern("^[a-zA-Z0-9 ]+$")]
        [Parameter(ValueFromPipelineByPropertyName, HelpMessage = "Display name can only contain letters, numbers and spaces", Mandatory=$False)]
        [string]$DisplayName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$AWSEventBus,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)][ValidateSet(
            "us-east-1", "us-east-2", "us-west-1", "us-west-2",
            "af-south-1", "ap-east-1", "ap-south-1", "ap-south-2",
            "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", "ap-southeast-4",
            "ap-northeast-1", "ap-northeast-2", "ap-northeast-3",
            "ca-central-1", "ca-west-1",
            "eu-central-1", "eu-central-2",
            "eu-west-1", "eu-west-2", "eu-west-3",
            "eu-north-1", "eu-south-1", "eu-south-2",
            "il-central-1",
            "me-central-1", "me-south-1",
            "sa-east-1",
            "us-gov-east-1", "us-gov-west-1",
            "cn-north-1", "cn-northwest-1"
        )]
        [string]$AWSRegion,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$GenesysOAuthClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$GenesysOAuthClientSecret,
        [Parameter(Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
                $TenantName = $Global:NectarTenantName 
            } ElseIf ($TenantName) {
                If ($TenantName -NotIn $Global:NectarTenantList) {
                    $TList = $Global:NectarTenantList -join ', '
                    Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
                }
            }

        $CloudAgentList = Get-NectarCloudAgent

        If ($CloudAgentList.Count -gt 1) {
            $CloudAgentName = Select-FromList -Prompt 'Multiple cloud agents found. Pick the desired cloud agent from the below list:' -Items $CloudAgentList.cloudAgentName
        }
        Else {
            $CloudAgentName = $CloudAgentList[0].cloudAgentName
        }

        If (!$PSBoundParameters.ContainsKey('DisplayName')) { $DisplayName = "Genesys Cloud $TenantName" }

        # Build the JSON body for creating the config
        $Body = @{
            awsApiKey            = ''
            awsEventBus            = $AWSEventBus
            cloudAgentName        = $CloudAgentName
            cloudRegion            = $AWSRegion.Replace('-','_')
            displayName         = $DisplayName
            oauthClientId        = $GenesysOAuthClientID
            oauthClientSecret    = $GenesysOAuthClientSecret
            tenant                 = $TenantName
        }

        $URI = "https://$($Global:NectarCloud)/aapi/clouddatasources/configuration/genesys?tenant=$TenantName"
        Write-Verbose $URI
        
        $JSONBody = $Body | ConvertTo-Json
        Write-Verbose $JSONBody

        $JSON = Invoke-RestMethod -Method POST -URI $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json'
        Return $JSON.data
    }
}


Function Test-NectarGenesysConnectivity {
    <#
        .SYNOPSIS
        Tests whether a tenant's Genesys Cloud OAuth client can authenticate and access the APIs Nectar DXP relies on.
 
        .DESCRIPTION
        Retrieves a tenant's Genesys Cloud OAuth Client ID and Secret via Get-NectarTenantDatasourceDetails, then obtains
        an OAuth client-credentials access token from Genesys Cloud and exercises a set of key Platform API endpoints
        (organization, users, groups, stations, telephony edges, telephony trunks, conversation analytics) to confirm
        the client has the access Nectar DXP needs to monitor the tenant's Genesys Cloud environment.
 
        .PARAMETER ID
        The Nectar DXP datasource ID for the tenant's Genesys Cloud integration. Accepts pipelined results from Get-NectarTenantDatasource.
 
        .PARAMETER Region
        The Genesys Cloud domain to authenticate against, eg. mypurecloud.com, usw2.pure.cloud, mypurecloud.ie, mypurecloud.de, mypurecloud.jp, mypurecloud.com.au.
        Genesys Cloud OAuth clients are region-specific. If not specified, this is derived from the datasource's cloudRegion field (returned by Get-NectarTenantDatasourceDetails).
        If cloudRegion can't be mapped to a known Genesys Cloud domain, this falls back to mypurecloud.com. Override explicitly if that's incorrect - the domain can be found in the browser address bar when logged into the org's Genesys Cloud (eg. apps.mypurecloud.ie).
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER API
        Instead of running the full PASS/FAIL test suite, query a single API and return up to the first 10 items it returns, so the data itself can be eyeballed. Select from Organization, Users, Groups, Stations, TelephonyEdges, TelephonyTrunks, ConversationsAnalytics.
        The OAuth token is still obtained first, since the API call depends on it. If omitted, the full PASS/FAIL test suite is run instead.
 
        .PARAMETER HideOutput
        Suppresses the PASS/FAIL console output. The results are still returned as an object. Has no effect when -API is used.
 
        .EXAMPLE
        Get-NectarTenantDatasource -TenantName contoso | Where-Object {$_.dataSource -eq 'genesys'} | Test-NectarGenesysConnectivity -Region usw2.pure.cloud
        Tests Genesys Cloud API access for the contoso tenant's configured Genesys Cloud integration.
 
        .EXAMPLE
        Test-NectarGenesysConnectivity -ID 42 -TenantName contoso
 
        .EXAMPLE
        Test-NectarGenesysConnectivity -ID 42 -TenantName contoso -API Stations
        Returns up to the first 10 stations Genesys Cloud reports, to verify the data is actually there.
 
        .NOTES
        Version 1.0
    #>


    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('DatasourceID')]
        [int]$ID,
        [Parameter(Mandatory=$False)]
        [string]$Region = 'mypurecloud.com',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateSet('Organization','Users','Groups','Stations','TelephonyEdges','TelephonyTrunks','ConversationsAnalytics')]
        [string]$API,
        [Parameter(Mandatory=$False)]
        [switch]$HideOutput
    )

    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) {
            $TenantName = $Global:NectarTenantName
        } ElseIf ($TenantName) {
            If ($TenantName -NotIn $Global:NectarTenantList) {
                $TList = $Global:NectarTenantList -join ', '
                Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
            }
        }

        $GenesysConfig = Get-NectarTenantDatasourceDetails -ID $ID -DatasourceType genesys -TenantName $TenantName
        Write-Verbose "Retrieved Genesys Cloud datasource details for ID $ID on tenant $($TenantName): $($GenesysConfig | ConvertTo-Json -Depth 5)"

        If (!$GenesysConfig.oauthClientId -Or !$GenesysConfig.oauthClientSecret) {
            Throw "Could not retrieve a Genesys Cloud OAuth Client ID/Secret for datasource ID $ID on tenant $TenantName."
        }

        # Maps the AWS region a Genesys Cloud org is deployed in (returned as cloudRegion, eg. 'us_east_1') to that org's login/API domain
        # Source: https://help.genesys.cloud/articles/aws-regions-for-genesys-cloud-deployment/
        $GenesysRegionMap = @{
            'us-east-1'      = 'mypurecloud.com'
            'us-east-2'      = 'use2.us-gov-pure.cloud'
            'us-west-2'      = 'usw2.pure.cloud'
            'ca-central-1'   = 'cac1.pure.cloud'
            'sa-east-1'      = 'sae1.pure.cloud'
            'mx-central-1'   = 'mxc1.pure.cloud'
            'eu-central-1'   = 'mypurecloud.de'
            'eu-west-1'      = 'mypurecloud.ie'
            'eu-west-2'      = 'euw2.pure.cloud'
            'eu-central-2'   = 'euc2.pure.cloud'
            'eusc-de-east-1' = 'edee1.eusc-pure.cloud'
            'me-central-1'   = 'mec1.pure.cloud'
            'ap-south-1'     = 'aps1.pure.cloud'
            'ap-northeast-1' = 'mypurecloud.jp'
            'ap-northeast-2' = 'apne2.pure.cloud'
            'ap-northeast-3' = 'apne3.pure.cloud'
            'ap-southeast-1' = 'apse1.pure.cloud'
            'ap-southeast-2' = 'mypurecloud.com.au'
        }

        If (!$PSBoundParameters.ContainsKey('Region')) {
            $CloudRegionKey = $GenesysConfig.cloudRegion -Replace '_', '-'
            If ($GenesysRegionMap.ContainsKey($CloudRegionKey)) {
                $Region = $GenesysRegionMap[$CloudRegionKey]
            }
            ElseIf ($GenesysConfig.cloudRegion) {
                Write-Warning "Could not map datasource cloudRegion '$($GenesysConfig.cloudRegion)' to a known Genesys Cloud domain. Falling back to $Region - pass -Region explicitly if this is incorrect."
            }
        }

        $APIRoot = "https://api.$Region/api/v2"

        # When returning actual data for a single API, pull back up to 10 items instead of just enough to prove connectivity
        $ItemLimit = If ($API) { 10 } Else { 1 }

        # Key APIs used by Nectar DXP for Genesys Cloud monitoring
        # ResultProperty is the property holding the list of items in that API's response, used when returning data for a single -API
        $Endpoints = [ordered]@{
            Organization            = @{ Method = 'GET'; URI = "$APIRoot/organizations/me"; ResultProperty = $Null }
            Users                    = @{ Method = 'GET'; URI = "$APIRoot/users?pageSize=$ItemLimit"; ResultProperty = 'entities' }
            Groups                    = @{ Method = 'GET'; URI = "$APIRoot/groups?pageSize=$ItemLimit"; ResultProperty = 'entities' }
            Stations                = @{ Method = 'GET'; URI = "$APIRoot/stations?pageSize=$ItemLimit"; ResultProperty = 'entities' }
            TelephonyEdges            = @{ Method = 'GET'; URI = "$APIRoot/telephony/providers/edges?pageSize=$ItemLimit"; ResultProperty = 'entities' }
            TelephonyTrunks            = @{ Method = 'GET'; URI = "$APIRoot/telephony/providers/edges/trunks?pageSize=$ItemLimit"; ResultProperty = 'entities' }
            ConversationsAnalytics    = @{
                Method = 'POST'
                URI = "$APIRoot/analytics/conversations/details/query"
                ResultProperty = 'conversations'
                Body = @{
                    interval = "$((Get-Date).AddDays(-1).ToString('yyyy-MM-ddT00:00:00.000Z'))/$((Get-Date).ToString('yyyy-MM-ddT00:00:00.000Z'))"
                    paging = @{ pageSize = $ItemLimit; pageNumber = 1 }
                } | ConvertTo-Json
            }
        }

        # Test only the requested API, if one was specified - otherwise run the full suite
        If ($API) { $Endpoints = [ordered]@{ $API = $Endpoints[$API] } }

        $AccessResults = [pscustomobject][ordered]@{ [string]'OAuthToken' = 'FAIL' }
        ForEach ($Key in $Endpoints.Keys) { $AccessResults | Add-Member -MemberType NoteProperty -Name $Key -Value 'FAIL' }

        If ($TenantName -And !$HideOutput) { Write-Host "TenantName: $TenantName - Genesys Region: $Region" }

        # Get an OAuth access token via the client credentials grant
        Try {
            $ClientDetailsText = "$($GenesysConfig.oauthClientId):$($GenesysConfig.oauthClientSecret)"
            $ClientDetails64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($ClientDetailsText))

            $AuthHeaders = @{
                Authorization = "Basic $ClientDetails64"
                'Content-Type' = 'application/x-www-form-urlencoded'
            }
            $AuthBody = @{ grant_type = 'client_credentials' }

            $AuthURI = "https://login.$Region/oauth/token"
            Write-Verbose $AuthURI

            If (!$HideOutput) { Write-Host 'OAuth Token Status: ' -NoNewLine }

            $AuthResponse = Invoke-RestMethod -Method POST -URI $AuthURI -Headers $AuthHeaders -Body $AuthBody

            If (!$HideOutput) { Write-Host 'PASS' -ForegroundColor Green }
            $AccessResults.OAuthToken = 'PASS'
        }
        Catch {
            If (!$HideOutput) {
                Write-Host 'FAIL' -ForegroundColor Red
                Get-JSONErrorStream -JSONResponse $_
            }
        }

        If (!$AuthResponse.access_token) {
            Write-Error "Could not obtain a Genesys Cloud access token for datasource ID $ID on tenant $TenantName. Verify the OAuth client credentials are correct and that -Region ($Region) matches the org's Genesys Cloud domain."
            Return $AccessResults
        }

        $Headers = @{ Authorization = "Bearer $($AuthResponse.access_token)" }

        # -API was specified - query just that one API and return the actual data instead of a PASS/FAIL summary
        If ($API) {
            $Endpoint = $Endpoints[$API]
            Try {
                Write-Verbose $Endpoint.URI

                If ($Endpoint.Method -eq 'POST') {
                    $Response = Invoke-RestMethod -Method POST -URI $Endpoint.URI -Headers $Headers -Body $Endpoint.Body -ContentType 'application/json'
                }
                Else {
                    $Response = Invoke-RestMethod -Method GET -URI $Endpoint.URI -Headers $Headers
                }

                If ($Endpoint.ResultProperty) {
                    Return $Response.($Endpoint.ResultProperty) | Select-Object -First 10
                }
                Else {
                    Return $Response
                }
            }
            Catch {
                Write-Error "Failed to query the $API API. $($_.Exception.Message)"
                Get-JSONErrorStream -JSONResponse $_
                Return
            }
        }

        ForEach ($Endpoint in $Endpoints.GetEnumerator()) {
            Try {
                If (!$HideOutput) { Write-Host "$($Endpoint.Key) Status:$(' ' * [Math]::Max(1, 24 - $Endpoint.Key.Length))" -NoNewLine }
                Write-Verbose $Endpoint.Value.URI

                If ($Endpoint.Value.Method -eq 'POST') {
                    $NULL = Invoke-RestMethod -Method POST -URI $Endpoint.Value.URI -Headers $Headers -Body $Endpoint.Value.Body -ContentType 'application/json'
                }
                Else {
                    $NULL = Invoke-RestMethod -Method GET -URI $Endpoint.Value.URI -Headers $Headers
                }

                If (!$HideOutput) { Write-Host 'PASS' -ForegroundColor Green }
                $AccessResults.($Endpoint.Key) = 'PASS'
            }
            Catch {
                If (!$HideOutput) { Write-Host 'FAIL' -ForegroundColor Red }
            }
        }
        Return $AccessResults
    }
}



#################################################################################################################################################
# #
# Zoom Functions #
# #


Function Get-NectarWebRTCConfig {
    <#
        .SYNOPSIS
        Return information about an existing WebRTC call data integration with Nectar DXP
         
        .DESCRIPTION
        Return information about an existing WebRTC call data integration with Nectar DXP. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarWebRTCConfig
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
                $TenantName = $Global:NectarTenantName 
            } ElseIf ($TenantName) {
                If ($TenantName -NotIn $Global:NectarTenantList) {
                    $TList = $Global:NectarTenantList -join ', '
                    Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
                }
            }

        # Get the existing WebRTC configuration
        $URI = "https://$Global:NectarCloud/aapi/configuration/webrtc?tenant=$TenantName"        
        Write-Verbose $URI
        
        $Body = (Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader).data

        Return $Body
    }
}


Function Set-NectarWebRTCConfig {
    <#
        .SYNOPSIS
        Modify an existing WebRTC call data integration with Nectar DXP
         
        .DESCRIPTION
        Modify an existing WebRTC call data integration with Nectar DXP. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER Platforms
        One or more platforms to pull WebRTC call data from
 
        .PARAMETER DisplayName
        The internal display name of the WebRTC configuration
         
        .PARAMETER RTCCollectionIntervalSecs
        How often (in seconds) to sample call quality data. Defaults to 60
         
        .PARAMETER LoginRefreshIntervalSecs
        How often (in seconds) to refresh login credentials for client WebRTC connections. Defaults to 86400 (1 day).
                 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
                 
        .EXAMPLE
        Set-NectarWebRTCConfig -CertID
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Teams','Zoom','AirBnB','CiscoWebEx', IgnoreCase=$False)]
        [string[]]$Platforms,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$DisplayName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$RTCCollectionIntervalSecs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$LoginRefreshIntervalSecs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
                $TenantName = $Global:NectarTenantName 
            } ElseIf ($TenantName) {
                If ($TenantName -NotIn $Global:NectarTenantList) {
                    $TList = $Global:NectarTenantList -join ', '
                    Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
                }
            }

        # Get the existing WebRTC config details
        $Body = (Get-NectarWebRTCConfig)[0]
        $URI = "https://$Global:NectarCloud/aapi/configuration/webrtc/$($Body.id)"
        Write-Verbose $URI
        
        # Remove extraneous details
        $Body.psobject.Properties.Remove('Id')
        $Body.psobject.Properties.Remove('stateId')
        $Body.psobject.Properties.Remove('lastUpdatedTime')
        $Body.psobject.Properties.Remove('clientId')
        $Body.psobject.Properties.Remove('kafkaTopic')
        $Body.psobject.Properties.Remove('sourceId')
        $Body | Add-Member -MemberType NoteProperty -Name 'tenant' -Value $TenantName
        
        # Update with entered parameters
        ForEach ($Param in $PSBoundParameters.GetEnumerator()) {
            $Body.($Param.key) = $Param.value
        }
        
        $JSONBody = $Body | ConvertTo-Json
        Write-Verbose $JSONBody
        
        If ($PSCmdlet.ShouldProcess(("Updating Nectar DXP WebRTC config on tenant {0}" -f $TenantName), ("Update Nectar DXP WebRTC config on tenant {0}?" -f $TenantName), 'Nectar DXP WebRTC Config Update')) {
            Try {
                $NULL = Invoke-RestMethod -Method PUT -URI $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            }
            Catch {
                If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
            }
        }
    }
}


Function New-NectarWebRTCConfig {
    <#
        .SYNOPSIS
        Enables WebRTC call data integration with Nectar DXP
         
        .DESCRIPTION
        Enables WebRTC call data integration with Nectar DXP. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER Platforms
        One or more platforms to pull WebRTC call data from
 
        .PARAMETER DisplayName
        The internal display name of the WebRTC configuration
         
        .PARAMETER RTCCollectionIntervalSecs
        How often (in seconds) to sample call quality data. Defaults to 60
         
        .PARAMETER LoginRefreshIntervalSecs
        How often (in seconds) to refresh login credentials for client WebRTC connections. Defaults to 86400 (1 day).
                 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        New-NectarWebRTCConfig
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [ValidateSet('Teams','Zoom','AirBnB','CiscoWebEx', IgnoreCase=$False)]
        [string[]]$Platforms,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$DisplayName = 'WebRTC',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$RTCCollectionIntervalSecs = 60,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$LoginRefreshIntervalSecs = 86400,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
                $TenantName = $Global:NectarTenantName 
            } ElseIf ($TenantName) {
                If ($TenantName -NotIn $Global:NectarTenantList) {
                    $TList = $Global:NectarTenantList -join ', '
                    Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
                }
            }
        
        $URI = "https://$Global:NectarCloud/aapi/configuration/webrtc"
        Write-Verbose $URI
        
        # Check for existing Teams/Azure config
        # Will increment the sourceID if one already exists
        [int]$WebRTCSourceID = ((Get-NectarWebRTCConfig -ErrorAction:SilentlyContinue).sourceID | Measure-Object -Maximum).Maximum + 1
        
        # Build the JSON body for creating the config
        $Body = @{
            tenant = $TenantName
            platforms = $Platforms
            displayName = $DisplayName
            rtcCollectionIntervalSecs = $RTCCollectionIntervalSecs
            loginRefreshIntervalSecs = $LoginRefreshIntervalSecs
            sourceId = $WebRTCSourceID
        }
        
        $JSONBody = $Body | ConvertTo-Json
        Write-Verbose $JSONBody
        
        Try {
            $NULL = Invoke-RestMethod -Method POST -URI $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
        }
        Catch {
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}


Function Remove-NectarWebRTCConfig {
    <#
        .SYNOPSIS
        Removes an existing WebRTC call data integration from Nectar DXP
         
        .DESCRIPTION
        Removes an existing WebRTC call data integration from Nectar DXP. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Remove-NectarWebRTCConfig -TenantName contoso
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
                $TenantName = $Global:NectarTenantName 
            } ElseIf ($TenantName) {
                If ($TenantName -NotIn $Global:NectarTenantList) {
                    $TList = $Global:NectarTenantList -join ', '
                    Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
                }
            }

        # Get the existing WebRTC config details
        $ExistingConfig = (Get-NectarWebRTCConfig)[0]
        $URI = "https://$Global:NectarCloud/aapi/configuration/webrtc/$($ExistingConfig.id)"
        Write-Verbose $URI
        
        $Body = @{
            tenant = $TenantName
        }
        
        $JSONBody = $Body | ConvertTo-JSON
        Write-Verbose $JSONBody
        
        If ($PSCmdlet.ShouldProcess(("Deleting Nectar DXP WebRTC config on tenant {0}" -f $TenantName), ("Delete Nectar DXP WebRTC config on tenant {0}?" -f $TenantName), 'Nectar DXP WebRTC Config Deletion')) {
            Try {
                $NULL = Invoke-RestMethod -Method DELETE -URI $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            }
            Catch {
                If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
            }
        }
    }
}





#################################################################################################################################################
# #
# Supporting Admin Functions #
# #


Function Get-NectarCloudAgent {
    <#
        .SYNOPSIS
        Returns a list of cloud agents for a given tenant from Nectar DXP
 
        .DESCRIPTION
        Returns a list of cloud agents for a given tenant from Nectar DXP. Requires a global admin account. Not available to tenant-level admins.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarCloudAgent -TenantName nectarcorp
 
        .NOTES
        Version 1.0
    #>


    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )

    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
                $TenantName = $Global:NectarTenantName 
            } ElseIf ($TenantName) {
                If ($TenantName -NotIn $Global:NectarTenantList) {
                    $TList = $Global:NectarTenantList -join ', '
                    Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
                }
            }

        # Get the cloud agents for the specified tenant
        $URI = "https://$($Global:NectarCloud)/aapi/cloudagents?tenant=$TenantName"
        Write-Verbose $URI
        
        Try {
            $Response = Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader
            Return $Response
        }
        Catch {
            Write-Error "Failed to retrieve cloud agents. $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}






#################################################################################################################################################
# #
# Supporting Functions #
# #

# SIG # Begin signature block
# MIIlcwYJKoZIhvcNAQcCoIIlZDCCJWACAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCA4PXEZ0HmRqoru
# VpYkI5Kr+ezbGoxOAY+0QYyAbgC3gKCCEvMwggXdMIIDxaADAgECAgh7LJvTFoAy
# mTANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx
# EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8G
# A1UEAwwoU1NMLmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTAe
# Fw0xNjAyMTIxNzM5MzlaFw00MTAyMTIxNzM5MzlaMHwxCzAJBgNVBAYTAlVTMQ4w
# DAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENv
# cnBvcmF0aW9uMTEwLwYDVQQDDChTU0wuY29tIFJvb3QgQ2VydGlmaWNhdGlvbiBB
# dXRob3JpdHkgUlNBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+Q/d
# oyt9y9Aq/uxnhabnLhu6d+Hj9a+k7PpKXZHEV0drGHdrdvL9k+Q9D8IWngtmw1aU
# nheDhc5W7/IW/QBi9SIJVOhlF05BueBPRpeqG8i4bmJeabFf2yoCfvxsyvNB2O3Q
# 6Pw/YUjtsAMUHRAOSxngu07shmX/NvNeZwILnYZVYf16OO3+4hkAt2+hUGJ1dDyg
# +sglkrRueiLH+B6h47LdkTGrKx0E/6VKBDfphaQzK/3i1lU0fBmkSmjHsqjTt8qh
# k4jrwZe8jPkd2SKEJHTHBD1qqSmTzOu4W+H+XyWqNFjIwSNUnRuYEcM4nH49hmyl
# D0CGfAL0XAJPKMuucZ8POsgz/hElNer8usVgPdl8GNWyqdN1eANyIso6wx/vLOUu
# qfqeLLZRRv2vA9bqYGjqhRY2a4XpHsCz3cQk3IAqgUFtlD7I4MmBQQCeXr9/xQiY
# ohgsQkCz+W84J0tOgPQ9gUfgiHzqHM61dVxRLhwrfxpyKOcAtdF0xtfkn60Hk7ZT
# NTX8N+TD9l0WviFz3pIK+KBjaryWkmo++LxlVZve9Q2JJgT8JRqmJWnLwm3KfOJZ
# X5es6+8uyLzXG1k8K8zyGciTaydjGc/86Sb4ynGbf5P+NGeETpnr/LN4CTNwumam
# du0bc+sapQ3EIhMglFYKTixsTrH9z5wJuqIz7YcCAwEAAaNjMGEwHQYDVR0OBBYE
# FN0ECQei9Xp9UlMSkpXuOIAlDaZZMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgw
# FoAU3QQJB6L1en1SUxKSle44gCUNplkwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3
# DQEBCwUAA4ICAQAgGBGUKfsmnRweHnBh8ZVyk3EkrWiTWI4yrxuzcAP8JSt0hZA9
# eGr0uYullzu1GJG7Hqf5QFuR+VWZrx4R0Fwdp2bjsZQHDDI5puobsHnYHZxwROOK
# 3cT5lR+KOEM/AYWlR6c9RrK85SJo93uc2Cw+CiHILTOsv8WBmTF0wXVxxb6x8CNF
# 9J1r/BljnaO8BMYYCyW7U4kPs4BQ3kXuRH+rlHhkmNP2KN2H2HBldPsOuRPrpw9h
# qTKWzN677WNMGLupQPegVG4giHF1GOp6tDRy4CMnd1y2kOqGJUCr7zMPy5+CvqIg
# +/a1LRrmwoWxdA/7yGUCpFIBR91JIsG/2OtrrH7e7GMzFbcjCI/GD41BWt2OxbmP
# 5UU/eNu60htAsf5xTT/ggaK6XrTsFeCT3QgffuFVmQsh3pOeCvvmo0m9NjD+53ey
# oHWXtS2BiBdlIPfakACfyVLMMso1fPU9D9gr1/UmbMkGNJYW6nBZGjJ5eQu2iH8P
# Ukg9v2zYokQu0U63cljTiROV/kSr+NeLG26cvCygW9VqAK9fN+HV+hALmJyG5yaP
# zvDsbopXC4DjTrLAoGNhkLpVaDd0araS25+hhiK2ZScO7LafQmDkZ8K12kELxNOL
# YRu8+h+RK9dEB166KazZxenvU0ha64DxKFghzbAGVfsnP1OQcKkEHlcnuTCCBnIw
# ggRaoAMCAQICCGQzUdPHOJ8IMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVT
# MQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NM
# IENvcnBvcmF0aW9uMTEwLwYDVQQDDChTU0wuY29tIFJvb3QgQ2VydGlmaWNhdGlv
# biBBdXRob3JpdHkgUlNBMB4XDTE2MDYyNDIwNDQzMFoXDTMxMDYyNDIwNDQzMFow
# eDELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9u
# MREwDwYDVQQKDAhTU0wgQ29ycDE0MDIGA1UEAwwrU1NMLmNvbSBDb2RlIFNpZ25p
# bmcgSW50ZXJtZWRpYXRlIENBIFJTQSBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
# ADCCAgoCggIBAJ+DE3OqsMZtIcvbi3qHdNBx3I6Xcprku4g0tN2AA8YvRaR0mr8e
# D1Dqnm1485/6USapPZ3RspRXPvs5iRuRK1bvZ8vmC+MOOYzGNfSMPd0l6QGsF0J9
# WBZA3PnVKEQdlWQwYTpk8pfXc0x9eyMCbfN161U9b6otxK++dKxd/mq2/OpceekP
# Q5y1UgUP7z6xsY/QSa2m40IZVD/zLw6hy3z+E/kjOdolHLg+AEo6bzIwN2Qex651
# B9hV0hjJDoq8o1zwfAqnhYHCDq+PmVzTYCW8g1ppHCUTzXL165yAm9wsZ8TdyQmY
# 1XPrxCGj5TKOPi9SmMZgN2SMsm9KVHIYzCeH+s11omMhTLU9ZP0rpptVryZMYLS5
# XP6rQ72t0BNmUB8L0omm/9eABvHDEQIzM2EX91Yfji87aOcV8XdWSimeA9rCKyZh
# MlugVuVJKY02p/XHUqJWAyAvOHiAvfYGrkE0y5RFvZvHiRgfC7r/qa5qQJkT3e9Q
# 3wG68gTW0DHfNDheV1vIOB5W1KxIpu3/+bjBO+3CJL5EYKd3zdU9mFm0Q+qqYH3N
# wuUv8ev11CDVlzRuXQRrBRHS05KMCSdE7U81MUZ+dBkFYuyJ4+ojcJjk0S/UihMY
# RpNl5Vhz00w9J3oiP8P4o1W3+eaHguxFHsVuOnyxTrmraPebY9WRQbypAgMBAAGj
# gfswgfgwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTdBAkHovV6fVJTEpKV
# 7jiAJQ2mWTAwBggrBgEFBQcBAQQkMCIwIAYIKwYBBQUHMAGGFGh0dHA6Ly9vY3Nw
# cy5zc2wuY29tMBEGA1UdIAQKMAgwBgYEVR0gADATBgNVHSUEDDAKBggrBgEFBQcD
# AzA7BgNVHR8ENDAyMDCgLqAshipodHRwOi8vY3Jscy5zc2wuY29tL3NzbC5jb20t
# cnNhLVJvb3RDQS5jcmwwHQYDVR0OBBYEFFTC/hCVAJPNavXnwNfZsku4jwzjMA4G
# A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEA9Q8mh3CvmaLK9dbJ8I1m
# PTmC04gj2IK/j1SEJ7bTgwfXnieJTYSOVNEg7mBD21dCPMewlfa+zOqjPY5PBsYr
# WYZ/63MbyuVAJuA9b8z2vXHGzX0OIEA51gXSr5QIv3/CUbcrtXuDIfBj2uWc4Wku
# dR1Oy2Ee9aUz3wKdFdntaZNXukZFLoC8Zb7nEj7eR/+QnBCt9laypNT61vwuvJch
# s3aD0pH6BlDRsYAogP7brQ9n7fh93NlwW3q6aLWzSmYXj+fw51fdaf68XuHVjJ8T
# u5WaFft5K4XVbT5nR24bB1z7VEUPFhEuEcOwvLVuHDNXlB7+QjRGjjFQTtszV5X6
# OOTmEturWC5Ft9kiyvRaR0ksKOhPjEI8ZGjp5kOsGZGpxxOCX/xxCje3nVB7PF33
# olKCNeS159MKb2v+jfmk19UdS+d9Ygj42desmUnbtYRBFC72LmCXU0ua/vGIenS6
# nnXp4NqnycwsO3tMCnjPlPc2YLaDPIpUy04NaCqUEXUmFOogN8zreRd2VXhxbeJJ
# ODM32+RsWccjYua8zi5US/1eAyrI3R5LcUTQdT4xYmWLKabtJOF6HYQ0f6QXfLSs
# fT81WMvDvxrdn1RWbUXlU/OIiisxo8o+UNEANOwnCMNnxlzoaL/PLhZluDxm/zuy
# lauajZ3MlPDteFB/7GRHo50wggaYMIIEgKADAgECAhBHaHS/7B9x7N3XWwMIISW7
# MA0GCSqGSIb3DQEBCwUAMHgxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQ
# MA4GA1UEBwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENvcnAxNDAyBgNVBAMMK1NT
# TC5jb20gQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBDQSBSU0EgUjEwHhcNMjYw
# NzA5MTQzMTE4WhcNMjcxMDEwMTQzMTE4WjB/MQswCQYDVQQGEwJVUzERMA8GA1UE
# CAwITmV3IFlvcmsxEDAOBgNVBAcMB0plcmljaG8xHjAcBgNVBAoMFU5lY3RhciBT
# ZXJ2aWNlcyBDb3JwLjELMAkGA1UECwwCSVQxHjAcBgNVBAMMFU5lY3RhciBTZXJ2
# aWNlcyBDb3JwLjCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAJmOMPRo
# al1bEwe/HPHB533mkxLFf2LGg8R3yZQfDMkVWS0ME52aHJT33z61eMLqFamuvqY7
# qeb6jcOjqenYUhNmFcnl1Oq2hFWWE7McdgYAc0boWHemqxHWrUXN53sCp1DFVTh3
# NFwTo2PaY/KLdIjsXXvmA9qQBytzxEmeaF5HyCmmTlQ/N4eDeqpMOAbQn1n7lLi7
# xiHq1UAll3uvB+y7Vq/3V/dCO33/aiQif1t6h2R+N2JhoW0vtvd+yMyY58klGfN6
# O9WHto+MAiGoyP8rs9XWAzhIRZ2JjwFjfTqQOxVv0lUH+ZolhJ9uQHvybL3wHwLN
# Qu8szoII3tqksiosdPc4Xk1nSyzrBfETafVVpey19HD6RhSIHrnPPHwWOzrdsT9j
# O9szScE3i+cfJcKiL98lJkhFq/ZGOIwAPgxPvZZQZqbzEcsR0iddA+KNLxKqzkv9
# 1GtETHdFoU07xY7+2NsY1xhs9jFFNhoeCVh+dk3lIBvpWkuMwM8mZKziUQIDAQAB
# o4IBlTCCAZEwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBRUwv4QlQCTzWr158DX
# 2bJLuI8M4zB6BggrBgEFBQcBAQRuMGwwSAYIKwYBBQUHMAKGPGh0dHA6Ly9jZXJ0
# LnNzbC5jb20vU1NMY29tLVN1YkNBLUNvZGVTaWduaW5nLVJTQS00MDk2LVIxLmNl
# cjAgBggrBgEFBQcwAYYUaHR0cDovL29jc3BzLnNzbC5jb20wUQYDVR0gBEowSDAI
# BgZngQwBBAEwPAYMKwYBBAGCqTABAwMBMCwwKgYIKwYBBQUHAgEWHmh0dHBzOi8v
# d3d3LnNzbC5jb20vcmVwb3NpdG9yeTATBgNVHSUEDDAKBggrBgEFBQcDAzBNBgNV
# HR8ERjBEMEKgQKA+hjxodHRwOi8vY3Jscy5zc2wuY29tL1NTTGNvbS1TdWJDQS1D
# b2RlU2lnbmluZy1SU0EtNDA5Ni1SMS5jcmwwHQYDVR0OBBYEFB7iZgW8t2SQ3I8n
# dKd+fgqBvDYxMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEAhCFC
# ydfNbwEfqJhrp612HM4Qh7JUphMm5ZLNomSEsonhjz1ltmMpB/0nGVH9Ond23hac
# 12+xzfnDAB2HsNhA9vVWYbgbxcH1duQbdRatjrtWKd1+YOfsvAS+O652yRQf26OL
# +2efdAnzaqsTYFoEJwFcgl1M5Uo+uSht961Jm7McHouRm/zYOnC+NjvGOgIL40KN
# P1pLQTxK5zc54+yFQANjiCPNI07AnkYCSgHDJCoKZbJTcUk26kQNqNE9AvjsIlpY
# uY8sTXHSIkN3WNuSPYr66GHaYj9m0AGUKGt/ZZePKmOsLu+nON5hwqXx+QX75yc2
# AIlKw/ItfZvBLeRuejoKnbOh6QiyDxn0lfAH8f/upDPCUfvBVADF56itElnCo+fC
# 36rzFnfBaVPbLVzqlAGSs7e6Yn669snsTHkKQm1fALEiZyt3dqOvppr8AO1f+oCc
# r/0jVezz0VyNUemChoAMBosREctn/fOYfL5NClq/mnI5m+tAHOajVk8LI6h7mlNv
# tIFKwfHCIH2EJM4ze+ZrSXWt8bfY+6pQwd4xCB9UmCw2DBBMQ11rvu1rLD+u3kIL
# NBAXHQGEnCoP+sY6h17wc/2Jb8BPg+yAvPXevqXmAdAKJrhvMmgsbttRxOQuVjrq
# 42h1me0i3Jy0obhODmpXsrVPm02ZBIYgiRcK/q8xghHWMIIR0gIBATCBjDB4MQsw
# CQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24xETAP
# BgNVBAoMCFNTTCBDb3JwMTQwMgYDVQQDDCtTU0wuY29tIENvZGUgU2lnbmluZyBJ
# bnRlcm1lZGlhdGUgQ0EgUlNBIFIxAhBHaHS/7B9x7N3XWwMIISW7MA0GCWCGSAFl
# AwQCAQUAoHwwEAYKKwYBBAGCNwIBDDECMAAwGQYJKoZIhvcNAQkDMQwGCisGAQQB
# gjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkE
# MSIEINpSlvHo9RP2ddPbsQ15LikFSTvjSNyXfB3EGbLNXXt5MA0GCSqGSIb3DQEB
# CwUABIIBgCxUqdgXaTfO9t/lOJXCtKHRbea9LdU2OxmmuZM2DjZNtFYdUWu7QwT7
# XqC4d3IA3nzDh5vezLKQYtFnOAFhT+1pjtMCsFCONSTixovo0e0j+/QSR/D9b00o
# FfQaYqMFGq08SWmzhVaKMKfVzgVBSA2ctVODR47OA8pJUeiWgQI/HbzllH1dVLLV
# kvMFdGb4jRzoH7oMTQbgkwaN5IvghJS6lKGE1Kzsfjv6yAa30NYCRFYXJkw1Tm4j
# /xV39RBVMMtPfGjegxu4Bw0vYLfvR2Fog7oTUa6JSwtkdhubsfSBmEMnoIrqGYwU
# bIQxMDCOJ94KmO09Mn4/L6Sxe9Hos0FWpn4XMpgZJmQ5JwR01w1cuoKHD809QtXr
# Rthqu9dG7zWzc8Vrn7zXi2JfRPCp+eV/9B3xPIMFwlltaBRZJREWzIf+iLJTNgDA
# +0l8PGz9aMOsAg2pddxzKBghFM5C3DtHih0zCPkXTRfVWPEFDV6mdAKM0yHfS3Vp
# HihmPsmRO6GCDxwwgg8YBgorBgEEAYI3AwMBMYIPCDCCDwQGCSqGSIb3DQEHAqCC
# DvUwgg7xAgEDMQ0wCwYJYIZIAWUDBAIBMH0GCyqGSIb3DQEJEAEEoG4EbDBqAgEB
# BgwrBgEEAYKpMAEDBgEwLzALBglghkgBZQMEAgEEIB8MGdTWRCExscXPTtCZkwtU
# qwaH8cF+jrQB6HfUucqIAggR5stEQp6YDxgPMjAyNjA5MTUxNzI5MjZaMAMCAQEC
# BgGgph3Mq6CCDAAwggT8MIIC5KADAgECAhAfaxZi0i4bbF3xwMGgYA44MA0GCSqG
# SIb3DQEBCwUAMHMxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UE
# BwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENvcnAxLzAtBgNVBAMMJlNTTC5jb20g
# VGltZXN0YW1waW5nIElzc3VpbmcgUlNBIENBIFIxMB4XDTI1MDIxODE2MzIwMloX
# DTM0MTExMjE4NTAwNVowbjELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAw
# DgYDVQQHDAdIb3VzdG9uMREwDwYDVQQKDAhTU0wgQ29ycDEqMCgGA1UEAwwhU1NM
# LmNvbSBUaW1lc3RhbXBpbmcgVW5pdCAyMDI1IEUxMFkwEwYHKoZIzj0CAQYIKoZI
# zj0DAQcDQgAEG/tRUcdv5lWW7E9eV8Tczq2DReerx2Jz47e884JGlqVQzW870D4Z
# HNJVWPLKAeFisHDrZcsWHWS/t77JF39pNqOCAVowggFWMB8GA1UdIwQYMBaAFAyd
# ECWOmqcbmYdDzwh+4b2BkPTPMFEGCCsGAQUFBwEBBEUwQzBBBggrBgEFBQcwAoY1
# aHR0cDovL2NlcnQuc3NsLmNvbS9TU0wuY29tLXRpbWVTdGFtcGluZy1JLVJTQS1S
# MS5jZXIwUQYDVR0gBEowSDA8BgwrBgEEAYKpMAEDBgEwLDAqBggrBgEFBQcCARYe
# aHR0cHM6Ly93d3cuc3NsLmNvbS9yZXBvc2l0b3J5MAgGBmeBDAEEAjAWBgNVHSUB
# Af8EDDAKBggrBgEFBQcDCDBGBgNVHR8EPzA9MDugOaA3hjVodHRwOi8vY3Jscy5z
# c2wuY29tL1NTTC5jb20tdGltZVN0YW1waW5nLUktUlNBLVIxLmNybDAdBgNVHQ4E
# FgQUznzZwASAxSQQagnqHKslPRH9qNIwDgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3
# DQEBCwUAA4ICAQCAc3Ukhb1mU2KnTsV9j2vUsnAspOXWH/L2vUGMOAcwTPtTsnuY
# DLfYnEDUovKMIImo2S5F+EMcYUR9m2NM6u7sBAwNIOJQO8IJzeNrPmnL2Ma/Ah7m
# emQttepeED5KoLMbvX1RKKDCEeRivu/w2JehpjRe7TenQGJlmt5mWmeCYYH37zo3
# 3gWogXHYjlnmK67t3iPtoA5kE3F9T2MUMggYO1Z9Z4KkXRDyssT/cMcOXMkqzkiX
# eL9Wg6XutNT3fyhKvEzDDDoYMGUpfysYfG+SOAhv0xeRWCUlIMew0BkN4JL+KdrE
# ocD4KG4Hwrg7EjFrqTV754cHKlqQBjfC43vDs+U+aE3qTkh2pmfcdkezZWOhHzjV
# n3CZU8V0YN2QFntc6Zvk5lRoq5+y+0RHRVtjYOTNqoBoi23WRz4j4VTPs+JXPY9T
# Ol6CR+1FHG+s/IgvTxuUlOdsxDReuoM3SsR+5Mu/heGGcrIlpeHEJR2M79xG6YzN
# nflBNQwi0FbLXEanSLKgVWcDJrak+xUy4Aj6zLXPGU5L2XmJLG8onyCmek6COphN
# ru7V7Jmj7gmVwaiKJHXsu2ExOsXWrra07nE6kjy3FRnqC0oa2QlXrB2P69ktzApn
# Yz3capWk6jpQGUaPHWwqxVnsAhTMlmWLg0nQzYphyt82eV5uRgqkOdKpbDCCBvww
# ggTkoAMCAQICEG1SGHCH6CNNhWAA0ICPk1YwDQYJKoZIhvcNAQELBQAwfDELMAkG
# A1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYD
# VQQKDA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0
# aWZpY2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTkxMTEzMTg1MDA1WhcNMzQxMTEy
# MTg1MDA1WjBzMQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcM
# B0hvdXN0b24xETAPBgNVBAoMCFNTTCBDb3JwMS8wLQYDVQQDDCZTU0wuY29tIFRp
# bWVzdGFtcGluZyBJc3N1aW5nIFJTQSBDQSBSMTCCAiIwDQYJKoZIhvcNAQEBBQAD
# ggIPADCCAgoCggIBAK5REBPS+TwgoCCF3slQHGTJ4f3F6TT/Cn8xSOhyWsVeqGH9
# 8Yf3UVz7t+bQwcITsD7CY6KoGP04OskBgareubfeMKcdKwIE1YBBjKhq4urwiOqx
# LUmVcvb2oM0wx3BnxQ3NBLu9ZkwMnjQlIY2mEwZMgDaqfZuiEa2BFzinXf3kRLKl
# Q5oa8ne3QU0vcG4qZvphy0xxBQXayqigzN3z2HQTq6N28EOjpnA2dajGPtiZ9aNJ
# eDfcDka5j3KbhBkzk4RWCjx5vP8H6DKHIIs02GHgxv/jG8JMIxWY1isG+IaB09li
# vKbxlvzhNAKZK5fQmUstrpYrVo7qqXAhJtv1tUaHzrp6QpuUL9dE/bSAC7UKO9xh
# yJSA1OsYWDx/wAmBA84JzX8IJ1olJjCEmlJ2F4o6dCARKA2Zhk+EU4LogpowBReT
# lTW2NNwUKAW+8Cte0rhrMBZQ47Vjd92V0gEvouOTMtQJgk2QVeqGwFVw8y4HSdQN
# a8sl8+Kay2MnyUXhLoQLFaeVaLs4SVXBOe3Ua1Gp5j3J2+8Yue1T4V5wrsNuocNR
# 3frpSt4yRIG3N68Bz1qqhk+eNUyO8WpXWlg6POZOJUdm0BzzRsB8V7kst8nM8joO
# e03KqhunBN69Ckeo8M32qo07zeveRrDwD2P4dmJLDYBflwZ1A/SQbS+HN+AHAgMB
# AAGjggGBMIIBfTASBgNVHRMBAf8ECDAGAQH/AgEAMB8GA1UdIwQYMBaAFN0ECQei
# 9Xp9UlMSkpXuOIAlDaZZMIGDBggrBgEFBQcBAQR3MHUwUQYIKwYBBQUHMAKGRWh0
# dHA6Ly93d3cuc3NsLmNvbS9yZXBvc2l0b3J5L1NTTGNvbVJvb3RDZXJ0aWZpY2F0
# aW9uQXV0aG9yaXR5UlNBLmNydDAgBggrBgEFBQcwAYYUaHR0cDovL29jc3BzLnNz
# bC5jb20wPwYDVR0gBDgwNjA0BgRVHSAAMCwwKgYIKwYBBQUHAgEWHmh0dHBzOi8v
# d3d3LnNzbC5jb20vcmVwb3NpdG9yeTATBgNVHSUEDDAKBggrBgEFBQcDCDA7BgNV
# HR8ENDAyMDCgLqAshipodHRwOi8vY3Jscy5zc2wuY29tL3NzbC5jb20tcnNhLVJv
# b3RDQS5jcmwwHQYDVR0OBBYEFAydECWOmqcbmYdDzwh+4b2BkPTPMA4GA1UdDwEB
# /wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAkhl1DaZaQs8ZB9ny/JT6wJvwFelE
# llovcTPdUOUTe5mTdw/E+3JtV8u6ppyLRbpIHbYlMy20KJAychU6xdaci4BsP9oV
# NxSRMsEjfHKz7ARqPNdpclhYAINLjsFGMO1iUNbXiAsnF/xboNCgfeMcMYbLyQYk
# U6UMobv9isrtQZ8e0EAQNV7qXJn4W0KyuTt0P8iIv/5DdDpIUBIktDZcjz2KEW6B
# 1gvvsKIM1esjYwWylAazBcQAake5pANMdSn8t1HdPKsiwuWfOguyRQazAX8oXz6S
# lZSIok0Lis9a02vGVtdhEaB0R3HxIyNRMMKWV1yuSeUXFuoexWav3GRPZC0WYb50
# SrW/l+wgrS8doetaMwyZon2L7ioYlIPSy1h9Dq/Q911PsSkbEZ3zrsB1roVnIfBu
# 5BJp0xvQrQ/Q4LavuvCoFR7QFoypNrotbNYi2AGMZw5td4zGZtCqUTPZi0BwSuRm
# +HRYAEMMThTwbJX/fYV1oC8mBN970yIvadIGKhh7+DmYdRJYBrL8inVFCZAK+YX2
# w1+qWEnCSPL/VTWJtSRMhQFfceDKbJC+pBNksvKzqkva0J1ZyMj1i4vDfSuBmbz4
# rfzsvvJxS+quZDdkmW6MeXevWGBXvqzdbAw+AqTVsAQUyP6tFeKZIL4S/fSFdl2r
# Ix2X+KXkqx3S+EYxggJYMIICVAIBATCBhzBzMQswCQYDVQQGEwJVUzEOMAwGA1UE
# CAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24xETAPBgNVBAoMCFNTTCBDb3JwMS8w
# LQYDVQQDDCZTU0wuY29tIFRpbWVzdGFtcGluZyBJc3N1aW5nIFJTQSBDQSBSMQIQ
# H2sWYtIuG2xd8cDBoGAOODALBglghkgBZQMEAgGgggFhMBoGCSqGSIb3DQEJAzEN
# BgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjYwOTE1MTcyOTI2WjAoBgkq
# hkiG9w0BCTQxGzAZMAsGCWCGSAFlAwQCAaEKBggqhkjOPQQDAjAvBgkqhkiG9w0B
# CQQxIgQgMPoGsqpzP7OlWG4tdwyJbpEcXxVST6Irwjz/2zmJsAQwgckGCyqGSIb3
# DQEJEAIvMYG5MIG2MIGzMIGwBCBUKvmhao1yLmYRSXiK6ZTBipqu5aZcs0SiVJr5
# bHnHizCBizB3pHUwczELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYD
# VQQHDAdIb3VzdG9uMREwDwYDVQQKDAhTU0wgQ29ycDEvMC0GA1UEAwwmU1NMLmNv
# bSBUaW1lc3RhbXBpbmcgSXNzdWluZyBSU0EgQ0EgUjECEB9rFmLSLhtsXfHAwaBg
# DjgwCgYIKoZIzj0EAwIERzBFAiB+tcenNabMnYVnXT3q3ISgQ11JQd44n79hVCid
# VpjSHAIhAOwXLgU6y6K900qUkCsfbU9W11fFhrR4x2KmUvJ+SgB4
# SIG # End signature block