Classes/Main/TeamworkServer.Class.ps1

Class TeamworkServer {
    [string]$BaseFqdn
    [string]$UriPath
    [string]$ApiToken

    #region storedTeamworkProperties
    ########################################################################

    [array]$CustomFields
    [array]$Companies
    [array]$ProjectCategories
    [array]$People
    [array]$Tags

    ########################################################################
    #endregion storedTeamworkProperties

    #region Tracking
    ########################################################################

    hidden [bool]$Connected
    [array]$UrlHistory
    [array]$RawQueryResultHistory
    [array]$QueryHistory
    [array]$QueryParamHistory
    $LastError
    hidden $LastResult
    hidden $LastRawResult
    hidden $RateLimiter = [System.Collections.ArrayList]@()

    ########################################################################
    #endregion Tracking

    # getApiUrl
    [String] getApiUrl([string]$method) {
        if ($this.BaseFqdn) {
            $url = "https://" + $this.BaseFqdn + $this.getApiVersionString($method) + $this.UriPath
            return $url
        } else {
            return $null
        }
    }

    # createQueryString
    [string] createQueryString ([hashtable]$hashTable) {
        $QueryString = [System.Web.httputility]::ParseQueryString("")

        foreach ($Pair in $hashTable.GetEnumerator()) {
            $QueryString[$($Pair.Name)] = $($Pair.Value)
        }

        return ("?" + $QueryString.ToString())
    }

    # processQueryResult
    [psobject] processQueryResult ($unprocessedResult) {
        if ($unprocessedResult.GetType().Name -eq 'String') {
            $processedResult = $unprocessedResult | ConvertFrom-Json -AsHashtable
        } else {
            $processedResult = $unprocessedResult
        }
        return $processedResult
    }

    # encodeAuthorizationHeader
    [string] encodeAuthorizationHeader () {
        $plainTextHeader = $this.ApiToken + ':x' # password can be any character
        $encodedHeader = [System.Text.Encoding]::UTF8.GetBytes($plainTextHeader)
        $base64Header = [System.Convert]::ToBase64String($encodedHeader)
        return $base64Header
    }

    # getApiVersionString
    [string] getApiVersionString ([string]$method) {
        $ApiVersionMap = @{}
        $ApiVersionMap.'projects.json' = @{}
        $ApiVersionMap.'projects.json'.'POST' = '/'
        $ApiVersionMap.'projects.json'.'PUT' = '/'

        $ApiVersionMap.'projectCategories.json' = @{}
        $ApiVersionMap.'projectCategories.json'.'GET' = '/'

        $CurrentUriPath = $this.UriPath # -replace '\d+', ''

        $thisVersion = $ApiVersionMap.$CurrentUriPath.$method

        if (-not $thisVersion) {
            switch -Regex ($CurrentUriPath) {
                'projects/\d+/people.json' {
                    switch ($method) {
                        'POST' {
                            $thisVersion = '/'
                        }
                    }
                }
                'projects/\d+/complete.json' {
                    switch ($method) {
                        'PUT' {
                            $thisVersion = '/'
                        }
                    }
                }
                'projects\/\d+\.json' {
                    switch ($method) {
                        'PUT' {
                            $thisVersion = '/'
                        }
                    }
                }
            }
        }

        if (-not $thisVersion) {
            $thisVersion = '/projects/api/v3/'
        }

        return $thisVersion
    }

    #region apiVersion
    ########################################################################



    ########################################################################
    #endregion apiVersion

    #region invokeApiQuery
    ########################################################################

    [psobject] invokeApiQuery([hashtable]$queryString, [string]$method, [string]$body) {
        # Wrike uses the query string as a body attribute, keeping this function as is for now and just using an empty querystring
        $url = $this.getApiUrl($method)

        # Populate Query/Url History
        $this.QueryHistory += $queryString

        $QueryParams = @{}
        $rawResult = $null
        # try query
        try {
            $QueryParams.Uri = $url
            switch ($method) {
                'PUT' {
                    $QueryParams.Uri += $this.createQueryString($queryString)
                    if ('' -ne $body) {
                        $QueryParams.Body = $body
                    }
                }
                'POST' {
                    $QueryParams.Body = $body
                    $QueryParams.ContentType = 'application/json'
                }
                'PATCH' {
                    $QueryParams.Body = $body
                    $QueryParams.ContentType = 'application/json'
                }
                'GET' {
                    $QueryParams.Uri += $this.createQueryString($queryString)
                }
                'DELETE' {
                    $QueryParams.Uri += $this.createQueryString($queryString)
                }
            }
            $this.UrlHistory += $QueryParams.Uri
            $QueryParams.Method = $method
            $QueryParams.Headers = @{
                'Authorization' = "Basic $($this.encodeAuthorizationHeader())"
            }

            $this.QueryParamHistory += $QueryParams

            $TimeStamp = [DateTimeOffset]::Now.ToUnixTimeMilliseconds()
            $this.RateLimiter += $TimeStamp
            if ($this.RateLimiter.Count -gt 150) {
                $TimeDifference = ($TimeStamp - $this.RateLimiter[-150])
                $TimeDifferenceInSeconds = $TimeDifference / 1000
                if ($TimeDifference -lt 60000) {
                    Write-Warning "Rate Limit hit, waiting $TimeDifferenceInSeconds seconds"
                    Start-Sleep -Milliseconds $TimeDifference
                }
            }

            $rawResult = Invoke-RestMethod @QueryParams
        } catch [System.Net.Http.HttpRequestException], [System.Management.Automation.RuntimeException] {
            $RetryCount = 0
            do {
                Start-Sleep -Seconds 3
                Write-Warning "Generic connection error, waiting 3 seconds and trying again"
                $rawResult = Invoke-RestMethod @QueryParams
                $RetryCount++
            } while ($RetryCount -lt 3)
            if ($null -eq $rawResult) {
                Throw "Retries failed, check your connection"
            }
        } catch {
            Throw $_
        }

        $this.RawQueryResultHistory += $rawResult
        $this.LastRawResult = $rawResult

        $proccessedResult = $this.processQueryResult($rawResult)
        $this.LastResult = $proccessedResult

        return $proccessedResult
    }

    # with just a querystring
    [psobject] invokeApiQuery([hashtable]$queryString) {
        return $this.invokeApiQuery($queryString, 'GET', '')
    }

    # with just a method
    [psobject] invokeApiQuery([string]$method) {
        return $this.invokeApiQuery(@{}, $method, '')
    }

    # with no method or querystring specified
    [psobject] invokeApiQuery() {
        return $this.invokeApiQuery(@{}, 'GET', '')
    }

    ########################################################################
    #endregion invokeApiQuery

    #region Initiators
    ########################################################################

    # empty initiator
    TeamworkServer() {
    }

    ########################################################################
    #endregion Initiators
}