Public/Utils/Invoke-ZoomRestMethod.ps1

<#

.SYNOPSIS
Invoke-ZoomRestMethod is used to make API calls to Zoom.

.DESCRIPTION
Invoke-ZoomRestMethod adds additional functionality to Invoke-RestMethod. It will automatically add the
correct headers to the invocation as they pertain to Zoom, based on the API key and secret provided to the
cmdlet. It also does some error handling, including retrying the call if there have been too many requests.

Invoke-ZoomRestMethod can be used to make API calls that don't have a cmdlet associated with the call
within PSZoom. See below.

.EXAMPLE
Get billing information.
$accountId = 123456789
$request = [System.UriBuilder]"https://api.zoom.us/v2/accounts/$accountId/billing"
Invoke-ZoomRestMethod -Uri $request.Uri -ApiKey your_api_key -ApiSecret your_api_secret -Method GET

#>


function Invoke-ZoomRestMethod {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Microsoft.PowerShell.Commands.WebRequestMethod]$Method,
        [switch]$FollowRelLink,
        [int]$MaximumFollowRelLink,
        [string]$ResponseHeadersVariable,
        [string]$StatusCodeVariable,
        [switch]$UseBasicParsing,
        [uri]$Uri,
        $WebSession,
        [string]$SessionVariable,
        [switch]$AllowUnencryptedAuthentication,
        $Authentication,
        [psCredential]$Credential,
        [switch]$UseDefaultCredentials,
        [string]$CertificateThumbprint,
        [x509Certificate]$Certificate,
        [switch]$SkipCertificateCheck,
        $SslProtocol,
        [secureString]$Token,
        [string]$UserAgent,
        [switch]$DisableKeepAlive,
        [int]$TimeoutSec,
        $Headers,
        [int]$MaximumRedirection,
        [int]$MaximumRetryCount,
        [int]$RetryIntervalSec,
        [uri]$Proxy,
        [psCredential]$ProxyCredential,
        [switch]$ProxyUseDefaultCredentials,
        [object]$Body,
        $Form,
        [string]$ContentType,
        [string]$TransferEncoding,
        [string]$InFile,
        [string]$OutFile,
        [switch]$PassThru,
        [switch]$Resume,
        [switch]$SkipHttpErrorCheck,
        [switch]$PreserveAuthorizationOnRedirect,
        [switch]$SkipHeaderValidation,
        [string]$ApiKey,
        [string]$ApiSecret
    )
    
    $params = @{
        AllowUnencryptedAuthentication  = 'AllowUnencryptedAuthentication'
        Authentication                  = 'Authentication'
        Body                            = 'Body'
        Certificate                     = 'Certificate'
        CertificateThumbprint           = 'CertificateThumbprint'
        ContentType                     = 'ContentType'
        Credential                      = 'Credential'
        DisableKeepAlive                = 'DisableKeepAlive'
        FollowRelLink                   = 'FollowRelLink'
        Form                            = 'Form'
        Headers                         = 'Headers'
        InFile                          = 'InFile'
        MaximumFollowRelLink            = 'MaximumFollowRelLink'
        MaximumRedirection              = 'MaximumRedirection'
        MaximumRetryCount               = 'MaximumRetryCount'
        Method                          = 'Method'
        OutFile                         = 'OutFile'
        PassThru                        = 'PassThru'
        PreserveAuthorizationOnRedirect = 'PreserveAuthorizationOnRedirect'
        Proxy                           = 'Proxy'
        ProxyCredential                 = 'ProxyCredential'
        ProxyUseDefaultCredentials      = 'ProxyUseDefaultCredentials'
        ResponseHeadersVariable         = 'ResponseHeadersVariable'
        Resume                          = 'Resume'
        RetryIntervalSec                = 'RetryIntervalSec'
        SessionVariable                 = 'SessionVariable'
        SkipCertificateCheck            = 'SkipCertificateCheck'
        SkipHeaderValidatio             = 'SkipHeaderValidation'
        SkipHttpErrorCheck              = 'SkipHttpErrorCheck'
        SslProtocol                     = 'SslProtocol'
        StatusCodeVariable              = 'StatusCodeVariable'
        TimeoutSec                      = 'TimeoutSec'
        Token                           = 'Token'
        TransferEncoding                = 'TransferEncoding'
        Uri                             = 'Uri'
        UseBasicParsing                 = 'UseBasicParsing'
        UseDefaultCredentials           = 'UseDefaultCredentials'
        UserAgent                       = 'UserAgent'
        WebSession                      = 'WebSession'
    }

    function Remove-NonPsBoundParameters {
        param (
            $Obj,
            $Parameters = $PSBoundParameters
        )
  
        process {
            $NewObj = @{}
      
            foreach ($Key in $Obj.Keys) {
                if ($Parameters.ContainsKey($Obj.$Key) -or -not [string]::IsNullOrWhiteSpace($Obj.Key)) {
                    $Newobj.Add($Key, (get-variable $Obj.$Key).value)
                }
            }
      
            return $NewObj
        }
    }

    $params = Remove-NonPsBoundParameters($params)

    if ($params.Headers -is [ref]) {
        # Update the token if it has expired.
        $TokenPayload = ($Headers.Value.authorization -split '\.')[1]
        $TokenExpireTime = [int]((ConvertFrom-Json ([System.Text.Encoding]::UTF8.GetString(
                        [Convert]::FromBase64String($TokenPayload + '=' * @(0..3)[ - ($TokenPayload.Length % 4)])))).exp)
        $CurrentUnixTime = ((Get-Date) - (Get-Date '1970/1/1 0:0:0 GMT')).TotalSeconds
        if ($CurrentUnixTime -ge $TokenExpireTime) {
            $Headers.Value = New-ZoomHeaders -ApiKey $ApiKey -ApiSecret $ApiSecret
        }

        $params.Headers = $Headers.Value
    }
    elseif ($null -eq $params.Headers) {
        $params.Headers = New-ZoomHeaders -ApiKey $ApiKey -ApiSecret $ApiSecret
    }

    if ([string]::IsNullOrEmpty($params.ContentType)) {
        $params.ContentType = 'application/json; charset=utf-8'
    }

    try {
        $response = Invoke-RestMethod @params
    }
    catch {
        if ($PSVersionTable.PSVersion.Major -lt 6) {
            $errorStreamReader = [System.IO.StreamReader]::new($_.exception.Response.GetResponseStream())
            $errorDetails = ConvertFrom-Json ($errorStreamReader.ReadToEnd())
        }
        else {
            $errorDetails = ConvertFrom-Json $_.errorDetails -AsHashtable
        }

        $exception = $_.exception
        $targetObject = $_.targetObject
        $errorCode = $exception.message.split(':')[1] -replace '[^0-9]*'
        $category = switch ($errorCode) {
            300 { 'InvalidOperation' }
            400 { 'InvalidOperation' }
            401 { 'AuthenticationError' }
            404 { 'InvalidOperation' }
            409 { 'ResourceExists' }
            429 { 'LimitsExceeded' }
            Default { 'InvalidOperation' }
        }
        Write-Error -Message "$($exception.message) $($errorDetails.message)" -ErrorId $errorCode `
            -CategoryReason $errorDetails.message -TargetName $targetObject.requestUri `
            -CategoryActivity $targetObject.method -Category $category

        #Rate limiting logic
        if ($errorCode -eq 429) {
            # Max retry count: 5
            if ($script:RetryCount -lt 5) {
                $script:RetryCount++
                Write-Warning 'Error 429. Too many requests encountered. This is usually because of rate limiting. Retrying in 1 second.'
                Start-Sleep -Seconds 1
                Invoke-ZoomRestMethod @params
            }
        }
    }
    
    if ($response) {
        Write-Output $response
    }
}