twitter.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. //Package anaconda provides structs and functions for accessing version 1.1
  2. //of the Twitter API.
  3. //
  4. //Successful API queries return native Go structs that can be used immediately,
  5. //with no need for type assertions.
  6. //
  7. //Authentication
  8. //
  9. //If you already have the access token (and secret) for your user (Twitter provides this for your own account on the developer portal), creating the client is simple:
  10. //
  11. // anaconda.SetConsumerKey("your-consumer-key")
  12. // anaconda.SetConsumerSecret("your-consumer-secret")
  13. // api := anaconda.NewTwitterApi("your-access-token", "your-access-token-secret")
  14. //
  15. //
  16. //Queries
  17. //
  18. //Executing queries on an authenticated TwitterApi struct is simple.
  19. //
  20. // searchResult, _ := api.GetSearch("golang", nil)
  21. // for _ , tweet := range searchResult.Statuses {
  22. // fmt.Print(tweet.Text)
  23. // }
  24. //
  25. //Certain endpoints allow separate optional parameter; if desired, these can be passed as the final parameter.
  26. //
  27. // v := url.Values{}
  28. // v.Set("count", "30")
  29. // result, err := api.GetSearch("golang", v)
  30. //
  31. //
  32. //Endpoints
  33. //
  34. //Anaconda implements most of the endpoints defined in the Twitter API documentation: https://dev.twitter.com/docs/api/1.1.
  35. //For clarity, in most cases, the function name is simply the name of the HTTP method and the endpoint (e.g., the endpoint `GET /friendships/incoming` is provided by the function `GetFriendshipsIncoming`).
  36. //
  37. //In a few cases, a shortened form has been chosen to make life easier (for example, retweeting is simply the function `Retweet`)
  38. //
  39. //More detailed information about the behavior of each particular endpoint can be found at the official Twitter API documentation.
  40. package anaconda
  41. import (
  42. "compress/zlib"
  43. "encoding/json"
  44. "fmt"
  45. "io/ioutil"
  46. "net/http"
  47. "net/url"
  48. "strings"
  49. "time"
  50. "github.com/ChimeraCoder/tokenbucket"
  51. "github.com/garyburd/go-oauth/oauth"
  52. )
  53. const (
  54. _GET = iota
  55. _POST = iota
  56. _DELETE = iota
  57. _PUT = iota
  58. BaseUrlV1 = "https://api.twitter.com/1"
  59. BaseUrl = "https://api.twitter.com/1.1"
  60. UploadBaseUrl = "https://upload.twitter.com/1.1"
  61. )
  62. var (
  63. oauthCredentials oauth.Credentials
  64. )
  65. type TwitterApi struct {
  66. oauthClient oauth.Client
  67. Credentials *oauth.Credentials
  68. queryQueue chan query
  69. bucket *tokenbucket.Bucket
  70. returnRateLimitError bool
  71. HttpClient *http.Client
  72. // Currently used only for the streaming API
  73. // and for checking rate-limiting headers
  74. // Default logger is silent
  75. Log Logger
  76. // used for testing
  77. // defaults to BaseUrl
  78. baseUrl string
  79. }
  80. type query struct {
  81. url string
  82. form url.Values
  83. data interface{}
  84. method int
  85. response_ch chan response
  86. }
  87. type response struct {
  88. data interface{}
  89. err error
  90. }
  91. const DEFAULT_DELAY = 0 * time.Second
  92. const DEFAULT_CAPACITY = 5
  93. //NewTwitterApi takes an user-specific access token and secret and returns a TwitterApi struct for that user.
  94. //The TwitterApi struct can be used for accessing any of the endpoints available.
  95. func NewTwitterApi(access_token string, access_token_secret string) *TwitterApi {
  96. //TODO figure out how much to buffer this channel
  97. //A non-buffered channel will cause blocking when multiple queries are made at the same time
  98. queue := make(chan query)
  99. c := &TwitterApi{
  100. oauthClient: oauth.Client{
  101. TemporaryCredentialRequestURI: "https://api.twitter.com/oauth/request_token",
  102. ResourceOwnerAuthorizationURI: "https://api.twitter.com/oauth/authenticate",
  103. TokenRequestURI: "https://api.twitter.com/oauth/access_token",
  104. Credentials: oauthCredentials,
  105. },
  106. Credentials: &oauth.Credentials{
  107. Token: access_token,
  108. Secret: access_token_secret,
  109. },
  110. queryQueue: queue,
  111. bucket: nil,
  112. returnRateLimitError: false,
  113. HttpClient: http.DefaultClient,
  114. Log: silentLogger{},
  115. baseUrl: BaseUrl,
  116. }
  117. go c.throttledQuery()
  118. return c
  119. }
  120. //NewTwitterApiWithCredentials takes an app-specific consumer key and secret, along with a user-specific access token and secret and returns a TwitterApi struct for that user.
  121. //The TwitterApi struct can be used for accessing any of the endpoints available.
  122. func NewTwitterApiWithCredentials(access_token string, access_token_secret string, consumer_key string, consumer_secret string) *TwitterApi {
  123. api := NewTwitterApi(access_token, access_token_secret)
  124. api.oauthClient.Credentials.Token = consumer_key
  125. api.oauthClient.Credentials.Secret = consumer_secret
  126. return api
  127. }
  128. //SetConsumerKey will set the application-specific consumer_key used in the initial OAuth process
  129. //This key is listed on https://dev.twitter.com/apps/YOUR_APP_ID/show
  130. func SetConsumerKey(consumer_key string) {
  131. oauthCredentials.Token = consumer_key
  132. }
  133. //SetConsumerSecret will set the application-specific secret used in the initial OAuth process
  134. //This secret is listed on https://dev.twitter.com/apps/YOUR_APP_ID/show
  135. func SetConsumerSecret(consumer_secret string) {
  136. oauthCredentials.Secret = consumer_secret
  137. }
  138. // ReturnRateLimitError specifies behavior when the Twitter API returns a rate-limit error.
  139. // If set to true, the query will fail and return the error instead of automatically queuing and
  140. // retrying the query when the rate limit expires
  141. func (c *TwitterApi) ReturnRateLimitError(b bool) {
  142. c.returnRateLimitError = b
  143. }
  144. // Enable query throttling using the tokenbucket algorithm
  145. func (c *TwitterApi) EnableThrottling(rate time.Duration, bufferSize int64) {
  146. c.bucket = tokenbucket.NewBucket(rate, bufferSize)
  147. }
  148. // Disable query throttling
  149. func (c *TwitterApi) DisableThrottling() {
  150. c.bucket = nil
  151. }
  152. // SetDelay will set the delay between throttled queries
  153. // To turn of throttling, set it to 0 seconds
  154. func (c *TwitterApi) SetDelay(t time.Duration) {
  155. c.bucket.SetRate(t)
  156. }
  157. func (c *TwitterApi) GetDelay() time.Duration {
  158. return c.bucket.GetRate()
  159. }
  160. // SetBaseUrl is experimental and may be removed in future releases.
  161. func (c *TwitterApi) SetBaseUrl(baseUrl string) {
  162. c.baseUrl = baseUrl
  163. }
  164. //AuthorizationURL generates the authorization URL for the first part of the OAuth handshake.
  165. //Redirect the user to this URL.
  166. //This assumes that the consumer key has already been set (using SetConsumerKey or NewTwitterApiWithCredentials).
  167. func (c *TwitterApi) AuthorizationURL(callback string) (string, *oauth.Credentials, error) {
  168. tempCred, err := c.oauthClient.RequestTemporaryCredentials(http.DefaultClient, callback, nil)
  169. if err != nil {
  170. return "", nil, err
  171. }
  172. return c.oauthClient.AuthorizationURL(tempCred, nil), tempCred, nil
  173. }
  174. // GetCredentials gets the access token using the verifier received with the callback URL and the
  175. // credentials in the first part of the handshake. GetCredentials implements the third part of the OAuth handshake.
  176. // The returned url.Values holds the access_token, the access_token_secret, the user_id and the screen_name.
  177. func (c *TwitterApi) GetCredentials(tempCred *oauth.Credentials, verifier string) (*oauth.Credentials, url.Values, error) {
  178. return c.oauthClient.RequestToken(http.DefaultClient, tempCred, verifier)
  179. }
  180. func defaultValues(v url.Values) url.Values {
  181. if v == nil {
  182. v = url.Values{}
  183. }
  184. v.Set("tweet_mode", "extended")
  185. return v
  186. }
  187. func cleanValues(v url.Values) url.Values {
  188. if v == nil {
  189. return url.Values{}
  190. }
  191. return v
  192. }
  193. // apiGet issues a GET request to the Twitter API and decodes the response JSON to data.
  194. func (c TwitterApi) apiGet(urlStr string, form url.Values, data interface{}) error {
  195. form = defaultValues(form)
  196. resp, err := c.oauthClient.Get(c.HttpClient, c.Credentials, urlStr, form)
  197. if err != nil {
  198. return err
  199. }
  200. defer resp.Body.Close()
  201. return decodeResponse(resp, data)
  202. }
  203. // apiPost issues a POST request to the Twitter API and decodes the response JSON to data.
  204. func (c TwitterApi) apiPost(urlStr string, form url.Values, data interface{}) error {
  205. resp, err := c.oauthClient.Post(c.HttpClient, c.Credentials, urlStr, form)
  206. if err != nil {
  207. return err
  208. }
  209. defer resp.Body.Close()
  210. return decodeResponse(resp, data)
  211. }
  212. // apiDel issues a DELETE request to the Twitter API and decodes the response JSON to data.
  213. func (c TwitterApi) apiDel(urlStr string, form url.Values, data interface{}) error {
  214. resp, err := c.oauthClient.Delete(c.HttpClient, c.Credentials, urlStr, form)
  215. if err != nil {
  216. return err
  217. }
  218. defer resp.Body.Close()
  219. return decodeResponse(resp, data)
  220. }
  221. // apiPut issues a PUT request to the Twitter API and decodes the response JSON to data.
  222. func (c TwitterApi) apiPut(urlStr string, form url.Values, data interface{}) error {
  223. resp, err := c.oauthClient.Put(c.HttpClient, c.Credentials, urlStr, form)
  224. if err != nil {
  225. return err
  226. }
  227. defer resp.Body.Close()
  228. return decodeResponse(resp, data)
  229. }
  230. // decodeResponse decodes the JSON response from the Twitter API.
  231. func decodeResponse(resp *http.Response, data interface{}) error {
  232. // Prevent memory leak in the case where the Response.Body is not used.
  233. // As per the net/http package, Response.Body still needs to be closed.
  234. defer resp.Body.Close()
  235. // Twitter returns deflate data despite the client only requesting gzip
  236. // data. net/http automatically handles the latter but not the former:
  237. // https://github.com/golang/go/issues/18779
  238. if resp.Header.Get("Content-Encoding") == "deflate" {
  239. var err error
  240. resp.Body, err = zlib.NewReader(resp.Body)
  241. if err != nil {
  242. return err
  243. }
  244. }
  245. // according to dev.twitter.com, chunked upload append returns HTTP 2XX
  246. // so we need a special case when decoding the response
  247. if strings.HasSuffix(resp.Request.URL.String(), "upload.json") {
  248. if resp.StatusCode == 204 {
  249. // empty response, don't decode
  250. return nil
  251. }
  252. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  253. return newApiError(resp)
  254. }
  255. } else if resp.StatusCode != 200 {
  256. return newApiError(resp)
  257. }
  258. return json.NewDecoder(resp.Body).Decode(data)
  259. }
  260. func NewApiError(resp *http.Response) *ApiError {
  261. body, _ := ioutil.ReadAll(resp.Body)
  262. return &ApiError{
  263. StatusCode: resp.StatusCode,
  264. Header: resp.Header,
  265. Body: string(body),
  266. URL: resp.Request.URL,
  267. }
  268. }
  269. //query executes a query to the specified url, sending the values specified by form, and decodes the response JSON to data
  270. //method can be either _GET or _POST
  271. func (c TwitterApi) execQuery(urlStr string, form url.Values, data interface{}, method int) error {
  272. switch method {
  273. case _GET:
  274. return c.apiGet(urlStr, form, data)
  275. case _POST:
  276. return c.apiPost(urlStr, form, data)
  277. case _DELETE:
  278. return c.apiPost(urlStr, form, data)
  279. case _PUT:
  280. return c.apiPost(urlStr, form, data)
  281. default:
  282. return fmt.Errorf("HTTP method not yet supported")
  283. }
  284. }
  285. // throttledQuery executes queries and automatically throttles them according to SECONDS_PER_QUERY
  286. // It is the only function that reads from the queryQueue for a particular *TwitterApi struct
  287. func (c *TwitterApi) throttledQuery() {
  288. for q := range c.queryQueue {
  289. url := q.url
  290. form := q.form
  291. data := q.data //This is where the actual response will be written
  292. method := q.method
  293. response_ch := q.response_ch
  294. if c.bucket != nil {
  295. <-c.bucket.SpendToken(1)
  296. }
  297. err := c.execQuery(url, form, data, method)
  298. // Check if Twitter returned a rate-limiting error
  299. if err != nil {
  300. if apiErr, ok := err.(*ApiError); ok {
  301. if isRateLimitError, nextWindow := apiErr.RateLimitCheck(); isRateLimitError && !c.returnRateLimitError {
  302. c.Log.Info(apiErr.Error())
  303. // If this is a rate-limiting error, re-add the job to the queue
  304. // TODO it really should preserve order
  305. go func(q query) {
  306. c.queryQueue <- q
  307. }(q)
  308. delay := nextWindow.Sub(time.Now())
  309. <-time.After(delay)
  310. // Drain the bucket (start over fresh)
  311. if c.bucket != nil {
  312. c.bucket.Drain()
  313. }
  314. continue
  315. }
  316. }
  317. }
  318. response_ch <- response{data, err}
  319. }
  320. }
  321. // Close query queue
  322. func (c *TwitterApi) Close() {
  323. close(c.queryQueue)
  324. }