Public/Send-SlackAPI.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
57
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
96
97
98
99
100
101
102
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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
function Send-SlackApi
{
    <#
    .SYNOPSIS
        Send a message to the Slack API endpoint
 
    .DESCRIPTION
        Send a message to the Slack API endpoint
 
        This function is used by other PSSlack functions.
        It's a simple wrapper you could use for calls to the Slack API
 
    .PARAMETER Method
        Slack API method to call.
 
        Reference: https://api.slack.com/methods
 
    .PARAMETER Body
        Hash table of arguments to send to the Slack API.
 
    .PARAMETER Token
        Slack token to use
 
    .PARAMETER Proxy
        Proxy server to use
 
    .PARAMETER RateLimit
        Indicates the API method is rate-limited and should automatically back-off/retry upon receipt of a HTTP 429 (Too Many Requests) response from the server.
 
    .FUNCTIONALITY
        Slack
    #>

    [OutputType([String])]
    [cmdletbinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Method,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [hashtable]$Body = @{ },

        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            if (-not $_ -and -not $Script:PSSlack.Token)
            {
                throw 'Please supply a Slack Api Token with Set-SlackApiToken.'
            }
            else
            {
                $true
            }
        })]
        [string]$Token = $Script:PSSlack.Token,

        [string]$Proxy = $Script:PSSlack.Proxy,

        [Switch]$RateLimit
    )

    # Create a "leaky bucket" for a given API token, indicating a counter of requests "in" the bucket and drip rate (per-second) that requests exit the bucket.
    # This is used to follow along with Slack's API rate-limiting algorithm.
    If ($Script:APIRateBuckets[$Token] -eq $Null) {
        $Script:APIRateBuckets[$Token] = @{
            Counter = 0
            MaxCount = 25
            LeakRateMsec = 1000
            LastDrip = [DateTime]::Now
        }
    }


    $Params = @{
        Uri = "https://slack.com/api/$Method"
    }
    if($Proxy)
    {
        $Params['Proxy'] = $Proxy
    }
    $Body.token = $Token

    # Update the bucket for this API key to "drain" it as necessary - even if we're not using a RLed API call in this instance.
    $Bucket = $Script:APIRateBuckets[$Token]

    # If we should "drip" (non-zero counter, at least 1 drip period has elapsed)
    If ($Bucket.Counter -gt 0 -and ([DateTime]::Now - $Bucket.LastDrip).TotalMilliseconds -gt $Bucket.LeakRateMsec) {
        # Figure out how many drips should have occurred.
        $NumDrips = [Math]::Floor(([DateTime]::Now - $Bucket.LastDrip).TotalMilliseconds / $Bucket.LeakRateMsec)

        # Decrement the counter by the number of drips (if the counter is nonzero afterwards), or set the counter to zero.
        $Bucket.Counter -= [Math]::Min($NumDrips, $Bucket.Counter)

        # Update the last drip timestamp to indicate we just dripped.
        $Bucket.LastDrip = [DateTime]::Now
    }

    try {

        # If we want to invoke a rate-limited API method and the bucket is full...
        If ($RateLimit -and ($Bucket.Counter -eq $Bucket.MaxCount)) {
                
            # Determine when the next drip will occur.
            $NextDrip = $Bucket.LastDrip.AddMilliseconds($Bucket.LeakRateMsec)
            Write-Verbose "Rate-limit bucket full, waiting..."
            
            # Sleep until then.
            Start-Sleep -Milliseconds ($NextDrip - [DateTime]::Now).TotalMilliseconds
            
            # Drip accordingly.
            $Bucket.Counter--
            $Bucket.LastDrip = [DateTime]::Now

        }

        $Response = Invoke-RestMethod @Params -body $Body

        # If we've successfully invoked a rate-limited API method...
        If ($RateLimit -and $Response.ok) {

            # Increase the counter for our bucket.
            $Bucket.Counter++    
        }

    }
    catch [System.Net.WebException] {
        # If we're configured to do rate-limiting...
        # (HTTP 429 is "Too Many Requests")
        If ($_.Exception.Response.StatusCode -eq 429 -and $RateLimit) {

            # Get the time before we can try again.
            $RetryPeriod = $_.Exception.Response.Headers["Retry-After"]

            # Set our bucket to be full.
            $Bucket.Counter = $Bucket.MaxCount
            
            # Figure out when the last drip "should" have occurred, based on how many seconds we have until the next drip.
            $Bucket.LastDrip = [DateTime]::Now.AddSeconds($RetryPeriod).AddMilliseconds($Bucket.LeakRateMsec * -1).AddMilliseconds(50)

            # Warn the user.
            Write-Verbose "Slack API rate-limit exceeded - blocking for $RetryPeriod second(s)."
            
            # (We don't actually have to sleep here, but rather recurse - the next call will handle sleeping.)
            Send-SlackApi @PSBoundParameters
            
        } Elseif ($_.ErrorDetails.Message -ne $null) {

            # Convert the error-message to an object. (Invoke-RestMethod will not return data by-default if a 4xx/5xx status code is generated.)
            $_.ErrorDetails.Message | ConvertFrom-Json | Parse-SlackError -Exception $_.Exception -ErrorAction Stop
            
        } Else {
            Write-Error -Exception $_.Exception -Message "Slack API call failed: $_"
        }
    }

    # Check to see if we have confirmation that our API call failed.
    # (Responses with exception-generating status codes are handled in the "catch" block above - this one is for errors that don't generate exceptions)
    If ($Response -ne $null -and $Response.ok -eq $False) {
        $Response | Parse-SlackError
    } Else {
        Write-Output $Response
    }

    
}