114 rivejä
3,1 KiB
Go
114 rivejä
3,1 KiB
Go
package anaconda
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
//Error code defintions match the Twitter documentation
|
|
//https://developer.twitter.com/en/docs/basics/response-codes
|
|
TwitterErrorCouldNotAuthenticate = 32
|
|
TwitterErrorDoesNotExist = 34
|
|
TwitterErrorAccountSuspended = 64
|
|
TwitterErrorApi1Deprecation = 68 //This should never be needed
|
|
TwitterErrorRateLimitExceeded = 88
|
|
TwitterErrorInvalidToken = 89
|
|
TwitterErrorOverCapacity = 130
|
|
TwitterErrorInternalError = 131
|
|
TwitterErrorCouldNotAuthenticateYou = 135
|
|
TwitterErrorStatusIsADuplicate = 187
|
|
TwitterErrorBadAuthenticationData = 215
|
|
TwitterErrorUserMustVerifyLogin = 231
|
|
|
|
// Undocumented by Twitter, but may be returned instead of 34
|
|
TwitterErrorDoesNotExist2 = 144
|
|
)
|
|
|
|
type ApiError struct {
|
|
StatusCode int
|
|
Header http.Header
|
|
Body string
|
|
Decoded TwitterErrorResponse
|
|
URL *url.URL
|
|
}
|
|
|
|
func newApiError(resp *http.Response) *ApiError {
|
|
// TODO don't ignore this error
|
|
// TODO don't use ReadAll
|
|
p, _ := ioutil.ReadAll(resp.Body)
|
|
|
|
var twitterErrorResp TwitterErrorResponse
|
|
_ = json.Unmarshal(p, &twitterErrorResp)
|
|
return &ApiError{
|
|
StatusCode: resp.StatusCode,
|
|
Header: resp.Header,
|
|
Body: string(p),
|
|
Decoded: twitterErrorResp,
|
|
URL: resp.Request.URL,
|
|
}
|
|
}
|
|
|
|
// ApiError supports the error interface
|
|
func (aerr ApiError) Error() string {
|
|
return fmt.Sprintf("Get %s returned status %d, %s", aerr.URL, aerr.StatusCode, aerr.Body)
|
|
}
|
|
|
|
// Check to see if an error is a Rate Limiting error. If so, find the next available window in the header.
|
|
// Use like so:
|
|
//
|
|
// if aerr, ok := err.(*ApiError); ok {
|
|
// if isRateLimitError, nextWindow := aerr.RateLimitCheck(); isRateLimitError {
|
|
// <-time.After(nextWindow.Sub(time.Now()))
|
|
// }
|
|
// }
|
|
//
|
|
func (aerr *ApiError) RateLimitCheck() (isRateLimitError bool, nextWindow time.Time) {
|
|
// TODO check for error code 130, which also signifies a rate limit
|
|
if aerr.StatusCode == 429 {
|
|
if reset := aerr.Header.Get("X-Rate-Limit-Reset"); reset != "" {
|
|
if resetUnix, err := strconv.ParseInt(reset, 10, 64); err == nil {
|
|
resetTime := time.Unix(resetUnix, 0)
|
|
// Reject any time greater than an hour away
|
|
if resetTime.Sub(time.Now()) > time.Hour {
|
|
return true, time.Now().Add(15 * time.Minute)
|
|
}
|
|
|
|
return true, resetTime
|
|
}
|
|
}
|
|
}
|
|
|
|
return false, time.Time{}
|
|
}
|
|
|
|
//TwitterErrorResponse has an array of Twitter error messages
|
|
//It satisfies the "error" interface
|
|
//For the most part, Twitter seems to return only a single error message
|
|
//Currently, we assume that this always contains exactly one error message
|
|
type TwitterErrorResponse struct {
|
|
Errors []TwitterError `json:"errors"`
|
|
}
|
|
|
|
func (tr TwitterErrorResponse) First() error {
|
|
return tr.Errors[0]
|
|
}
|
|
|
|
func (tr TwitterErrorResponse) Error() string {
|
|
return tr.Errors[0].Message
|
|
}
|
|
|
|
//TwitterError represents a single Twitter error messages/code pair
|
|
type TwitterError struct {
|
|
Message string `json:"message"`
|
|
Code int `json:"code"`
|
|
}
|
|
|
|
func (te TwitterError) Error() string {
|
|
return te.Message
|
|
}
|